Python输入输出五层阶梯:从控制台到系统调用的完整指南 很多初学者觉得 Python 的输入输出特别“简单”无非就是print加input学到第三天就会了。但等你真正做脚本处理日志、写爬虫落地数据、搞自动化测试的时候会发现输入输出这块的坑比想象中多得多编码错了、文件关了没、大文件读得慢、打印输出顺序乱了、日志怎么从内存里导出来……一头雾水。我自己带过不少人过这一关也踩过不少回坑。后来我把 Python 的输入输出拆成了五个层次从控制台到文件、从内存到系统调用这五层爬完基础知识会特别扎实。这篇就把这套思路完整写出来每层都有代码、有经验、有踩坑记录希望能给正在学 Python 的朋友一条可以照着走的路。另外提前说一句这篇是给想认真打好基础的人写的不是什么“一天速成攻略”。每一层都配有真实场景说明只记 API 不练场景意义不大。1. 为什么输入输出值得专门学先说说我对五层阶梯的划分逻辑先看一个最简单的程序name input(请输入名字) print(你好, name)表面上是“读一行、写一行”但背后其实是两条完全不同的数据通路。input从标准输入流读取print往标准输出流写入中间还要经过编码解码、缓冲处理、操作系统的文件描述符调度。这些背后的东西才是真正决定你程序健不健壮、性能好不好、能否跨平台的关键。我把输入输出学习拆成五层阶梯是照着数据流动路径来划分的层级核心对象典型 API对应场景第一层控制台交互print、input、格式化输出命令行小程序、交互脚本第二层文件系统open、with、pathlib日志处理、配置文件读写第三层内存缓冲区StringIO、BytesIO单元测试、日志缓存、参数构造第四层流与标准 IOsys.stdin、stdout、重定向管道协作、子进程通信、脚本重定向第五层序列化与系统调用json、pickle、os.read、缓冲机制数据落地、持久化、高性能读写这五层一旦打通你对“程序怎么和数据打交道”这件事会有整体认识。很多后面学网络编程、爬虫、数据处理的人之所以卡住往往就是文件 IO 和流的理解不够扎实。这也解释了为什么我把这篇的重点放在“概念的递进”上而不是单纯罗列print的 10 种玩法。API 是零散的理解是体系化的。只有体系化地学后面看到新 IO 工具才不会慌因为你已经知道它在数据通路的哪个环节发挥作用了。2. 第一层阶梯控制台输入输出这些细节没你想的那么简单控制台交互是每个 Python 学习者最早接触的输入输出方式也是很多人“自以为会了”的部分。但事实上print和input两个函数里藏着不少容易被忽略的参数和边界行为下面逐一拆开讲。2.1 print 完整参数详解sep、end、file、flush 怎么用print的完整定义是这样的print(*objects, sep , end\n, filesys.stdout, flushFalse)sep多个对象之间的分隔符默认是一个空格。end结尾符默认是换行。file输出的目标默认是标准输出可以改成文件对象或者内存缓冲区。flush是否立即把缓冲区里的内容推送到目标设备。很多人写进度条时会把end用得很熟练但是很少人关注flush。看这个例子import time for i in range(10): print(i, end, flushTrue) time.sleep(0.5)如果在终端里跑这段代码数据会“一个一个”蹦出来。如果去掉flushTrue程序也有可能把所有数字一次性在最后打印出来原因是输出默认走缓冲区只有攒到一定量或者程序结束才真正显示。这个特性在跑长期任务时特别关键。我之前写一个数据清洗脚本总共要处理 20 万条记录每条 1 到 2 秒整个任务要跑一晚上。当时往日志里打印进度由于没有flush第二天早上起来打开日志文件发现最新的进度信息根本没写进去程序中途断了都不知道卡在哪一步。从那以后只要涉及长时间运行的脚本我一律显式加flushTrue。2.2 input 的三个隐藏坑空白处理、类型转换、异常输入input本身只是个“读一行字符串”的接口但新手在这里经常出问题第一个坑是空白字符。input(请输入数字)返回的字符串带着用户敲的回车换行符如果你想干净地拿内容需要先.strip()去掉首尾空白s input(请输入).strip()第二个坑是类型转换。输入的全是字符串想做加减乘除必须自己int()或float()。更严谨的写法还要捕获异常try: num int(input(请输入一个整数).strip()) except ValueError: print(输入的不是整数)第三个坑是 EOF 情况。在管道场景下如果上游没有数据也没关闭输入流input()会抛EOFError。写命令行工具时最好用sys.stdin去判断是否有数据这个在第四层会详细展开。2.3 三种字符串格式化方式的特点与实践选型Python 里格式化字符串主要有三种方式%运算符、str.format()和 f-string。我的建议是优先用 f-string因为它最直观、可读性最高而且性能相对更好。name 数据 cnt 42 print(f共处理 {cnt} 条记录来源{name}) print(共处理 {} 条记录来源{}.format(cnt, name)) print(共处理 %d 条记录来源%s % (cnt, name))f-string 用起来最顺手但有一个细节要注意格式化时大括号里可以写表达式像f{a * b:.2f}不过别在里面写太复杂的逻辑否则代码很难读。另外早期 Python 版本对 f-string 里某些特殊字符有限制如果是旧环境用format()更稳妥。第一层阶梯说到底是“人 — 程序”之间的直接对话重点不在于你会不会写print而在于你对输出目标、格式、边界情况的理解是否到位。这层地基打不好后面几层的很多概念会很难建立起来。3. 第二层阶梯文件读写编码和资源管理一个都不能少控制台输入输出是“人机交互”文件读写则是“程序与存储交互”。绝大多数业务数据是存在磁盘上的读写文件是所有脚本绕不开的必修课。3.1 open 函数模式全解r、w、a、b、 到底怎么组合open()的第二个参数是 mode它决定你对文件的打开方式。常见组合和含义如下模式含义文件不存在时文件存在时r只读默认报错FileNotFoundError从头读w只写自动创建直接清空原内容a追加写自动创建保留原内容末尾追加r读写报错从头覆盖w读写自动创建清空原内容a追加加读自动创建保留原内容末尾追加b二进制模式与上面组合与上面组合这里最关键的认知是w会清空原文件。我用w模式写过一次自动化脚本本来想往已有配置文件里追加一条记录结果程序一跑原来的配置全部清空。那次教训让我记住了只要文件里原本有内容需要保留一律用a或先确认再写。同理想“既读又写”却不想清空原有数据用r而不是w。打开文件之后最标准的做法是配合with使用with open(data.txt, r, encodingutf-8) as f: content f.read()with的好处是即使中间抛异常文件也会被自动关闭不需要手动维护f.close()。手动调用close()在异常情况下极容易漏执行导致文件句柄泄漏。3.2 读文件应该选 read、readline 还是 readlines读文件的常用方法有三个.read()读取全部内容返回一个字符串或字节串。.readline()每次读一行。.readlines()读取全部行返回一个字符串列表。小文件随便用哪个都行但大文件场景差别很明显。一个 2GB 的日志文件直接.read()会吃掉大量内存机器可能直接卡死。正确姿势是逐行迭代with open(big.log, r, encodingutf-8) as f: for line in f: # 每行只占一块小内存处理完就释放 process(line)文件对象本身是迭代器直接for line in f每次只取一行这是处理大文件最省内存的写法。readlines()看起来方便实际上是把所有行一次性读进内存除非文件确实不大否则不建议用。另外还有一个细节Windows 下文本模式的换行符会被转换成\r\n或单独处理的机制。Python 的open()在文本模式下默认做通用换行转换也就是把读进来的\r\n转成\n写出时再转回去。跨平台处理文本文件时记住这一点能避免很多奇怪的问题。如果需要禁用这种转换可以把newline传进去。3.3 文件路径处理为什么推荐 pathlib新手学文件读写时最烦的就是路径拼接。直接写字符串拼接会踩系统差异的坑——Windows 用反斜杠Linux 和 macOS 用正斜杠。pathlib把这些差异全部封装好了from pathlib import Path base_dir Path(data) target base_dir / 2025 / result.csv target.parent.mkdir(parentsTrue, exist_okTrue)用/拼接路径跨平台没问题代码还简洁。还有.exists()、.read_text()、.write_text()这些高频方法日常处理文件非常顺手。除非你确实需要处理非常底层的原生路径逻辑否则我建议优先用pathlib而不是os.path。文件读写这层阶梯里还要提防编码错误。最常见的报错是UnicodeDecodeError原因往往是文件实际编码和打开时声明的编码不一致。读取外部文件时如果不能确定编码可以先尝试自动检测或者用errorsignore容错但这只是治标治本的办法是约定好编码比如内部系统一律统一用 UTF-8 写入。我自己写过很多批处理脚本处理的源文件来自各种老系统遇到过 GBK、拉丁文编码的文件最后统一在入口处识别编码再转换省去了大量后续麻烦。4. 第三层阶梯StringIO 和 BytesIO内存里也能做输入输出文件读写解决的是“磁盘数据”问题但还有一类场景数据并不需要落盘只是想临时借用一个“类文件”的接口来读写字符串或二进制数据。这就是StringIO和BytesIO存在的意义。4.1 什么是 StringIO把字符串变成可以读取和写入的文件StringIO位于io模块它让你把字符串当作文件来操作from io import StringIO buf StringIO() buf.write(第一行\n) buf.write(第二行\n) content buf.getvalue() print(repr(content))这段代码最终打印出第一行\n第二行\n。看起来就是把两个字符串拼一起但关键在于StringIO对象有完整的文件接口.read()、.write()、.readline()、.seek()都可以用。最常见的应用场景是单元测试。比如被测代码里调用了print()输出状态信息测试时你可以把sys.stdout换成StringIO然后断言输出内容这就是后面第四层“重定向”的经典配合。另一个场景是日志缓存。有些企业内部的日志系统不希望你直接写文件而是希望你把日志内容以字符串形式传给远程上报接口。借助StringIO你可以在内存里攒日志攒够了再一次上报灵活且不伤磁盘。4.2 BytesIO 与二进制数据的临时处理BytesIO和StringIO的区别是它处理的是字节串。读图片、压缩包、音频文件时经常需要把二进制数据从网络下载或从磁盘读入内存后临时使用类文件接口做解析。这时候直接BytesIO就好from io import BytesIO data bpython io bytes buf BytesIO(data) buf.seek(6) chunk buf.read(4) print(chunk) # bio b这个例子里seek直接跳到字节偏移位置定位非常灵活特别适合解析二进制格式的场景。4.3 为什么内存 IO 很重要统一接口的价值很多初学者不理解“为什么不直接操作字符串”要绕一圈用StringIO。核心原因是很多函数只接受文件对象或类文件对象而不是字符串。与其写两套分支不如把数据放到StringIO里统一走文件接口。我做过的某个模拟项目中有一个数据清洗函数需要从文件读取原始数据后来数据源变成了 API 返回的字符串。如果我当时把函数写成只接受文件路径就得大改但如果我写成接受任何类文件对象直接传入StringIO就能复用改动成本非常小。这就是“面向接口编程”的思想不关心数据流从哪里来、到哪里去只关心它符合怎样的读写协议。第三层阶梯学会的是io模块的抽象能力这为后面理解流、管道、网络 socket 打好了基础。5. 第四层阶梯流与标准输入输出重定向理解程序的数据管道到了这一层要把“文件读写”和“控制台交互”统一起来了。Python 的输入输出本质上是流stream文件和标准输入输出都是一样的流对象只是流的来源和目的地不同。这样想很多抽象就顺了。5.1 标准输入、标准输出和标准错误到底指什么每个进程启动之后系统默认给三个打开的文件描述符名称文件描述符Python 对象默认去向标准输入0sys.stdin键盘输入标准输出1sys.stdout终端显示标准错误2sys.stderr终端显示平时用print()其实就是在往sys.stdout里写数据。而input()是从sys.stdin里读数据。关键经验标准错误应该和标准输出分开用。正常输出走stdout错误和警告走stderr。这样在做日志采集时你可以只把错误重定向到错误文件而正常日志进正常日志文件排查问题时互不干扰。写命令行工具时我也坚持这个约定错误信息一律用print(..., filesys.stderr)。5.2 在程序内部重定向redirect_stdout 的高效用法如果不想改变整个进程的输出只想在某一段代码里临时捕获输出可以用contextlib.redirect_stdoutfrom contextlib import redirect_stdout from io import StringIO buf StringIO() with redirect_stdout(buf): print(这段内容会被捕获) print(不会直接显示到终端) print(捕获内容:, buf.getvalue())这段代码里被缩进的print全部写进了buf外部打印不受影响。这个技巧在写测试用例时非常实用被测函数在内部大量调用print你可以干干净净地捕获全部输出逐行断言是否符合预期。5.3 用 print 的 file 参数实现一变多输出print的file参数接受任意类文件对象所以你可以同时输出到多个目标。比如既要保证终端能看到又想把内容写进文件import sys log_file open(run.log, a, encodingutf-8) def dual_print(msg): print(msg) print(msg, filelog_file) log_file.flush()这里的要点是调用flush()否则日志文件可能长时间不落盘进程强行结束就丢了内容。5.4 缓冲机制什么时候立刻写入什么时候攒一批流式 IO 的核心机制是缓冲。默认情况下Python 的sys.stdout如果连接到终端是行缓冲的遇到换行就刷新如果重定向到文件则是块缓冲的攒到一定字节数才写盘。简单验证方法是python -c import sys; print(hi); input() out.txt在另一个终端查看out.txt你会发现可能是空的因为输出被块缓冲吃掉了进程没退出字还没写盘。对需要实时进度的脚本务必手动设置flushTrue或者调sys.stdout.flush()。这是我在写长跑任务时用得最频繁的操作之一。流重定向这层的核心收获是程序不是封闭的它的输入输出可以来自别处也可以导向别处。懂了这个理解“管道”、“重定向”、“子进程交互”就轻松了。6. 第五层阶梯序列化与系统级 IO把数据和外部世界打通前四层讲的是文本流怎么流动第五层开始处理结构化的数据和更底层的 IO。简单说这层解决“数据结构怎么变成可存储、可传输的字节流”的问题也触及“系统调用那一层是怎么读写数据”的问题。6.1 JSON 的输入输出序列化与反序列化跨语言、跨系统传递结构化数据JSON 是目前最通用的方案之一。Python 里用json模块import json data {name: demo, scores: [80, 90, 100]} # 序列化写入文件 with open(data.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) # 反序列化从文件读取 with open(data.json, r, encodingutf-8) as f: loaded json.load(f) print(loaded[scores])这里有两个高频细节ensure_asciiFalse可以让中文保持原样而不是变成\uXXXX转义indent2让输出的 JSON 带缩进肉眼可读。如果是开发环境我都会开缩进如果是线上批量写尽量不开缩进能省空间省 IO。6.2 二进制数据与字节级读写遇到图片、音频、二进制协议文件文本模式无法处理必须用二进制模式。读写方式类似但内容是byteswith open(image.png, rb) as f: header f.read(8) print(header)在字节级读取的基础上可以用struct模块解析二进制格式import struct # 把 4 字节整数 2 字节短整数 1 字节字符解析出来 with open(binary.dat, rb) as f: raw f.read(8) a, b, c struct.unpack(i h c, raw)日常业务中解析二进制格式的协议比如自定义采集设备的数据包需要用到这种能力。学到这里输入输出就不仅仅局限于文本文件了。6.3 系统级 IOos.read、os.write 与缓冲的关系Python 层面的open会帮你做缓冲、编码等很多事情但更底层的是操作系统提供的读写接口。用os.read和os.write可以直接对文件描述符操作import os fd os.open(data.txt, os.O_RDONLY) chunk os.read(fd, 1024) os.close(fd)这里第三个参数表示最多读多少字节。os.read不一定一次读满因为底层数据可能暂时不够这种“未定义读取长度”的容忍度是对各种实际情况的适配。相比之下Python 的文件对象已经封装好会根据需要读更多数据来满足你的.read(1024)请求。两者的差异了解即可日常开发用文件对象是首选只有在嵌入式、高性能框架等场景下才需要直接操作系统调用。第五层的另一个重点是pickle。它可以把 Python 对象原样序列化成字节流import pickle obj {name: demo, data: [1, 2, 3]} with open(obj.pkl, wb) as f: pickle.dump(obj, f)但要记住一个原则pickle只在 Python 程序之间传递数据时使用不可用于跨语言通信也绝对不能加载来路不明的 pickle 数据否则可能执行恶意代码。能用 JSON、CSV 这类标准格式就不要用 pickle。6.4 五种常见序列化格式怎么选格式适用范围优点缺点JSON跨语言通信易读、生态好不支持日期等复杂类型CSV表格数据处理极简、Excel 友好类型信息弱picklePython 内部序列化支持任意对象不安全、不能跨语言XML文档类数据结构标准、校验强冗长、解析慢二进制自定义高性能场景最节省空间需要自己定义规则选择格式时先想清楚数据要传给谁、需要保留多久、需不需要跨语言读取然后再决定。7. 典型报错场景与排查思路规避常见的 IO 坑输入输出模块的错误五花八门但归拢一下常见问题集中在几个固定方向。下面把我在实际开发和教学中遇到的高频错误整理出来附上定位思路。7.1 编码相关的报错最常见的报错是UnicodeDecodeError: utf-8 codec cant decode byte 0xba in position 0: invalid start byte处理思路先确认文件本身的真实编码再用对应编码打开。如果文件来源不确定可以用chardet或charset-normalizer做检测。日常保持一个习惯所有内部写入全部用 UTF-8所有外部读取先确认编码能省掉大部分这类问题。7.2 文件占用与句柄泄漏Windows 下经常出现PermissionError: [Errno 13] Permission denied原因可能是文件被 Excel 打开或者前一个程序没释放句柄。排查步骤检查是否有其他进程占用了该文件。确认自己代码里没写open而没close。检查是不是用w模式打开一个只读文件操作。尽量使用with块可以大幅降低这类问题。7.3 输入输出顺序混乱有时候打印日志明明先调用了print(start)但实际输出里end反而先出现。这是缓冲造成的。处理方案是显式加flushTrue或者开启python -u无缓冲模式。这个看起来小但在多进程日志场景里影响很大。7.4 文件路径不存在报错一般是FileNotFoundError。排查路径时别依赖“当前工作目录”这个隐式设置最好把执行入口在哪个目录说清楚或者直接基于脚本文件所在目录拼接路径from pathlib import Path base Path(__file__).resolve().parent target base / data / input.csv这样无论脚本从哪里启动路径都是确定的。7.5 自检清单检查项具体内容编码读写两端编码是否一致内部是否统一 UTF-8模式w会不会清空不该清空的文件关闭是否用with有无文件句柄泄漏路径用的是相对路径还是绝对路径是否受启动位置影响缓冲长时间任务是否忘记flush大文件是否误用read()一次读入内存8. 进阶扩展这五层还可以往哪里延伸五层阶梯学完之后还可以往几个方向继续延伸所有延伸方向都建立在已经打好的 IO 基础上。第一个方向是网络 IO。把socket当作一种流来读写服务器和客户端之间通过字节流通信本质上就是用send/recv替代write/read。如果理解了前面“流”的统一性网络编程会好上手很多。第二个方向是异步 IO。用asyncio做高并发读写时文件读写和网络请求都是通过事件循环调度open对应的aiofiles、网络请求对应的aiohttp背后依然是输入输出的核心模型。第三个方向是内存映射文件。对大文件做随机读写时mmap可以把磁盘文件映射到虚拟内存读写像操作内存一样。处理非常大的结构化二进制文件这个方向很实用。第四个方向是压缩与归档。gzip、zipfile、tarfile都提供了类文件对象你可以像读写普通文件一样操作压缩包内容先读压缩包里的对象再解析不需要先解压。这些方向不必急着全学但只要理解了数据流动路径再看任何 IO 工具你看到的都是“读入、处理、写出”三个环节的延长和组合。这里再分享一个我正在用的学习思路把每个 IO 功能都写成“最小可运行示例”然后尝试更换数据源和数据目的地。比如同一段处理逻辑输入可以从文件来也可以从BytesIO来输出可以到终端、到文件、到内存缓冲区。反复做这种替换练习你会迅速把零散知识点编织成网络。根据我个人的带人经验能把第五层吃透的人写出的代码在工程上往往更稳。因为 IO 是最容易出错、也最容易影响性能的环节理解越深你越能在设计阶段就避开大量隐患。这篇五层阶梯拆到这里是希望能帮你建立一个整体的 IO 视图剩下的就是动手多写多换场景了。