《流畅的python》— 列表推导与生成器表达式
阅读原文时间:2023年08月24日阅读:1

列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列。如果你的代码里并不经常使用它们,那么很可能你错过了许多写出可读性更好且更高效的代码的机会。

很多Python 程序员都把列表推导(list comprehension)简称为listcomps,生成器表达式(generator expression)则称为genexps。我有时也会这么用。

1、列表推导

>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[36, 162, 163, 165, 8364, 164]

for 循环可以胜任很多任务:遍历一个序列以求得总数或挑出某个特定的元素、用来计算总和或是平均数,还有其他任何你想做的事情。在示例2-1 的代码里,它被用来新建一个列表。

另一方面,列表推导也可能被滥用。以前看到过有的Python 代码用列表推导来重复获取一个函数的副作用。通常的原则是,只用列表推导来创建新的列表,并且尽量保持简短。如果列表推导的代码超过了两行,你可能就要考虑是不是得用for 循环重写了。就跟写文章一样,并没有什么硬性的规则,这个度得你自己把握。

2、生成器表达式

虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节省内存。

生成器表达式的语法跟列表推导差不多,只不过把 方括号换成圆括号 而已。下面这个示例展示了如何用生成器表达式建立元组和数组。

>>> 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.9.1 节中有更多关于数组的详细讨论。

下面这个示例则是利用生成器表达式实现了一个笛卡儿积,用以打印T恤衫的 2 种颜色和 3 种尺码的所有组合。与上一个示例不同的是,用到生成器表达式之后,内存里不会留下一个有 6 个组合的列表,因为生成器表达式会在每次 for 循环运行时才生成一个组合。如果要计算两个各有 1000 个元素的列表的笛卡儿积,生成器表达式就可以帮忙省掉运行 for 循环的开销,即一个含有 100 万个元素的列表。

>>> 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 恤样式的列表。


3、总结:两者区别

  • 生成器表达式每次处理一个对象,而不是一口气处理和构造整个数据结构,这样做可以节省大量的内存,在处理的数据量较大时,最好考虑使用生成器表达式而不是列表推导式;
  • 得到的值不一样,列表推导式得到的是一个列表,生成器表达式获取的是一个生成器,但是你可以遍历输出。而且列表推导式可以多次迭代,生成器表达式只能单次迭代。