Python 生成器

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异常。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程