目录

List Comprehension and Generator Expression

前言

本文将简单地介绍 Python 中的列表推导式以及生成器表达式,用一些实例描述它们的基本使用方法,并阐述它们的本质区别。

注意

本文中的命令行示范,都是基于以下环境:

  • 操作系统: CentOS 7
  • Python: 3.7.9

 

列表推导式

什么是列表推导式

Python 的list comprehension,是用于构建列表的快捷方式,基于一种描述语句来创建一个列表,类似于数学上的 集合建构式符号。“list comprehension"这个词语,没有统一的中文译法,这里暂且采用主流书籍中的译法:列表推导式。有一些英语技术网站上会把列表推导式简称为 listcomps,知道即可。

如果你的 Python 代码中并不是经常使用它,那么你可能错过了许多写出可读性更好且更高效的代码的机会。举个例子,以下两段代码哪个更容易读懂:

示例1:

1
2
3
4
5
6
even_numbers = []
for i in range(10):
  if i % 2 == 0:
    even_numbers.append(oct(i)

print(even_numbers)

示例2:

1
2
even_numbers = [oct(x) for x in range(10) if x % 2 == 0]
print(even_numbers)

虽然任何学过一点 Python 的人应该都能读懂示例1的代码,但是应该没有人会否认示例2的代码更简短,而且读起来更方便,因为这段代码的功能从字面上就能轻松读懂。

示例2中的[oct(x) for x in range(10) if x % 2 == 0]就是一个列表推导式。

如何写列表推导式

列表推导式采用列表的方括号表达。中间的第一部分是生成结果元素的表达式,上述例子中就是求变量 x 的八进制值 oct(x),当然你也可以用任何和 x 相关的其它表达式,比如 abs(x) 等;第二部分是 for 循环,表示如何选取变量 x;第三部分是判断式,表示第二部分选取的变量 x 是否需要做过滤。

要读懂列表推导式,推荐做法是从第二部分开始读,看看右边有没有过滤条件,最后再将筛选出来的变量放到第一部分的表达式上去。

上述例子用中文表达,就是:从0到9的整数中,选取偶数,计算它的八进制值,并放在列表中。

列表推导式甚至可以有多个 for 循环,如果表达式需要多个变量的话。比如:

1
even_numbers_addition = [x + y for x in range(10) if x % 2 == 0 for y in range(10) if y % 2 == 0]

上述例子中,表达式是对 x 和 y 求和,变量 x 和 y 如何选?右边用了两个 for 循环以及各自的判断式,告诉你 x 和 y 的选取方式。这个列表推导式还可以换一种写法:

1
even_numbers_addition = [x + y for x in range(10) for y in range(10) if x % 2 == 0 and y % 2 == 0]

两种写法的执行结果完全一样。

列表推导式的优点很明显,就是代码简洁、可读性强,所以什么时候适合用列表推导式,有两个原则:

  • 只用来创建新的列表
  • 列表推导代码不超过两行

如果构建列表的代码超过两行,你可能就要考虑是不是用普通 for 循环重写了。

同 filter/map 的比较

一般来说,列表推导式能做的事,filter/map也能做,本文开头第一个例子,用 filter/map 来实现,如下:

1
2
even_numbers = list(map(oct, filter(lambda x: x % 2 == 0, range(10))))
print(even_numbers)

相比较而言,filter/map 的实现,可读性要稍差些,因为需要借助难以理解和阅读的 lambda 表达式。从运行效率上来说,filter/map 可能也比列表推导式要差,我会在另一篇文章中通过实例来比较两者的运行效率。

 

生成器表达式

什么是生成器表达式

生成器表达式(generator expression),在用法上与列表推导式非常相似,在形式上生成器表达式使用圆括号(parentheses)作为定界符,而不是列表推导式所使用的方括号。第一节的例子,也可以用生成器表达式来写:

1
2
even_numbers = list(oct(x) for x in range(10) if x % 2 == 0)
print(even_numbers)

可以看到,在写法上,生成器表达式除了用圆括号以外,还需要用 list 构造函数,把表达式结果转换成 list。

注意,如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。例如,上述例子中,生成器表达式是 list 构造函数的唯一参数,就不需要添加额外的括号。

如果不用 list 转换,结果会怎么样呢:

1
2
even_numbers = (oct(x) for x in range(10) if x % 2 == 0)
print(even_numbers)

上述代码的执行结果是:

1
<generator object <genexpr> at 0x7eff2f4aa7d0>

这里的原因,也是生成器表达式和列表推导式最大不同:列表推导式的结果是一个列表,而生成器表达式的结果是一个生成器对象。

 

生成器表达式和列表推导式的区别

上面讲到,生成器表达式和列表推导式最大不同在于生成器表达式的结果是一个生成器对象。

生成器对象本身也是一个迭代器对象,凡是迭代器对象,背后都遵守了迭代器协议,具有惰性求值的特点,只在需要时生成新元素,即每次按需生成一个元素,而不是一口气生成整个数据结构:

1
2
g = (x for x in range(1000) if x % 2 == 0)
print(g)

上述生成器表达式中,似乎产生了500个元素,但实际上它并没有开始计算,而是返回一个生成器对象(generator object):

1
<generator object <genexpr> at 0x7eff2f4aa7d0>

只有当它被用来构造 list 或 tuple,或者用于 for 遍历的时候,每个元素的值才会被计算:

1
print(list(g))

执行结果:

1
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22.....
1
2
for i in g:
	print(i)

执行结果:

1
2
3
4
0
2
4
...

这里需要注意,如果你连续执行上面两个例子,那么第二个 for 循环的例子,不会打印任何东西。这是因为在执行第一个例子的语句时,生成器对象 g 已经遍历结束了。要想让第二个例子成功,需要重新创建生成器对象:

1
2
3
g = (x for x in range(1000) if x % 2 == 0)
for i in g:
	print(i)

比列表推导式,生成器表达式的优点在于具有更高的效率,内存空间占用更少,尤其适合大数据处理的场合。我们可以用getsizeof方法来检测两种方式占用的内存空间大小:

1
2
3
4
5
from sys import getsizeof
my_list = [x for x in range(1000) if x % 2 == 0]
my_generator = (x for x in range(1000) if x % 2 == 0)
print(getsizeof(my_list))
print(getsizeof(my_generator))

执行结果:

1
2
4272
120

可以看到,生成器表达式产生的生成器对象只有120个字节,而列表推导式产生的列表对象占了足足4272个字节。

除了把生成器对象转换成 list 或 tuple,或者用 for 遍历,我们还可以用内置的next()方法逐个获取生成器内的元素:

1
2
3
4
g = (x for x in range(2))
print(next(g))
print(next(g))
print(next(g))

执行结果:

1
2
3
4
5
0
1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

可以看到,前两个 next(g) 分别返回第一第二个元素,当再次调用 next(g) 时,由于生成器已经遍历完,所以抛出StopIteration异常,该异常常用来作为判断迭代器遍历结束的标志。

我们甚至可以用 yield 语法来自定义生成器,关于这部分,本人将单独写文章来介绍。

 

参考

https://www.programiz.com/python-programming/list-comprehension


- 全文完 -

相关文章

「 您的赞赏是激励我创作和分享的最大动力! 」