人们都说 python 语言简洁优雅,表达力强。下面我们通过温州中学 python 训练平台上的一些题目来~~体会它的简洁性~~(完成一些最短代码挑战)。

警告⚠:请不要直接复制本文中的代码提交到 OJ 上。

if-else 表达式

if-else 不仅可以作控制语句,也可作表达式:

判断能否构成三角形

a, b, c = map(int, input().split())
print('yes' if a+b > c and a+c > b and b+c > a else 'no')

也可以堆叠:

密码翻译

conv = lambda c: 'a' if c == 'z' else 'A' if c == 'Z' else chr(ord(c)+1) if c.isalpha() else c
print(''.join(map(conv, input())))

注意过多的堆叠可能导致代码可读性下降。在本文所展示的大多代码中,为了完成最短代码挑战,还使用了短变量名,省略中间变量,lambda 函数等手段。读者在实践中应当考虑权衡代码可读性。

可迭代对象与函数式编程初探

让我们从读入一行空白字符分割的整数说起:a, b, c = map(int, input().split())

input() 从标准输入中读入一行;_.split() 将其按照空白字符将其分割成一个字符串的列表;map(int, _)int 函数作用于列表中每一个元素(将每一个元素转换为整数型),并返回一个迭代器。将其解包并赋值给 a, b, c

>>> input()
11 45 14
'11 45 14'
>>> _.split()
['11', '45', '14']
>>> map(int, _)
<map object at 0x7f290437cca0>
>>> list(_) # 为了查看迭代器的所有元素,将其转换为列表
[11, 45, 14]
>>> a, b, c = _
>>> print(a, b, c)
11 45 14

下面介绍 mapfilter 的语法。

map 的作用就是对一个序列中的每一个元素作一个变换,其名字可以理解为映射。简单的 map 语法是 map(func, iter),其中 func 是一个函数对象,iter 是一个可迭代对象。假如 iter 对应的序列为 [a0, a1, a2, ...] 那么 map 的返回值就对应 [func(a0), func(a1), func(a2), ...]

filter 的语法是 filter(predicate, iter),返回值也是一个迭代器。predicate 是一个接受一个参数,返回一个布尔值的函数对象。它将 iter 中每一个使 predicate 取值为真的元素筛选出来。比如:

>>> def is_odd(x):
...     return x%2 == 1
... 
>>> list(filter(is_odd, [c, b, a]))
[45, 11]

再比如:

讲话模式

import re
s, d = input().lower(), {}
for wd in re.findall('[a-z]+', s):
	d[wd] = d.get(wd, 0) + 1
mf = max(d.values())
print(min(filter(lambda x: d[x] == mf, d)), mf)

解释:当上述程序运行到末两行时,d 已经是一个含有所有单词及其对应出现次数的字典。mf 是单词出现次数的最大值。lambda x: d[x] == mf 是一个函数对象,传入参数 x,如果字典 dx 对应的值(单词 x 的出现次数)等于 mf(出现次数最多),它返回真。filter(_, d) 则从字典的键中选出“出现次数最多”的键(单词),并 min(_) 取出字典序最小者。

诶,好像节省的码量不多(

请注意:程序中的正则表达式 [a-z]+ 不能准确描述题目中所定义的单词,但暂时能通过所有数据。

正则表达式博大精深,在此篇幅所限不作介绍。

可见,python 的函数式编程非常依赖迭代器的概念。基于迭代器的内置函数还有很多。

潜伏者

def q(): print('Failed'); exit()
p1, p2, p3, d, s = input(), input(), input(), {}, set()
for u, v in zip(p1, p2):
    if u in d and d[u] != v: q()
    d[u] = v
for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
    if c not in d or d[c] in s: q()
    s.add(d[c])
print(''.join(d[c] for c in p3))

解释:第三行中, zip(p1, p2) 将两个可迭代对象同时遍历,节省了枚举下标访问带来的时间开销、码量(有时,还有更多变量带来的含义混乱)。具体地,zip(p1, p2) 会返回一个迭代器,对应序列 [(p1[0], p2[0]), (p1[1], p2[1]), (p1[2], p2[2]), ...]for 会遍历这个序列并将每个元组解包赋值给 u, v

重组最大数

print(''.join(sorted(input(), reverse=1)))

解释:sorted(x) 返回 x 中所有元素升序排序后的列表。

生成器表达式和列表推导式

方括号中含有 for 的表达式,称为列表推导式,可以推导出一个含有自变量的表达式的列表,如:

>>> [2*x + 1 for x in [a, b, c]]
[23, 91, 29]

方括号换成圆括号,则是生成器表达式,生成器返回的是迭代器而不是列表,这样就可以支持惰性求值,加快运行速度。一个特性是生成器表达式的圆括号可以与函数调用的括号共用。

寻找完美数

a, b = int(input()), int(input())
print('\n'.join(str(x) for x in [6, 28, 496, 8128] if a <= x <= b))

解释:[6, 28, 496, 8128] 预先算好数据范围内的所有完美数。(str(x) for x in _ if a <= x <= b) 将列表中在给定范围内的数字选出来,转成字符串。

诶,好像 map 和 filter 能做到的,生成器表达式和列表推导式都能做,那么要 map 和 filter 有何用呢?

数字统计

l, r = map(int, input().split())
print(sum(str(x).count('2') for x in range(l, r+1)))

**(11月8日更新)**极端的运用:

彩色图像转成黑白图像

import sys
raw = sys.stdin.readlines()
n, m = map(int, raw[0].split())
bw = [''.join(map(lambda r, g, b: str(+(r*0.299 + g*0.587 + b*0.114 < 132)),
                  *[map(int, ln.split(','))]*3)) for ln in raw[1:1+n]]
print('\n'.join(bw))
for ln in raw[n+2:]:
    i, j = map(int, ln.split())
    print(bw[i-1][j-1])

解释(图像的一行是如何处理的):

  • 正号添加在逻辑值前面,可以将其提升为整数(0 或 1),所以 conv = lambda r, g, b: str(+(r*0.299 + g*0.587 + b*0.114 < 132)) 是一个接受三个参数,计算黑白值并返回字符串 '0''1' 的函数。
  • ln 是输入文件中的一行;it = map(int, _.split(',')) 是一个迭代器,含有这一行中以逗号分隔的整数;[it]*3 是含有三个这个迭代器的列表;map(conv, *_) 将该列表解包传给 map,相当于 map(conv, it, it, it)。由于 python 保证参数的求值顺序是从左到右,且刚刚的列表乘法是元素的浅拷贝,所以三个元素是同一个迭代器——这非常关键,所以 map 会三次访问 itconv 就按顺序得到连续的三个整数(而不是同一个整数三次)。(如果你能看懂这里 more-itertools 中的 grouper 函数,那么你应该能看懂这里的。)''.join(_),就能得到一个表示黑白图像的一行的 01 字符串。

参考资料

最后,欢迎读者来信(qq邮箱)讨论。