列表推导和生成器表达式
列表推导是构建列表的一种方式,生成器表达式可以用来创建其他任何类型的序列。
列表推导
列表,可以通过多种方式构建:
. 使用一对方括号表示空列表:[]
. 使用方括号,里面的项用逗号隔开:[a],[a, b, c]
. 使用类型构造器:list() 或者 list(iterable)
. 使用列表推导:[x for x in iterable]
以上都是生成列表的形式。其中,列表推导更具有可读性。
列表推导和可读性
通过下面两个示例,对比使用方括号和列表推导:
示例 1:将字符串变为 Unicode 码位的列表
>>> letters = "abcdefg"
>>> codes = []
>>> for letter in letters:
... codes.append(ord(letter))
...
>>> codes
[97, 98, 99, 100, 101, 102, 103]
示例 2:将字符串变为 Unicode 码位的另一种写法
>>> letters = "abcdefg"
>>> codes = [ord(letter) for letter in letters]
>>> codes
[97, 98, 99, 100, 101, 102, 103]
两者想比较下,列表推导更具有可读性。当然,列表推导乱用也会有副作用。通常的原则是,用列表推导创建新列表的时候,尽可能简短。若列表推导超过两行,可考虑使用 for 循环创建。但并没有硬性要求,选择权在使用者手上。
变量泄露
在 Python 2 中,列表推导存在变量泄露问题。但在 Python 3 中,这个问题将不会再出现。
Python 2 列表推导示例:
Python 2.7.12 (default, Oct 8 2019, 14:14:10)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> x = "this word"
>>> dummy = [x for x in "abc"]
>>> x
'c'
从结果可见,x 的值被取代了。
这是 Python 3 中的代码:
Python 3.6.5 |Anaconda, Inc.| (default, Apr 29 2018, 16:14:56)
[GCC 7.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x = "this word"
>>> dummy = [x for x in "abc"]
>>> x
'this word'
>>> dummy
['a', 'b', 'c']
x 的值没有发生改变,dummy 也创建成功。
过滤元素
这里尝试同 filter() 和 map() 结合比较。
使用列表推导和 map/filter 结合创建同样的列表
>>> letters = "abcdefg"
>>> match_portion = [ord(letter) for letter in letters if ord(letter) > 100]
>>> match_portion
[101, 102, 103]
>>> match_portion = list(filter(lambda x: x > 100, map(ord, letters)))
>>> match_portion
[101, 102, 103]
这是两者各自的实现方式
In [4]: %timeit [ord(letter) for letter in letters if ord(letter) > 100]
764 ns ± 4.36 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [5]: %timeit list(filter(lambda x: x > 100, map(ord, letters)))
1.37 µs ± 5.37 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
这里是尝试比较两者的运行速度,可以看出, map/filter 组合比列表推导要慢些。
map() 函数接受两个参数,一个是函数,一个是 Iterable,map 将传入的函数依次作用到序列的每个元素,并把结果作为新的 Iterator 返回。
filter() 函数也接受一个函数和一个序列。和 map() 不同,filter() 把传入的函数依次作用于每个元素,然后依据返回值是 True 还是 False 决定保留还是丢弃该元素。
map 和 filter 更详细的讨论,会在后面另开篇幅进行阐述。
生成器表达式
虽然列表推导能够用来初始化元组,数组或者其他序列类型,但是生成器表达式是更好的选择。因为生成器表达式遵守迭代器协议,是逐个生产出元素,而不是创建一个完整的列表,然后把列表传递到某个构造函数中。两者比较,显然是生成器表达式更节约内存。
生成器表达式的语法与列表推导类似,只是将方括号换成圆括号。
示例用生成器表达式初始化元组
>>> letters = "abcdefg"
>>> tuple(ord(letter) for letter in letters)
(97, 98, 99, 100, 101, 102, 103)
注意:如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外在用括号将它们围起来。
>>> tuple(ord(letter) for letter in letters)
(97, 98, 99, 100, 101, 102, 103)
>>> tuple((ord(letter) for letter in letters))
(97, 98, 99, 100, 101, 102, 103)
效果相同,若生成器表达式是唯一参数,不需要额外括号。
尝试使用生成器表达式实现笛卡尔积,示例如下:
>>> letters = ['a', 'b', 'c', 'd', 'e', 'f']
>>> nums = ['1', '2', '3']
>>> for le_num in ('{} {}'.format(le, num) for le in letters for num in nums):
... print(le_num)
...
a 1
a 2
a 3
b 1
b 2
b 3
c 1
c 2
...
在这里,生成器表达式会在每次 for 循环运行的时候才产生一个组合,所以内存不会留下组合的列表。若两个列表元素数量非常多,生成器表达式在这里就能大大减少 for 运行的开销。
此处只是简单介绍生成器如何初始化列表外的序列,以及避免额外的内存占用。详细的生成器工作原理,后续同样会另开篇幅进行阐述。
参考资料
来源
David M. Beazley;Brian K. Jones.Python Cookbook, 3rd Edtioni.O’Reilly Media.2013.
“4. Built-in Types”.docs.python.org.Retrieved 14 January 2020
廖雪峰.“Python 教程”.liaoxuefeng.com.[2020-01-17].
欢迎关注微信公众号《书所集录》