先讲一个很多刷题党都经历过的场景:笔试给你两小时,前面那道题你五分钟就想清楚了算法思路,结果卡在读取输入上——样例怎么都读不对,调了半天才敢写核心逻辑,最后时间不够用,能跑的代码也来不及提交。这种憋屈,刷过题的基本都懂。python编程的笔试里,输入输出看着是基础,实际上它是第一道真正的门槛:评测机只认标准输入和标准输出,你的代码逻辑再漂亮,I/O 写得不得体,照样给你一个红色的大叉。这篇文章不聊算法设计,专门聊笔试里的输入输出,把最常见的读取场景、输出格式、性能取舍和排雷经验一次性讲透,适合正在准备笔试和面试、但总在输入输出环节翻车的同学参考。
1. 笔试的裁判是什么:先弄懂评测机怎么判你的代码
1.1 标准输入、标准输出与“面向输出”的判题方式
我见过不少第一次参加线上笔试的同学,拿到题目第一反应是打开文件、读文件、写文件,有的甚至写死了一个路径去读in.txt。这其实不怪他们,平时写脚本习惯了文件读取,但笔试评测机和本地脚本不一样。
在线评测系统做的事情非常简单:它把预先准备好的测试数据当作标准输入喂给你的程序,然后捕获你的程序写到标准输出的内容,再和标准答案做比对。整个过程不涉及文件路径,不需要你打开任何文件,只要你的程序老老实实从stdin读、往stdout写,就没问题。
所以第一课是:你的程序永远不要自己去猜数据文件在哪,也不要试图读取某个路径,直接用输入函数从标准输入拿数据。在这个前提下,输入输出的代码就成了一种固定套路,你练熟了,遇到新题只是套模板而已。
这里还要理解评测机比对答案的方式。不同平台的比对策略不完全一样,但大体上它会把你的输出逐字符和标准答案比对。行尾多余的空格、中间漏掉的换行、多余的空白字符,都可能直接导致答案错误。有些平台会忽略行尾空格和末尾换行,但绝大多数平台不会帮你合并多行,也不会自动补全缺少的空格。换句话说,你的输出必须满足题目要求的格式,差一个空格都可能有风险。
1.2 四类最常见的输入输出约定
我刷题和辅导别人刷题这些年,把笔试里的输入输出模式总结成了四类,你先把这四类认清楚,后面读代码就不会慌:
- 第一类:单行输入,多个值,比如“第一行包含两个整数 a 和 b”。
- 第二类:多行定长输入,比如“第一行一个整数 n,接下来 n 行每行 m 个数”。
- 第三类:不定行数输入,直到文件结束(EOF),比如“多组测试数据,每组一行,处理到文件结束”。
- 第四类:带格式的输入,比如用逗号、冒号分隔,或者带前缀字符串的输入,需要额外加工。
题目通常在描述里会写得很明确,但很多人读题只盯着算法部分,把开头几行的输入约定扫一眼就过去了,等到写代码时才发现读不进去。我的习惯是反过来:先看输入输出约定,再决定怎么读、怎么组织数据结构,最后才看算法本身。这个顺序真的很重要。
2. 读取方式怎么选:input 与 sys.stdin 的正确用法
2.1 input() 的底层逻辑和适用边界
Python 初学阶段接触最多的就是input()。它的用法很简单,input()会把读到的内容作为字符串返回,并且去掉末尾的换行符。比如输入一行1 2 3,input()返回的是字符串"1 2 3",不是列表,也不是数字,你需要自己处理。
但很多人不知道,input()的底层其实就是封装了sys.stdin.readline(),差别在于它会把末尾的换行符去掉,并且内部做了字符串解码。因此在数据量小的时候用input()完全没问题,代码也简洁。
什么时候该换用sys.stdin?我的经验是:当你觉得自己可能要处理几千行、几万行甚至更多数据时,input()的性能就不够看了。它每调用一次都要经历一层封装和字符串处理,循环里成千上万次调用,累计开销很可观。真实笔试里,有些题目数据量能到十万行、百万行,此时input()可能让你的程序超时。
这里要补一个底层知识:sys.stdin本身是一个文本流对象,而sys.stdin.buffer是它的底层字节流。直接读字节流,省掉了字符解码的环节,性能明显更好。你在字节流上 read 得到的是 bytes,split()之后得到的也是字节片段,但好消息是int()可以直接处理bytes,所以map(int, data.split())这种写法依然成立。
2.2 sys.stdin.buffer:大数据量下的性能利器
我在本地模拟过一个场景,生成了 100 万行整数数据,每行 3 个数字,分别用input()逐行读和sys.stdin.buffer.read()一次性读再做split()。实测下来,input()那套方案跑了九百多毫秒,sys.stdin.buffer.read().split()方案只要两百毫秒左右。数据量越大,差距越明显。
所以我的选择标准很朴素:
- 数据量小、行数少、只是为了快速写题:直接用
input(),简单可靠,代码可读性高。 - 数据量可能很大,或者题目的测试数据祖传变态:上
sys.stdin.buffer。
这里并不是说input()一定会超时,而是说,能用一行代码避免性能风险,为什么不直接用?笔试环境里,运行时间卡得很死的时候,早一秒跑完就多一分保险。
2.3 三个可复用的读取模板
为了方便,我把常用的读取模板整理成了三种,你可以根据题型直接套用。
模板一:一次性读取所有数据,适合先拿到完整数据再处理的题目。
import sys def solve(): data = sys.stdin.buffer.read().split() # data 是字节片段的列表,比如 [b'3', b'5', b'1', b'2', b'3'] # 需要时可以直接 int(x) 转换 # 接下来按数组下标取数即可 it = iter(data) n = int(next(it)) # ...模板二:按行读取,适合需要逐行判断、逐行处理的题目。
import sys for line in sys.stdin.buffer: # 每行都是 bytes,注意末尾带有换行符 line = line.strip() if not line: continue a, b = map(int, line.split()) # 处理逻辑模板三:用iter(callable, sentinel)逐行读到 EOF,适合需要手动控制循环的场景。
import sys readline = sys.stdin.buffer.readline for line in iter(readline, b''): line = line.strip() # 处理逻辑这个iter(callable, sentinel)算一个冷门但好用的技巧。它不断调用readline,直到读到的内容等于哨兵值b''(也就是 EOF)时停住。比起while True: line = readline(); if not line: break这种写法,它少两行,也不容易漏 break,我强烈建议你把它记下来。
3. 输入场景拆解:从单行定长到矩阵和不定行数
3.1 单行多值:strip、split、map 三件套
笔试里最常见的输入就是“一行多个值”。比如题目写“第一行包含两个整数 a 和 b”,那你写:
a, b = map(int, input().split())这行代码拆开来看是三层:split()默认按空白字符分割,连续的空格、制表符、换行符都会被当成同一个分隔符;map(int, ...)把分割后的字符串列表逐个转成整数;最后解包赋值给a和b。绝大多数“一行 N 个数”的输入,一行代码就能解决。
但这里有几个坑。第一,split()返回的是一个可迭代对象,如果你的输入行有前导空格或结尾空格,split()会把它处理干净,不需要额外strip()。第二,如果你遇到的是“每行一个数字”,不要写int(input())之外的花哨代码,直接int(input())就行。第三,如果一行要读入的值个数不固定,不能硬解包,先接成列表再说:
nums = list(map(int, input().split()))我经常看到同学在“一行多个数”的地方非要循环读,读半天读几行也不对,其实根源就是没有意识到一行可以一次性全部读完。
3.2 行数已知:列表推导式读矩阵
另一类高频输入是矩阵或者二维数组。题目会给出行数 n 和列数 m,紧接着给 n 行数据。这种情况下,最简洁的读法是列表推导式:
n, m = map(int, input().split()) matrix = [list(map(int, input().split())) for _ in range(n)]一行一行读进来,每行按空格分割后转成列表,最终得到嵌套列表。这个写法效率不错,代码也短。如果你的矩阵元素不是数字而是字符,比如:
3 3 ### #.# ###那就不需要split()了,直接list(input().strip())就能把每行拆成字符列表:
n, m = map(int, input().split()) grid = [list(input().strip()) for _ in range(n)]这里strip()的作用是去掉行尾的换行符,不然list()会把'\n'也拆进去,后面处理起来全是坑。
关于矩阵,还有一个常见痛点:有些题目的输入里,行末可能有多余空格,比如“1 2 3 ”这样。用split()会自然忽略末尾空格,不用担心。
3.3 行数未知:EOF 循环的两种标准写法
不定行数这种题型出镜率极高,因为评测系统没有告诉你有多少组数据,你的程序必须自己判断什么时候读完。标准做法是读到 EOF 就停。
第一种写法,利用sys.stdin.buffer的迭代特性:
import sys for line in sys.stdin.buffer: line = line.strip() if not line: # 跳过空行,很多人会在这里纠结 continue # 处理一行数据第二种写法,用while配合input()捕获异常:
while True: try: line = input() except EOFError: break if not line: continue # 处理一行数据我个人更推荐第一种。它天然适配 EOF 语义,代码量也少。第二种适合那些必须知道“这是第几行”或者要在循环里做特殊处理的场景,但它依赖EOFError异常,这种风格有人觉得绕,有人觉得稳,你自己选顺手的就行。
还有一个小细节:如果题目保证每一行都有数据,不会出现空行,那么if not line: continue这个判断其实可以省略。有些平台会在文件末尾多放一个空行,这时不处理空行可能导致int()转换报错,所以我习惯保留这个判断。
3.4 奇怪格式输入的处理技巧
笔试里偶尔会出现花式输入,比如一行数据是:
1,2,3,4,5或者:
a=1, b=2这种情况下,split()就不好使了,需要先做一次字符替换。我的处理顺序是:先看分隔符是什么,再用replace()把特殊分隔符统一替换成空格,最后走标准流程。
以逗号分隔的数字为例:
line = input().strip() nums = list(map(int, line.replace(',', ' ').split()))如果分隔符是,但元素前后还带着空格,比如"1, 2, 3",上面的写法依然有效,因为split()会把多余空格忽略掉。如果格式是"a=1, b=2",那就更花哨一些,一般用正则表达式提取数字比较省事:
import re nums = list(map(int, re.findall(r'\d+', input())))这种题不多见,但一旦遇到,re.findall能救你一命。它直接提取字符串里所有数字,省去手动 split 的麻烦。要注意,如果题目里还有负号,你需要把正则改成-?\d+。
4. 输出的隐形扣分点:从 print 到 join 与格式化
4.1 print 默认行为带来的三个坑
print()在笔试里也有讲究。第一,print()默认在末尾追加换行符,这本身没问题,问题是很多人不知道它一次输出多个参数时会用空格分隔。比如:
print(1, 2, 3)输出的是一行1 2 3,中间有空格。如果题目要求每两个数字之间以空格分隔,那这个行为刚好符合要求;如果要求连续输出,就不能这么写。
第二,print()默认结尾是\n,如果题目要求一行末尾不要换行,你需要用end=''或者end=' '来手动控制。但我要提醒你,绝大多数笔试题目不要求你输出到“一行无换行”,你正常换行反而更安全。
第三,如果你想在循环里逐行输出,最忌讳的是疯狂调用print()。数据量大时,输出本身也是一种开销,频繁调用print()会拖慢程序。更好的做法是先把所有输出内容收集到一个列表里,最后一次性用'\n'.join(lines)输出,或者直接sys.stdout.write(...)。
4.2 批量输出:用 join 统一管理格式
我最推荐的输出策略是“收集后统一输出”。比如你要输出多行结果,可以这样:
res = [] for i in range(n): # 计算当前行结果 res.append(f"Case #{i+1}: {ans}") sys.stdout.write("\n".join(res))注意这里join之后通常不需要再加末尾换行。很多评测平台对末尾换行的容忍度不同,但你不加末尾换行也没问题,加了也不扣分。真正会被扣分的是:该换行的地方没换行,该空格的地方没空格。
再举一个常见的坑:输出矩阵时,每行元素之间以空格分隔,行尾不能有多余空格。有人写出这样的代码:
for row in matrix: for x in row: print(x, end=' ') print()这样每行末尾会多一个空格,风险很大。用join就不会出这个问题:
for row in matrix: print(' '.join(map(str, row)))join天然保证行尾没有多余空格,这是我最喜欢它的原因。
4.3 浮点数精度与对齐输出
涉及到浮点数输出时,题目通常会明确说“保留两位小数”或者“四舍五入保留三位小数”。这时候不要用round()去碰运气,直接格式化。
Python 3 里最舒服的写法是 f-string:
print(f"{value:.2f}")它支持四舍五入到指定小数位,配合f"{value:8.3f}"还能做占位对齐。如果你在做老项目用format(),写法是"{:.2f}".format(value),效果一样。
还有一个隐藏细节:输出大量浮点数时,格式化操作本身也有性能开销。如果数据量巨大,建议把所有格式化后的字符串收集到列表里再一次性输出,而不是边算边 print。格式化后的结果已经是一个字符串,放在列表里没问题。
5. 实测排雷:本地没问题,提交为什么错了
5.1 换行符、多余空格和隐藏字符
排雷环节,我先讲一个我自己踩过的坑。有段时间我在某个平台答题,本地跑样例一切正常,但提交后频繁答案错误。排查了半天,最后发现是我读入的数据每行末尾带着\r。问题出在测试数据是用某些工具生成的,行尾是\r\n,我的代码在split()时没遇到问题,但凡是需要对字符串本身做处理的题,比如统计字符、比较字符串,就全乱了。
解决方案很简单:读一行之后先line.strip()再处理。strip()会去掉行尾的\r和\n,也能去掉行首空格。加不加这一行,遇到特殊数据时就是天壤之别。
所以我的统一原则是:凡是按行读的字符串,先 strip 再往下走。凡是按空格拆的,直接用split(),不用 strip。
5.2 调试输出污染 stdout
这是另一个经典事故。本地调试时为了看中间结果,写了一堆print("中间值:", x),代码提交前忘了删,评测系统把你的调试信息当成答案输出,当然答案错误。
我的土办法:调试信息写进sys.stderr,这样本地能看到输出,评测系统捕获 stdout 时不会被污染。
import sys print("debug:", val, file=sys.stderr)如果你确实用print调试,提交前就用“查找替换”把所有print(全删掉,或者干脆统一加注释标记。总之,输出口要控制好。
5.3 本地运行正常但提交报错的排查清单
当你遇到“样例全过但提交报错”时,按照下面这个顺序排查:
- 检查代码里有没有操作本地文件路径。
- 检查有没有未删除的调试输出。
- 检查读取输入时有没有
strip()或者split()处理不当。 - 检查循环里有没有死循环风险,比如
while True但缺少退出条件。 - 检查递归深度,是不是爆栈了。
- 检查数据量大的时候,
input()是否导致超时。
大部分“本地能跑提交就挂”的问题,基本逃不出上面几条。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 答案错误,本地却通过 | 输出行尾有额外空格或换行不合理 | 用join生成输出,检查每行内容 |
| 答案错误,且输出乱码 | strip()没调用,\r被当成数据 | 读字符串后统一strip() |
| 运行时错误 | 读取时遇到空行,int()转换失败 | 加if not line: continue |
| 运行时错误 | 索引越界,读取的数据比预期少 | 检查split()后长度,必要时打印看一下 |
| 超时 | 数据量大但用input()逐行读 | 改用sys.stdin.buffer.read() |
| 超时 | 循环内频繁print() | 收集结果,最后一次性输出 |
| 答案错误 | 调试print()污染 stdout | 写入sys.stderr或提交前删除 |
这张表我建议你收藏,笔试前翻一遍,能省不少冤枉时间。
6. 实例拆解:两道典型题目的完整输入输出代码
6.1 经典求和:多组数据直到 EOF
题目描述通常是这样的:“输入包含多组测试数据,每组数据一行,包含两个整数 a 和 b,处理到文件结束为止,对每组数据输出 a+b。”
这道题的标准输入输出代码,用模板二就能解决:
import sys def solve(): out_lines = [] for line in sys.stdin.buffer: line = line.strip() if not line: continue a, b = map(int, line.split()) out_lines.append(str(a + b)) sys.stdout.write("\n".join(out_lines)) if __name__ == "__main__": solve()注意我先把结果放进了out_lines,最后一次性输出。这样做的好处是,无论有多少组数据,输出格式都稳定,也不会因为频繁print()拖慢速度。
如果题目要求读到0 0结束,那就在循环里加一个判断:
for line in sys.stdin.buffer: line = line.strip() if not line: continue a, b = map(int, line.split()) if a == 0 and b == 0: break out_lines.append(str(a + b))这里的break和continue要分清楚:continue跳过错觉为空的行,break才是真正退出整个循环。
6.2 矩阵转置:输出格式控制的完整示范
题目描述:“输入第一行两个整数 n 和 m,接下来 n 行每行 m 个整数,输入矩阵的第 j 行第 i 列元素是原矩阵第 i 行第 j 列元素,输出转置后的矩阵。”
读矩阵和输出转置的完整代码:
import sys def solve(): data = sys.stdin.buffer.read().split() if not data: return it = iter(data) n = int(next(it)) m = int(next(it)) matrix = [] for _ in range(n): row = [int(next(it)) for _ in range(m)] matrix.append(row) out_lines = [] for j in range(m): col = [str(matrix[i][j]) for i in range(n)] out_lines.append(" ".join(col)) sys.stdout.write("\n".join(out_lines)) if __name__ == "__main__": solve()这里我直接用read().split(),把所有数字一次性读出来,再通过iter依次取数。不管输入分成几行、中间有多少换行,全部被split()抹平了,程序根本不受换行影响。这种做法在矩阵类题目里格外好用。
输出转置矩阵时,关键是“按列取元素”拼成新的行,用join控制行内空格,最外层join控制行与行之间的换行。整个输出格式稳如泰山。
结尾
刷题刷到现在,我的体会是:笔试里的输入输出,练的不是“会不会用函数”,而是“能不能把题目的描述转成稳定的代码习惯”。我建议你专门花几个晚上,把本文里的模板全部敲一遍,不要复制,一行一行自己写,写到不用思考就能默写的程度。等你上手了就会发现,所谓卡输入输出,其实是把精力浪费在了不该浪费的地方。把这部分固定下来,你的注意力才能真正放到算法上,笔试也才有底气。再退一步,真遇到没见过的输入格式,记住一个原则:先 strip,再 split,能用 buffer 就不 input,能 join 就别手工拼字符串。按这个思路走,输入输出这道坎,你跨过去只是时间问题。