跳转到内容

推导式与生成器表达式

  • 要造一个列表 → 用列表推导(listcomp)。它的目的写在脸上:造列表。
  • 要把数据喂给别的构造器(tuple()、array()、sum())→ 用生成器表达式(genexp)。它不造中间列表。
  • 不是为了用那个列表 → 两个都别用。写 for 循环。

对比两段做同一件事的代码:

# for 循环版
codes = []
for symbol in symbols:
codes.append(ord(symbol))
# 列表推导版
codes = [ord(symbol) for symbol in symbols]

for 循环能干的事太多了:累加、筛选、调用副作用、拼字符串。看到 for 你得读完循环体 才知道它在干嘛。看到 [] 加 for,你立刻知道:它在造一个新列表。

这就是「可读性」的来源,不是短,是意图显式。

beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))

两段等价,但第二段你得在脑子里过一遍:map 把 ord 作用到每个元素,filter 保留 大于 127 的,最后 list() 一下。第一段就是「对每个 s,如果 ord(s) > 127,就收下 ord(s)」。

顺带一个我以前搞错的印象:map/filter 并不比推导式快。书里也澄清了这一点。

笛卡尔积:for 子句的顺序就是嵌套顺序

Section titled “笛卡尔积:for 子句的顺序就是嵌套顺序”
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> [(c, s) for c in colors for s in sizes]
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')]

结果顺序等于把两个 for 按书写顺序嵌套。想让结果先按尺码分组,就把两个 for 对调:

>>> [(c, s) for s in sizes for c in colors]
[('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'), ('black', 'L'), ('white', 'L')]

第 1 章的 FrenchDeck 初始化就是这么写的:

self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

先按花色分组,组内按点数 —— 因为 for suit 写在外面。

语法一样,把方括号换成圆括号:

>>> tuple(ord(s) for s in symbols)
(36, 162, 163, 165, 8364, 164)
>>> array.array('I', (ord(s) for s in symbols))
array('I', [36, 162, 163, 165, 8364, 164])

两个细节:

  1. 生成器表达式是函数调用的唯一参数时,圆括号可以省(tuple(...) 那行)。
  2. 有多个参数时不能省(array(...) 那行,'I' 是第一个参数,生成器是第二个)。

省的不是时间,是峰值内存。用笛卡尔积举例更容易看出差别:

for tshirt in (f'{c} {s}' for c in colors for s in sizes):
print(tshirt)

如果两个列表各有 1000 项,用推导式要先造一个 100 万项的列表才能开始循环; 用生成器表达式一项一项给你,那个列表从不出现。

一、推导式有自己的局部作用域。

>>> x = 'ABC'
>>> codes = [ord(x) for x in x]
>>> x
'ABC'

for 子句里的 x 是推导式内部的变量,跑完就没了,没污染外面的 x。这是 Python 3 的 行为,Python 2 里会泄漏出来。

二、但海象运算符 := 绑定的变量会留下来。

>>> codes = [last := ord(c) for c in x]
>>> last
67
>>> c
NameError: name 'c' is not defined

last 还在,c 没了。区别在于 := 的目标变量的作用域属于外层函数,不是推导式。

tests/test_ch02.py 里对应 test_comprehension_scoping,断言了三件事: x 还是 'ABC'、last == 67、c 不在作用域里。