0006 · 读完输入再动手

读完输入再动手

本节目标:能把单行多整数、多行整数、n×m 矩阵、混合行列与带空行的多组数据各读成指定的数据结构,并用最小样例与打印把输入层的错误定位出来。

第 1 课那道题只有一行输入,一个整数。第 4 课和第 5 课处理的 data 是已经切好的一串字符串。真实题面的输入要复杂得多:一行里塞好几个整数、一个数占一行、n \times m 的矩阵、行列数写在第一行、数据块之间夹着空行。

学生通用的做法是 input() 一行一行读,一边读一边算。遇到「第一行 n m,接下来 n 行每行 m 个整数」时,卡住的位置常常落在第一步:数据还没有完整地落进一个数据结构,后面每一步都在猜自己读到哪儿了。这一课把动作定下来:先用一次读入把整份输入变成数据结构,算法只面对数据结构。 这门课后面每一道题都从这一步开始。

输入没读完,别动手

把常见输入摊开,格式只有几类,每一类这一课都会给一份能跑的模板:

这些格式的共同点是:空白字符在承担分组的职责。换行与空格告诉程序哪些数属于同一行、哪些数属于同一组,而程序得先把这份分组关系还原成数据结构,才轮得到算法。

规矩只有一句:先读完、再算。 读入只发生一次,结果是一个数据结构;算法从数据结构出发,不再碰输入。

「边读边算」的问题在「先读完再算的规矩」一节用一份对照代码讲清。这里先说它的两种表现:读到的位置与你以为的位置不一致;出错时报的那一行离真正的原因很远。

一次读完的代价是 O(n) 的时间与额外空间——token 表与数据结构都随输入规模增长(第 2 课的口径:O(n) 的额外空间意思是这类开销随输入规模线性增长,在题目给的数据规模内属于常规开销)。它不改变复杂度档位,省下来的是「读指针现在到哪了」这件事。

把整个输入读成一块

标准输入在 Python 里有一个名字:sys.stdin。官方文档把它和另外两条流写在同一处:

文档紧接着说明:这些流都是常规文本文件,与 open() 函数返回的对象一致。这句话给了两件事。一是 sys.stdin 上能用的读法就是文件对象的读法(read、readline、遍历整个文件对象、readlines),课本讲文件读写那一节可以直接搬过来。二是 input() 并不在另一条通道上,它只是 stdin 上的一个便利入口——这个认识在讲提示文字那一节会变成一个判零分的坑。

sys.stdin.read() 把整份输入读成一个字符串。字符串上的 split() 不带参数时按任意空白切分:空格、制表符、换行都算分隔符,连续几个当作一个,首尾的空白丢掉。于是一整份输入变成一张 token 表。

import sys

text = sys.stdin.read()
tokens = text.split()
print(tokens)
print([int(x) for x in tokens])

样例 sample1.in 是一行 8 个整数:

3 1 4 1 5 9 2 6
$ cat sample1.in | python read_block.py
['3', '1', '4', '1', '5', '9', '2', '6']
[3, 1, 4, 1, 5, 9, 2, 6]

第一行打印出来的是 token 表:一整份输入在这里变成了一个字符串列表,每一项是一个 token。第二行是按 token 逐个转出来的整数列表。要留意的正是第一行——token 表里全是字符串,这件事在「读进来的都是字符串」一节会变成一个安静的坑。

官方文档还有一条备注:要从标准流写入或读取二进制数据,请使用底层二进制 buffer 对象。读入这一路上对应的是 sys.stdin.buffer.read(),它交回的是字节串,切出来的 token 是 bytes:

import sys

tokens = sys.stdin.buffer.read().split()
print(tokens[0], type(tokens[0]))
print(sum(map(int, tokens)))
$ cat sample1.in | python buffer_read.py
b'3' <class 'bytes'>
31

第一个 token 的类型是 bytes(打印成 b'3')。int() 也接受 bytes(实测 b'3' 得 3),所以 map(int, tokens) 照样能用。这层 buffer 省掉的是文本解码这一步,代价是 token 变成了 bytes:一旦某处要拿 token 与普通字符串比较或拼接,还得自己 decode()。普通题目用 sys.stdin.read() 就够。

读法之间差多少,本机实测一次(20 万个整数、每行一个):

读法本机实测
input() 一次读一行(20 万次调用)约 120 毫秒
for line in sys.stdin 逐行读约 65 到 90 毫秒
sys.stdin.read().split()约 70 毫秒
sys.stdin.buffer.read().split()约 70 毫秒
别背这些毫秒

数字是本机实测,换机器、换 Python 版本都会变。要记的是两条:这几种读法都落在同一档复杂度(都是 O(n)),差的是常数;逐行 input() 每次调用都要付一笔固定开销(第 4 课量过),20 万次叠起来本机慢了约一倍。哪种更快我没有找到官方给出的速度结论,上面的数字是自己量的。从「先读完」里拿到的主要东西是一份完整、可打印、可检查的数据结构;速度上的差别只是附带。

按行读还是一起读

按行切分的读法,官方教程在文件对象那一节写得很全,四条都用得上:

实测最直观。下面的程序把三种读法都用一遍(第三次调用是故意的):

import sys

line = sys.stdin.readline()
print('readline ->', repr(line))
print('readlines ->', repr(sys.stdin.readlines()))
print('input     ->', repr(input()))
$ cat sample4.in | python three_readers.py
readline -> '3 4\n'
readlines -> ['1 2 3 4\n', '5 6 7 8\n', '9 10 11 12\n', '2\n', '1 1\n', '3 4\n']
Traceback (most recent call last):
  File "three_readers.py", line 5, in <module>
    print('input     ->', repr(input()))
          ~~~~~^^
EOFError: EOF when reading a line

前两行确认了两条官方口径:readline() 交回的行末尾留着 \n('3 4\n'),readlines() 只是把剩下的行都装进一个列表,每一项同样带着 \n。最后一次调用报错是因为读取位置只能往前:readline() 拿走了第 1 行,readlines() 拿走了剩下全部,input() 被调用时已经到末尾。

整体读入也一样。sys.stdin.read() 读完之后再用 input(),同样撞 EOF:

import sys

data = sys.stdin.read().split()
print('读到的 token 数:', len(data))
n = int(input())
print('再读一行:', n)
$ cat sample4.in | python read_then_input.py
读到的 token 数: 19
Traceback (most recent call last):
  File "read_then_input.py", line 4, in <module>
    n = int(input())
          ~~~~~^^
EOFError: EOF when reading a line

结论很直接:一份程序里只用一种读入口,而且只读一次。 读完之后,所有需要的数据都在你手上。

input() 与 readline() 的差别只有一处:input() 交回的那一行去掉了行尾换行符。实测读 sample2.in 的第一行 5:

$ cat sample2.in | python input_strip.py
'5'
长度 1

两种切分的分工,用一张表记:

按空白切分 text.split()按行切分 text.splitlines()
分隔依据任意空白(空格、制表符、换行)换行符
换行去了哪消失,与空格等价行边界保留成列表元素
连续空白当作一个空行切出一个空字符串 ''
行尾换行符当作空白处理,不保留不保留(readline() 保留 \n)
适合的题面「给 n \times m 个整数」这类只讲个数的「每行一组」「每行一条记录」这类行本身有含义的
怕什么基本不怕,空白怎么排都行空行、折行、行首缩进

先读完再算的规矩

拿一份「边读边算」的程序看它怎么错。题面是:第一行 n m,接下来是 n \times m 个整数,求所有数之和。

import sys

n, m = map(int, input().split())
total = 0
for _ in range(n):
    row = list(map(int, input().split()))
    total += sum(row)
print(total)

同一份程序,两组输入。第一组是排版整齐的 sample3.in:

3 4
1 2 3 4
5 6 7 8
9 10 11 12
$ cat sample3.in | python rows_running.py
78

结果对。换一块数据(sample6.in),还是那 12 个数,只是换行位置被换过:

3 4
1 2 3 4
5 6
7 8
9 10 11 12
$ cat sample6.in | python rows_running.py
36

36 是错的,正确答案是 78,而这个程序一个错字都没报。错在读取这一层:input() 每被调用一次就吃掉一行,「第 i 次循环」于是与「输入的第 i 行」绑在了一起。这份程序认为每行恰好是矩阵的一行,而实际数据里有两行被折开了,于是它读到的三行是 1 2 3 4、5 6、7 8,最后一行 9 10 11 12 根本没被读。读过的位置拿不回来:前面读错一段,后面每一次读都跟着错。

换「先读完、再算」的写法,同一块数据:

import sys

def read_all():
    return sys.stdin.read()

def parse_matrix(text):
    tokens = text.split()
    n, m = int(tokens[0]), int(tokens[1])
    vals = [int(x) for x in tokens[2:2 + n * m]]
    return n, m, [vals[i * m:(i + 1) * m] for i in range(n)]

def solve():
    n, m, a = parse_matrix(read_all())
    print('n m =', n, m)
    for i, row in enumerate(a):
        print('第', i, '行长度', len(row), row)
    print('总和', sum(sum(row) for row in a))

solve()
$ cat sample6.in | python tokens_then_compute.py
n m = 3 4
第 0 行长度 4 [1, 2, 3, 4]
第 1 行长度 4 [5, 6, 7, 8]
第 2 行长度 4 [9, 10, 11, 12]
总和 78

78 是对的,原因落在 split() 上:它只认空白,不区分空白长什么样(一个空格、一行、三行都一样),所以换行怎么排都不影响切出来的 token 表;矩阵的三行是从 token 表上按 m 个一段切出来的,切的位置由 n、m 决定。

这次的 12 个数被折行,是我为了让错位看得见而改的排版;真实评测数据多半按题面排版给你,所以「边读边算」在很多题上也能过。它的代价在别处:这份程序把「输入的换行位置」当成了算法的一部分,一旦样例、别人分享的数据、或者从题面复制粘贴时换行被打乱,读指针就错位,而报错的位置与原因隔得很远(后面「多组数据与空行」里那份逐行版程序,报错在第 3 行,原因在第 2 行那个空行上)。

把这条规矩用第 4 课的骨架固定下来:

import sys

def read_all():
    """唯一碰标准输入的地方。"""
    return sys.stdin.read()

def parse_input(text):
    """只面对数据:字符串进来,数据结构出去。"""
    ...

def solve():
    data = parse_input(read_all())
    ...

solve()

三条纪律:

五种格式的解析模板

下面五份模板各是一个能跑的程序:一个 read_all()、一个解析函数、一个 solve()。样例输入存成了文件(sample1.in 到 sample6.in),跑法统一是 cat 样例文件 | python 程序.py。每份模板都标出它产出的数据结构。

单行多整数

样例 sample1.in:

3 1 4 1 5 9 2 6

one_line.py:

import sys

def read_all():
    """唯一读标准输入的地方:整份输入读成一个字符串。"""
    return sys.stdin.read()

def parse_ints(text):
    """按空白切分整份输入,逐个转成整数。"""
    return [int(x) for x in text.split()]

def parse_ints_loop(text):
    """同一件事的循环版。"""
    out = []
    for x in text.split():
        out.append(int(x))
    return out

def parse_ints_map(text):
    """同一件事的 map 版(第 5 课的工具)。"""
    return list(map(int, text.split()))

def solve():
    text = read_all()
    a = parse_ints(text)
    print(a)
    print(sum(a), max(a))
    print('推导式版与循环版一致:', a == parse_ints_loop(text))
    print('推导式版与 map 版一致:', a == parse_ints_map(text))
    print('token 表:', text.split())

solve()
$ cat sample1.in | python one_line.py
[3, 1, 4, 1, 5, 9, 2, 6]
31 9
推导式版与循环版一致: True
推导式版与 map 版一致: True
token 表: ['3', '1', '4', '1', '5', '9', '2', '6']

产出的数据结构:[3, 1, 4, 1, 5, 9, 2, 6],一个整数列表;sum 得 31,max 得 9。

转换这一步有三种写法,输出逐字一致(第 5 课那条:改写完必须对着原版比对输出):

写法代码什么时候用
列表推导式[int(x) for x in text.split()]默认写法
循环加 append先 out = [],再逐项 out.append(int(x))转换里还要顺手做别的事
maplist(map(int, text.split()))转换函数已经现成,这里是 int

map 返回迭代器(第 5 课),要收进列表才能反复用;只要直接喂给 sum、max 这类走一遍就完的函数,可以省掉 list(...),写成 sum(map(int, tokens))。

多行整数

样例 sample2.in,第一行给 n:

5
3
-1
4
2
7

many_lines.py:

import sys

def read_all():
    return sys.stdin.read()

def parse_n_then_lines(text):
    """第一行是 n,接下来 n 行每行一个整数。"""
    lines = [line for line in text.splitlines() if line.strip()]
    n = int(lines[0])
    return [int(line) for line in lines[1:n + 1]]

def solve():
    a = parse_n_then_lines(read_all())
    print(a)
    print(len(a), sum(a))

solve()
$ cat sample2.in | python many_lines.py
[3, -1, 4, 2, 7]
5 15

数据结构:[3, -1, 4, 2, 7],长度 5,与第一行的 n 对得上。第二个 print 就是输入层的自检——长度与题面给的个数一致,才轮到看答案 15 对不对。

三处边界:

不给 n 的版本,题面是「一行一个整数,给到输入结束」。样例 sample2b.in(第 4 行是空行,数据本身只有 5 个数):

3
-1
4

2
7

many_lines_eof.py:

import sys

def read_all():
    return sys.stdin.read()

def parse_ints_to_eof(text):
    """不给 n:一行一个整数,读到没有为止,空行跳过。"""
    return [int(line) for line in text.splitlines() if line.strip()]

def solve():
    a = parse_ints_to_eof(read_all())
    print(a)
    print(len(a), sum(a))

solve()
$ cat sample2b.in | python many_lines_eof.py
[3, -1, 4, 2, 7]
5 15

空行被 if line.strip() 跳过了,剩下的 5 个数一个不少。

矩阵 n×m

样例 sample3.in:

3 4
1 2 3 4
5 6 7 8
9 10 11 12

矩阵有两种读法,matrix.py 把两种都写出来对比:

import sys

def read_all():
    return sys.stdin.read()

def parse_matrix_by_lines(text):
    """按行读:第一行 n m,接下来正好 n 行、每行 m 个数。"""
    lines = [line for line in text.splitlines() if line.strip()]
    n, m = map(int, lines[0].split())
    return n, m, [list(map(int, line.split())) for line in lines[1:n + 1]]

def parse_matrix_by_tokens(text):
    """按空白读:token 从头取 n*m 个,再按 m 个一行切开。"""
    tokens = text.split()
    n, m = int(tokens[0]), int(tokens[1])
    vals = [int(x) for x in tokens[2:2 + n * m]]
    return n, m, [vals[i * m:(i + 1) * m] for i in range(n)]

def solve():
    text = read_all()
    n1, m1, a1 = parse_matrix_by_lines(text)
    n2, m2, a2 = parse_matrix_by_tokens(text)
    print('按行读 ->', a1)
    print('按空白读 ->', a2)
    print('两种读法一致:', a1 == a2)

solve()
$ cat sample3.in | python matrix.py
按行读 -> [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
按空白读 -> [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
两种读法一致: True

数据结构:[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],三行四列的二维列表(列表的列表)。取 a[1][2] 得 7:a[1] 是第二行 [5, 6, 7, 8],再取它的第 3 个元素(下标从 0 起)。

按空白读那一版的核心是两步:先把 n \times m 个数取成一段 vals,再按 m 个一段切开。

关于那段切片:

混合行列

样例 sample4.in:第一行 n m,接着是矩阵,然后一行给查询个数 q,再 q 行每行两个整数。

3 4
1 2 3 4
5 6 7 8
9 10 11 12
2
1 1
3 4

matrix_and_queries.py:

import sys

def read_all():
    return sys.stdin.read()

def parse_matrix_and_queries(text):
    """第一行 n m;接着 n*m 个数是矩阵;再一个 q;再 q 组「行 列」。"""
    tokens = text.split()
    n, m = int(tokens[0]), int(tokens[1])
    pos = 2
    vals = [int(x) for x in tokens[pos:pos + n * m]]
    pos += n * m
    matrix = [vals[i * m:(i + 1) * m] for i in range(n)]
    q = int(tokens[pos])
    pos += 1
    queries = []
    for _ in range(q):
        r, c = int(tokens[pos]), int(tokens[pos + 1])
        pos += 2
        queries.append((r, c))
    print('还剩没用的 token:', tokens[pos:])
    return matrix, queries

def solve():
    matrix, queries = parse_matrix_and_queries(read_all())
    print(matrix)
    print(queries)
    for r, c in queries:
        print(r, c, '->', matrix[r - 1][c - 1])

solve()
$ cat sample4.in | python matrix_and_queries.py
还剩没用的 token: []
[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
[(1, 1), (3, 4)]
1 1 -> 1
3 4 -> 12

数据结构是两块:matrix 是三行四列的二维列表;queries 是元组的列表 [(1, 1), (3, 4)]。最后两行说明查询里的行列从 1 数起(第 3 行第 4 列是 12),所以取值要写 matrix[r - 1][c - 1]。

pos 是这一版的要点:读的位置从隐式的读指针变成了一个显式的下标变量,可以打印、可以对照长度。最后那行 还剩没用的 token: [] 就是输入层的自检——token 一个不剩,说明个数都对上了;剩下若干个说明有数据没读,pos 越过末尾则说明读多了。

多组数据与空行

样例 sample5.in(组间有空行,末尾也有一个空行):

2

3
1 2 3

4
10 20 30 40

groups.py:

import sys

def read_all():
    return sys.stdin.read()

def parse_groups_with_t(text):
    """第一行是组数 t;每组先一行 n,再来 n 个数。"""
    tokens = text.split()
    t = int(tokens[0])
    pos = 1
    groups = []
    for _ in range(t):
        n = int(tokens[pos])
        pos += 1
        groups.append([int(x) for x in tokens[pos:pos + n]])
        pos += n
    print('读完时 pos =', pos, '/ token 总数 =', len(tokens))
    return groups

def solve():
    groups = parse_groups_with_t(read_all())
    print(groups)
    print([sum(g) for g in groups])

solve()
$ cat sample5.in | python groups.py
读完时 pos = 10 / token 总数 = 10
[[1, 2, 3], [10, 20, 30, 40]]
[6, 100]

数据结构:[[1, 2, 3], [10, 20, 30, 40]],列表的列表,每一组是内层列表。pos 与 len(tokens) 都是 10,说明空行什么都没影响——split() 把空行当成空白,切完就没了。这条也正是「末尾空行」与「组间空行」在按空白切分的读法下不需要额外代码的原因。

不给组数的版本(一直读到输入结束),样例 sample5b.in:

3
1 2 3

4
10 20 30 40

groups_to_eof.py 把 pos 指针换成 while pos < len(tokens):

import sys

def read_all():
    return sys.stdin.read()

def parse_groups_to_eof(text):
    """不给组数:一直读到 token 用完。"""
    tokens = text.split()
    pos = 0
    groups = []
    while pos < len(tokens):
        n = int(tokens[pos])
        pos += 1
        groups.append([int(x) for x in tokens[pos:pos + n]])
        pos += n
    return groups

def solve():
    print(parse_groups_to_eof(read_all()))

solve()
$ cat sample5b.in | python groups_to_eof.py
[[1, 2, 3], [10, 20, 30, 40]]

两种版本的差别只在有没有组数这一行,空行在两种版本里都不需要额外处理。

同一份输入用 input() 逐行读就会崩,崩的位置还很容易看错:

t = int(input())
for _ in range(t):
    n = int(input())
    a = list(map(int, input().split()))
    print(sum(a))
$ cat sample5.in | python groups_by_input.py
Traceback (most recent call last):
  File "groups_by_input.py", line 3, in <module>
    n = int(input())
ValueError: invalid literal for int() with base 10: ''

报错指的是第 3 行,int('') 的原因却是第 2 行那个空行——它被 input() 当成了一行的内容。找错的第一个动作是把它打出来看:print(repr(input())) 会显示 '\n',一看就知道读到的是一行空行,而不是一个数。

读进来的都是字符串

教材讲 input() 时给过一句要紧的话:使用函数 input() 时,Python 将用户输入解读为字符串。 书里的例子是问年龄,敲 21,变量拿到的是 '21'。它接着说:如果只想打印输入,这一点问题都没有;但要把它当数用,就会引发错误——书里那行 age >= 18 报的就是类型错误(在本机 3.14 上,这条报错现在的写法是 TypeError: '<' not supported between instances of 'str' and 'int')。

sys.stdin 这条路一样:token 表里全是字符串。两个字符串比较的时候不会报类型错误,规则换成了另一套:

print('10' < '9')
print('2' < '10')
print(sorted(['10', '9', '100', '2']))
print(sorted(['10', '9', '100', '2'], key=int))
print(max(['10', '9']), min(['10', '9']))
print(max([int(x) for x in ['10', '9']]))
$ python str_cmp.py
True
False
['10', '100', '2', '9']
['2', '9', '10', '100']
9 10
10

结果都反直觉:'10' 小于 '9';排序出来 '100' 排在 '2' 前面;max(['10', '9']) 得 '9',转成整数再求最大才是 10。

原因是字符串比较按字符逐个进行:从第 1 个字符起比编码顺序,一旦不同就定了输赢,长度根本轮不到参与。'1' 的编码是 49,'9' 的编码是 57,'10' 的第 1 个字符更小,结论当场就定下了。

编码可以用 ord() 直接看:ord('1') 得 49,ord('9') 得 57。

修法落在转换的时机上:在解析层一次转完,算法层看不到数字字符串。 这一课的五份模板都是这个结构——parse_* 交给 solve 的数据里,数字已经是 int。三条纪律:

一个字符串与一个整数混着比会当场报错:'10' < 5 得 TypeError: '<' not supported between instances of 'str' and 'int'。报错拦下你是好事;两个都是字符串时它不报错,这才是这个坑难查的原因。

用打印定位错误

本节的手法都围绕一件事:先把出错的位置缩小,再改代码。在二十行代码里随机加 print,只会让输出变长。

  1. 先跑最小样例。拿题面给的第一组样例(通常最小),不要从大样例开始。大样例只会告诉你「错了」,最小样例能让你把中间值一眼读完。
  2. 打印数据结构与它的长度。print('n m =', n, m)、print(len(row), row),与题面给的 n、m 对一遍。长度对不上就是解析层的问题,算法先别看。
  3. 用 repr() 看不可见的字符。print(repr(line)) 会把引号、\n、多余的空格显示出来(官方教程在讲输出格式时给过这条:只想快速显示变量进行调试,可以用 repr() 或 str())。空行导致的 ValueError 靠它一眼分清读到的是 '\n' 还是 ''。
  4. 把「读」与「算」分开打印。先打印全部数据结构,再打印算法结果:答案不对而数据结构是对的,问题在算法;数据结构本身就错,先修解析。这一步能省掉大量来回。
  5. 二分注释掉一半。确认数据结构没错之后,把 solve() 的后半先注释掉、或者先 return 前半的结果,看答案从哪一半开始不对,范围每次减半。
  6. 打印指针下标。token 版的解析里 pos 是显式变量,print('pos', pos, 'len', len(tokens)) 一句话告诉你读多了还是读少了(多组数据那份模板里就是这么自检的)。
  7. 改对了就收手。调试用的 print 必须在提交前删掉,原因见下一节。

走一遍完整的例子。要算矩阵所有数的和,切片右端写错了一个:

import sys

def read_all():
    return sys.stdin.read()

def parse_matrix(text):
    tokens = text.split()
    n, m = int(tokens[0]), int(tokens[1])
    vals = [int(x) for x in tokens[2:2 + n * m]]
    return n, m, [vals[i * m:i * m + m - 1] for i in range(n)]

def solve():
    n, m, a = parse_matrix(read_all())
    print('n m =', n, m)
    for i, row in enumerate(a):
        print('第', i, '行长度', len(row), row)
    print('总和', sum(sum(row) for row in a))

solve()
$ cat sample3.in | python slice_bug.py
n m = 3 4
第 0 行长度 3 [1, 2, 3]
第 1 行长度 3 [5, 6, 7]
第 2 行长度 3 [9, 10, 11]
总和 54

输出里的 n m = 3 4 说每行该有 4 个数,紧接着的「行长度 3」说只有 3 个,结论就出来了:每一行少了 1 个数,问题在切片的右端(i * m + m - 1)。改回 (i + 1) * m:

$ cat sample3.in | python slice_fixed.py
n m = 3 4
第 0 行长度 4 [1, 2, 3, 4]
第 1 行长度 4 [5, 6, 7, 8]
第 2 行长度 4 [9, 10, 11, 12]
总和 78

这次定位只用了两行输出,靠的是第 2 条手法。切片右端写错这类 bug 不会报错,只会让结果偏小,属于必须靠打印长度才能找出来的那一类。

提示文字混进标准输出

教材在讲 input() 时给了一条写法建议:每当使用函数 input() 时,都应指定清晰易懂的提示。它面向的程序是交互式的:程序暂停下来等人敲一行,提示告诉你该输入什么。蓝桥杯的评测机不读提示。它把标准输入接到数据文件上,运行你的程序,然后拿你的标准输出与标准答案逐字比对——多一个字符、多一行、少一个换行都算错。

input() 的提示走哪条流,官方文档在讲 sys.stdin 的地方写明了:stdout 用于 print() 和表达式语句的输出,以及用于 input() 的提示符。提示与答案走的是同一条流,评测机读的正是它。

实测一次,把两条流分别重定向到两个文件:

n = int(input("请输入 N:"))
print(n * 2)
$ echo 10 | python prompt_demo.py > out.txt 2> err.txt
$ cat out.txt
请输入 N:20
$ cat err.txt

err.txt 是空的。提示「请输入 N:」紧贴在答案 20 前面,占的是标准输出——在评测机上,这一行就是让逐字比对失败的那一行。

于是两条规矩:

这一课的模板顺带解决了第一条:读入走 sys.stdin.read(),input() 根本不出现,想加提示也没有地方加。哪道题非用 input() 不可的时候,提交前做两个搜索:

第 1 课记下的这条误解在这里落到手上:它从一条「知道就好」的注意事项,变成提交前要动手检查的两处。

样例与提交的两条路径

同一道题在这门课里要跑两条路:自测的时候你在本机跑,提交的时候评测机跑。两条路的接口是同一个——标准输入与标准输出。所以自测的做法是把样例输入存成文件,再把这个文件接到标准输入上:

$ cat sample1.in | python one_line.py
[3, 1, 4, 1, 5, 9, 2, 6]

Windows 的 cmd 里也可以写 python one_line.py < sample1.in;PowerShell 不支持 <,用 cat 文件 | python 程序.py 这一行。这条命令下程序读到的标准输入就是那个文件的内容,提交时评测机做的事与它一样,只是换了一份数据。

由此有三条自测纪律:

输入层写错的人常常以为是算法错了。数据少读了一段、多读了一位,算法拿到的数据结构就不对,算出来的结果自然不对,而报错信息可能落在算法那一半。先打印数据结构这一步,把这两件事分开了。

这一课用到的 Python

要做的事写法边界
一次读完sys.stdin.read()读完之后流停在末尾,再 input() 会 EOFError
按空白切分text.split()连续空白算一个,首尾丢掉;空串得 []
按行切分text.splitlines()不保留行尾换行符;readline() 会保留 \n
空行跳过if line.strip()int('')、int('\n') 都报 ValueError
转成整数[int(x) for x in tokens]与 list(map(int, tokens))、循环加 append 输出一致
快速读入sys.stdin.buffer.read().split()token 是 bytes,int() 同样接
n×m 铺成二维列表[vals[i*m:(i+1)*m] for i in range(n)]切片右端不包含;每行是新列表
多组数据t = int(tokens[0]) 加 pos 指针每组结束要 pos += n;读完打印 pos 与 len(tokens) 核对
一行一个整数[int(line) for line in text.splitlines() if line.strip()]行数由输入决定时用这版
看不可见字符print(repr(line))显示引号与 \n
读一行整数n = int(input())竞赛代码里括号不写参数;提示会写进 stdout
比大小、排序前先转 int'10' < '9' 是 True;sorted 要 key=int

这一课练的动作会一直用下去:紧接着的实验一要把读入、求解、输出组装成一个能跑的脚本,读入那一半就是这里的五份模板;第 3 课的容器选择决定解析出来的数据结构长什么样;第 5 课的推导式与 map 负责把 token 表变成数字。先读完、再算,后面每一道题都从这一步开始。

练习 五组样例与一次打印定位

这一课的动手部分落在 lab 里(任务与断言见 lab/0006-python-io-and-debug/,做完对照 lab/solutions/)。要做的是三件事。

  1. 备五组不同格式的样例(单行多整数、多行整数、矩阵、混合行列、末尾空行),每组用这一课的模板解析成指定的数据结构,并把结果打印出来与题面核对。每一组都要写清你产出的数据结构长什么样(每一维有多少个元素、元素是什么类型)。
  2. 在解析或求解里故意写错一处下标,先用最小样例跑一次,再用打印(长度、repr、指针下标)把出错范围缩到某一处,改对。记下你打印了什么、看到什么、据此得出什么结论。
  3. 交出去之前把调试用的 print 全部删掉,确认标准输出只剩答案那一行:拿样例输入跑一遍,输出与题面样例逐字对得上。
没看懂?随时问 把看不懂的段落(或报错)原样复制回会话,说一句哪里不懂就行,我只解释这一段,不打断你的进度。