Python 生成器表达式,随着深入了解并在代码中以不同的方法实现Python迭代器协议后,我意识到“语法糖”是一个不断出现的主题。
你已经看到,基于类的迭代器和生成器函数在底层都使用了相同的设计模式。
生成器函数能够用来快速在代码中支持迭代器协议,与基于类的迭代器相比省去了大量烦琐的工作。这些特殊的语法或语法糖既节省了时间,又减轻了开发人员的工作负担。
这种情况在Python和其他编程语言中不断出现。在程序中使用某个设计模式的人越多,语言创建者就越有可能将这个设计模式抽象出来并提供快捷的实现。
程序设计语言就这样随着时间不断演变。开发者从中受益,得到越来越强大的组件,减少了烦琐的工作,从而在更短的时间内实现更多的功能。
从之前章节可以看出,生成器为编写基于类的迭代器提供了语法糖。本节将介绍的生成器表达式则在此之上又添加了一层语法糖。
生成器表达式能够更方便地编写迭代器,看起来像是简化后的列表解析式语法。生成器表达式用一行代码就能定义迭代器。
来看一个例子:
iterator = ('Hello' for i in range(3))
迭代完成后,这个生成器表达式产生的值序列与前一节中的bounded_repeater
生成器函数相同。这里再次把bounded_repeater
列出来以防你忘记了:
def bounded_repeater(value, max_repeats):
for i in range(max_repeats):
yield value
iterator = bounded_repeater('Hello', 3)
从代码量上来看,生成器函数需要4行,基于类的迭代器需要更多,而现在生成器表达式只要1行,很了不起吧!
但这里走得太快了,先脚踏实地,确保生成器表达式定义的迭代器能按预期工作:
>>> iterator = ('Hello' for i in range(3))
>>> for x in iterator:
... print(x)
'Hello'
'Hello'
'Hello'
看起来很不错!单行生成器表达式似乎获得了和bounded_repeater
生成器函数相同的结果。
但有一点需要注意,生成器表达式一经使用就不能重新启动或重用,所以在某些情况下生成器函数或基于类的迭代器更加合适。
Python 生成器表达式 生成器表达式与列表解析式
从前面可以看到,生成器表达式与列表解析式有些类似:
>>> listcomp = ['Hello' for i in range(3)]
>>> genexpr = ('Hello' for i in range(3))
但与列表解析式不同,生成器表达式不会构造列表对象,而是像基于类的迭代器或生成器函数那样“即时”生成值。
将生成器表达式分配给变量能够得到一个可用的“生成器对象”:
>>> listcomp
['Hello', 'Hello', 'Hello']
>>> genexpr
<generator object <genexpr> at 0x1036c3200>
与其他迭代器一样,需要调用next()
获取由生成器表达式生成的值:
>>> next(genexpr)
'Hello'
>>> next(genexpr)
'Hello'
>>> next(genexpr)
'Hello'
>>> next(genexpr)
StopIteration
也可以对生成器表达式调用list()
函数来构造一个包含所有生成值的列表对象:
>>> genexpr = ('Hello' for i in range(3))
>>> list(genexpr)
['Hello', 'Hello', 'Hello']
当然,这里只是为了展示如何将生成器表达式(或其他任何迭代器)转换为列表。如果真的需要一个列表对象,那么通常从一开始就会直接编写一个列表解析式。
下面来仔细看看这个简单的生成器表达式的语法结构,初始的基本模式看起来如下所示:
genexpr = (expression for item in collection)
上面的生成器表达式“模板”对应于下面这个生成器函数:
def generator():
for item in collection:
yield expression
和列表解析式一样,这种固定模式可用来将许多生成器函数转换为简洁的生成器表达式。
Python 生成器表达式 过滤值
还可以为上面的模板添加条件来过滤元素,来看一个例子:
>>> even_squares = (x * x for x in range(10)
if x % 2 == 0)
这个生成器产生从0到9所有偶数的平方数。过滤条件使用%
(取模)运算符来排除所有不能被2整除的值:
>>> for x in even_squares:
... print(x)
0 4 16 36 64
现在更新生成器表达式模板,在添加if
条件过滤元素后,模板如下所示:
genexpr = (expression for item in collection
if condition)
同样,这种模式对应下面这种直观但代码量更多的生成器函数,此时语法糖的优点就展现了出来:
def generator():
for item in collection:
if condition:
yield expression
Python 生成器表达式 内联生成器表达式
因为生成器表达式也是表达式,所以可以与其他语句一起内联使用。例如,可以定义一个迭代器并立即在for
循环中使用:
for x in ('Bom dia' for i in range(3)):
print(x)
另外还有一个语法技巧可以美化生成器表达式。如果生成器表达式是作为函数中的单个参数使用,那么可以删除生成器表达式外层的括号:
>>> sum((x * 2 for x in range(10)))
90
# 与
>>> sum(x * 2 for x in range(10))
90
这样可以编写简洁且高性能的代码。因为表达式会像基于类的迭代器或生成器函数那样“即时”生成值,所以内存占用很低。
Python 生成器表达式 物极必反
像列表解析式一样,生成器表达式还可以处理更复杂的情况。比如嵌套多个for
循环和添加链式过滤语句,让生成器表达式能处理许多情形:
(expr for x in xs if cond1
for y in ys if cond2
...
for z in zs if condN)
上面这个模式可转换为下面这个生成器函数逻辑:
for x in xs:
if cond1:
for y in ys:
if cond2:
...
for z in zs:
if condN:
yield expr
这就是我想要提出的一个重要警告:不要编写这样深度嵌套的生成器表达式。从长远来看,过度嵌套的生成器表达式很难维护。
这是一个物极必反的情形,过度使用一个美丽而简单的工具会产生难以阅读和调试的程序。
与列表解析式一样,我个人会避免编写嵌套两层以上的生成器表达式。
生成器表达式是有用且具有Python特色的工具,但不能当作万金油来用。对于复杂的迭代器,最好编写生成器函数或者基于类的迭代器。
如果需要使用嵌套的生成器和复杂的过滤条件,通常最好将子生成器提取出来(这样就可以命名)然后再互相链接。下一节会介绍这一点。
如果还是不确定应该用哪种方法,那么就先编写几个不同的实现,然后从中选择可读性最好的实现。相信我,从长远来看这样能节省时间。
关键要点
-
生成器表达式与列表解析式类似,但不构造列表对象,而是像基于类的迭代器或生成器函数那样“即时”生成值。
-
生成器表达式一经使用就不能重新启动或重新使用。
-
生成器表达式最适合实现简单的“实时”迭代器,而对于复杂的迭代器,最好编写生成器函数或基于类的迭代器。