Python 生成器表达式

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特色的工具,但不能当作万金油来用。对于复杂的迭代器,最好编写生成器函数或者基于类的迭代器。
如果需要使用嵌套的生成器和复杂的过滤条件,通常最好将子生成器提取出来(这样就可以命名)然后再互相链接。下一节会介绍这一点。
如果还是不确定应该用哪种方法,那么就先编写几个不同的实现,然后从中选择可读性最好的实现。相信我,从长远来看这样能节省时间。

关键要点

  • 生成器表达式与列表解析式类似,但不构造列表对象,而是像基于类的迭代器或生成器函数那样“即时”生成值。

  • 生成器表达式一经使用就不能重新启动或重新使用。

  • 生成器表达式最适合实现简单的“实时”迭代器,而对于复杂的迭代器,最好编写生成器函数或基于类的迭代器。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程