Python 生成器,我们在Python迭代器技巧节花了很多时间编写基于类的迭代器。从教学的角度来看还行,不过从例子中可以看出,编写这种迭代器类需要大量样板代码。说实话,作为一个“懒惰”的开发者,我不喜欢烦琐而重复的工作。
不过迭代器在Python中非常有用,能够编写漂亮的for-in
循环,让代码更有Python特色且高效。如果有一种更方便的方式来编写这些迭代器就好了……
好消息是真的有这样的方法!Python又提供了一些语法糖来简化迭代器的编写。本节将介绍如何使用生成器和yield
关键字以较少的代码快速编写迭代器。
Python 生成器 无限生成器
让我们首先回顾一下之前用来介绍迭代器思想的Repeater
示例。这个类实现了一个基于类的无限循环的迭代器,简化版的Repeater
类如下所示:
class Repeater:
def __init__(self, value):
self.value = value
def __iter__(self):
return self
def __next__(self):
return self.value
的确,对于这样简单的迭代器来说代码有点多了。这个类中的某些部分似乎相当规整,每个基于类的迭代器好像都是这么写的。
此时Python生成器就派上用场了。如果将前面的迭代器类重写为生成器,看起来是这样的:
def repeater(value):
while True:
yield value
从7行代码缩短到3行,不错吧?从中可以看出,生成器看起来像普通函数,但它没有return
语句,而是用yield
将数据传回给调用者。
这个新的生成器实现是否仍然像基于类的迭代器一样工作?让我们用for-in
循环测试一下:
>>> for x in repeater('Hi'):
... print(x)
'Hi'
'Hi'
'Hi'
'Hi'
'Hi'
...
代码能正常工作,但依然是无限循环输出问候语。这个简短的生成器实现似乎与Repeater
类相同。(如果想在解释器会话中跳出无限循环,请按Ctrl + C。)
那么生成器是如何工作的呢?生成器看起来像普通函数,但行为完全不同。提醒一下初学者,调用生成器函数并不会运行该函数,仅仅创建并返回一个生成器对象:
>>> repeater('Hey')
<generator object repeater at 0x107bcdbf8>
只有在对生成器对象上调用next()
时才会执行生成器函数中的代码:
>>> generator_obj = repeater('Hey')
>>> next(generator_obj)
'Hey'
如果细看repeater
函数的代码,就会发现yield
关键字像是在某种程度上停止这个生成器函数的执行,然后在稍后的时间点恢复:
def repeater(value):
while True:
yield value
这种心智模型很符合实际情况。当函数内部调用return
语句时,控制权会永久性地交还给函数的调用者。在调用yield
时,虽然控制权也是交还给函数的调用者,但只是暂时的。
return
语句会丢弃函数的局部状态,而yield
语句会暂停该函数并保留其局部状态。实际上,这意味着局部变量和生成器函数的执行状态只是暂时隐藏起来,不会被完全抛弃。再次调用生成器的next()
能够恢复执行函数:
>>> iterator = repeater('Hi')
>>> next(iterator)
'Hi'
>>> next(iterator)
'Hi'
>>> next(iterator)
'Hi'
这使得生成器完全兼容迭代器协议,因此我喜欢将生成器看作主要用来实现迭代器的语法糖。
对于大多数类型的迭代器来说,编写生成器函数比定义冗长的基于类的迭代器更容易且更易读。
Python 生成器 能够停下来的生成器
本节开始时又编写了一个无限生成器。现在你可能想知道如何编写能够在一段时间后停下来的生成器,而不是一直运行下去。
回忆一下,在基于类的迭代器中,可以通过手动引发StopIteration
异常来表示迭代结束。因为生成器与基于类的迭代器完全兼容,所以背后仍然使用这种方法。
幸运的是,程序员现在可以使用更好的接口。如果控制流从生成器函数中返回,但不是通过yield
语句,那么生成器就会停止。这意味着不必再抛出StopIteration
了。
来看一个例子:
def repeat_three_times(value):
yield value
yield value
yield value
注意这个生成器函数中没有循环,只是简单地包含了三条yield
语句。如果yield
暂时中止函数的执行并将值传递给调用者,那么当到达该生成器的末尾时会发生什么?让我们来看看:
>>> for x in repeat_three_times('Hey there'):
... print(x)
'Hey there' 'Hey
there' 'Hey
there'
你可能已经预料到,该生成器在迭代三次后停止产生新值。我们可以假设这是通过当执行到函数结尾时引发StopIteration
异常来实现的。通过另一个实验来确认一下:
>>> iterator = repeat_three_times('Hey there')
>>> next(iterator)
'Hey there'
>>> next(iterator)
'Hey there'
>>> next(iterator)
'Hey there'
>>> next(iterator)
StopIteration
>>> next(iterator)
StopIteration
这个迭代器表现得和预期的一样。一旦到达生成器函数的末尾,就会不断抛出StopIteration
以表示所有值都用完了。
回到另一个例子。BoundedIterator
类实现了一个只会重复特定次数的迭代器:
class BoundedRepeater:
def __init__(self, value, max_repeats):
self.value = value
self.max_repeats = max_repeats
self.count = 0
def __iter__(self):
return self
def __next__(self):
if self.count >= self.max_repeats:
raise StopIteration
self.count += 1
return self.value
为什么不尝试以生成器函数重新实现这个BoundedRepeater
类呢?下面是第一次尝试:
def bounded_repeater(value, max_repeats):
count = 0
while True:
if count >= max_repeats:
return
count += 1
yield value
由于想演示在生成器中使用return
语句会终止迭代并产生StopIteration
异常,因此这个函数中的while
循环有点笨拙。后面很快会整理并简化这个生成器函数,但先尝试一下目前的代码:
>>> for x in bounded_repeater('Hi', 4):
... print(x)
'Hi'
'Hi'
'Hi'
'Hi'
很好,现在有一个能重复特定次数后终止的生成器。它使用yield
语句传回值,直到碰到return
语句后停止迭代。
就像刚刚说的那样,这个生成器可以进一步简化。利用Python为每个函数的末尾添加一个隐式return None
语句的特性,我们来编写下面这个最终版:
def bounded_repeater(value, max_repeats):
for i in range(max_repeats):
yield value
可以确定,简化后的生成器仍然以相同的方式工作。所有方面都考虑到了,从BoundedRepeater
类中的12行实现转到了基于生成器的3行实现,功能完全相同,同时代码行数减少了75%,还不赖吧!
正如刚才看到的,与编写基于类的迭代器相比,生成器能帮助“抽象出”大部分样板代码,减轻程序员的负担,从而编写出更简短和易于维护的迭代器。生成器函数是Python中的一个重要特性,应该毫不犹豫地应用到自己的程序中。
关键要点
-
生成器函数是一种语法糖,用于编写支持迭代器协议的对象。与编写基于类的迭代器相比,生成器能抽象出许多样板代码。
-
yield
语句用来暂时中止执行生成器函数并传回值。 -
在生成器中,控制流通过非
yield
语句离开会抛出StopInteration
异常。