列表推导和生成器表达式
列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来
创建其他任何类型的序列。如果你的代码里并不经常使用它们,那么很
可能你错过了许多写出可读性更好且更高效的代码的机会。
如果你对我说的“更具可读性”持怀疑态度的话,别急着下结论,我马上
就能说服你。
很多 Python 程序员都把列表推导(list comprehension)简称为
listcomps,生成式表达器(generator expression)则称为 genexps。
我有时也会这么用。
列表推导和可读性
先来个小测试,你觉得示例 2-1 和示例 2-2 中的代码,哪个更容易读
懂?
示例 2-1 把一个字符串变成 Unicode 码位的列表
>>> symbols = '$¢£¥€¤'
>>> codes = []
>>> for symbol in symbols:
... codes.append(ord(symbol))
...
>>> codes
[36, 162, 163, 165, 8364, 164]
示例 2-2 把字符串变成 Unicode 码位的另外一种写法
>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[36, 162, 163, 165, 8364, 164]
虽说任何学过一点 Python 的人应该都能看懂示例 2-1,但是我觉得如果学会了列表推导的话,示例 2-2 读起来更方便,因为这段代码的功能从
字面上就能轻松地看出来。
for 循环可以胜任很多任务:遍历一个序列以求得总数或挑出某个特定
的元素、用来计算总和或是平均数,还有其他任何你想做的事情。在示
例 2-1 的代码里,它被用来新建一个列表。
另一方面,列表推导也可能被滥用。以前看到过有的 Python 代码用列表
推导来重复获取一个函数的副作用。通常的原则是,只用列表推导来创
建新的列表,并且尽量保持简短。如果列表推导的代码超过了两行,你
可能就要考虑是不是得用 for 循环重写了。就跟写文章一样,并没有什
么硬性的规则,这个度得你自己把握。
句法提示
Python 会忽略代码里 []、{} 和 () 中的换行,因此如果你的代码里
有多行的列表、列表推导、生成器表达式、字典这一类的,可以省
略不太好看的续行符 \。
列表推导不会再有变量泄漏的问题
Python 2.x 中,在列表推导中 for 关键词之后的赋值操作可能会影
响列表推导上下文中的同名变量。像下面这个 Python 2.7 控制台对
话:
Python 2.7.6 (default, Mar 22 2014, 22:59:38)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 'my precious'
>>> dummy = [x for x in 'ABC']
>>> x
'C'
如你所见,x 原本的值被取代了,但是这种情况在 Python 3 中是不
会出现的。
列表推导、生成器表达式,以及同它们很相似的集合(set)推导
和字典(dict)推导,在 Python 3 中都有了自己的局部作用域,就
像函数似的。表达式内部的变量和赋值只在局部起作用,表达式的
上下文里的同名变量还可以被正常引用,局部变量并不会影响到它
们。
这是Python 3 代码:
>>> x = 'ABC'
>>> dummy = [ord(x) for x in x]
>>> x ➊
'ABC'
>>> dummy ➋
[65, 66, 67]
>>>
➊ x 的值被保留了。
➋ 列表推导也创建了正确的列表。
列表推导可以帮助我们把一个序列或是其他可迭代类型中的元素过滤或
是加工,然后再新建一个列表。Python 内置的 filter 和 map 函数组合
起来也能达到这一效果,但是可读性上打了不小的折扣。
列表推导同filter和map的比较
filter 和 map 合起来能做的事情,列表推导也可以做,而且还不需要
借助难以理解和阅读的 lambda 表达式。详见示例 2-3。
示例 2-3 用列表推导和 map/filter 组合来创建同样的表单
>>> symbols = '$¢£¥€¤'
>>> beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
>>> beyond_ascii
[162, 163, 165, 8364, 164]
>>> beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
>>> beyond_ascii
[162, 163, 165, 8364, 164]
我原以为 map/filter 组合起来用要比列表推导快一些,Alex Martelli
却说不一定——至少在上面这个例子中不一定。在本书的代码仓库
(https://github.com/fluentpython/example-code)中有名为 02-arrayseq/
listcomp_speed.py(https://github.com/fluentpython/examplecode/
blob/master/02-array-seq/listcomp_speed.py)的脚本,代码中有这两
个方法的效率的比较。
笛卡儿积
如前所述,用列表推导可以生成两个或以上的可迭代类型的笛卡儿积。
笛卡儿积是一个列表,列表里的元素是由输入的可迭代类型的元素对构
成的元组,因此笛卡儿积列表的长度等于输入变量的长度的乘积,如图
2-2 所示。
图 2-2:含有 4 种花色和 3 种牌面的列表的笛卡儿积,结果是一个包
含 12 个元素的列表
如果你需要一个列表,列表里是 3 种不同尺寸的 T 恤衫,每个尺寸都有2 个颜色,示例 2-4 用列表推导算出了这个列表,列表里有 6 种组合。
示例 2-4 使用列表推导计算笛卡儿积
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors for size in sizes] ➊
>>> tshirts
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'),
('white', 'M'), ('white', 'L')]
>>> for color in colors: ➋
... for size in sizes:
... print((color, size))
...
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
>>> tshirts = [(color, size) for size in sizes ➌
... for color in colors]
>>> tshirts
[('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'),
('black', 'L'), ('white', 'L')]
➊ 这里得到的结果是先以颜色排列,再以尺码排列。
➋ 注意,这里两个循环的嵌套关系和上面列表推导中 for 从句的先后
顺序一样。
➌ 如果想依照先尺码后颜色的顺序来排列,只需要调整从句的顺序。
我在这里插入了一个换行符,这样顺序安排就更明显了。
在第 1 章的示例 1-1 中,有下面这样一段程序,它的作用是生成一个按
花色分组的 52 张牌的列表,其中每个花色各有 13 张不同点数的牌。
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
列表推导的作用只有一个:生成列表。如果想生成其他类型的序列,生成器表达式就派上了用场。下一节就是对生成器表达式的一个简单介
绍,其中可以看到如何用它生成列表以外的序列类型。
生成器表达式
虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成
器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协
议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这
个列表传递到某个构造函数里。前面那种方式显然能够节省内存。
生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而
已。
示例 2-5 展示了如何用生成器表达式建立元组和数组。
示例 2-5 用生成器表达式初始化元组和数组
>>> symbols = '$¢£¥€¤'
>>> tuple(ord(symbol) for symbol in symbols) ➊
(36, 162, 163, 165, 8364, 164)
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols)) ➋
array('I', [36, 162, 163, 165, 8364, 164])
➊ 如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要
额外再用括号把它围起来。
➋ array 的构造方法需要两个参数,因此括号是必需的。array 构造
方法的第一个参数指定了数组中数字的存储方式。
示例 2-6 则是利用生成器表达式实现了一个笛卡儿积,用以打印出上文
中我们提到过的 T 恤衫的 2 种颜色和 3 种尺码的所有组合。与示例 2-4
不同的是,用到生成器表达式之后,内存里不会留下一个有 6 个组合的
列表,因为生成器表达式会在每次 for 循环运行时才生成一个组合。如
果要计算两个各有 1000 个元素的列表的笛卡儿积,生成器表达式就可
以帮忙省掉运行 for 循环的开销,即一个含有 100 万个元素的列表。
示例 2-6 使用生成器表达式计算笛卡儿积
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in ('%s %s' % (c, s) for c in colors for s in sizes): ➊
... print(tshirt)
...
black S
black M
black L
white S
white M
white L
➊ 生成器表达式逐个产出元素,从来不会一次性产出一个含有 6 个 T
恤样式的列表。
第 14 章会专门讲到生成器的工作原理。这里只是简单看看如何用生成
器来初始化除列表之外的序列,以及如何用它来避免额外的内存占用。
接下来看看 Python 中的另外一个很重要的序列类型:元组(tuple)。