• 沒有找到結果。

9.5.3 __getattr__ 、 __setattr__ 等方法

9.7 生成器

10 11 12 13 14

提示 通过对可迭代对象调用内置函数iter,可获得一个迭代器。

>>> it = iter([1, 2, 3])

>>> next(it) 1

>>> next(it) 2

还可使用它从函数或其他可调用对象创建可迭代对象,详情请参阅库参考手册。

9.6.2 从迭代器创建序列

除了对迭代器和可迭代对象进行迭代(通常这样做)之外,还可将它们转换为序列。在可以 使用序列的情况下,大多也可使用迭代器或可迭代对象(诸如索引和切片等操作除外)。一个这 样的例子是使用构造函数list显式地将迭代器转换为列表。

>>> class TestIterator:

... value = 0

... def __next__(self):

... self.value += 1

... if self.value > 10: raise StopIteration ... return self.value

... def __iter__(self):

... return self ...

>>> ti = TestIterator()

>>> list(ti)

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

9.7 生成器

生成器是一个相对较新的Python概念。由于历史原因,它也被称为简单生成器(simple generator)。生成器和迭代器可能是近年来引入的最强大的功能,但生成器是一个相当复杂的概 念,你可能需要花些功夫才能明白其工作原理和用途。虽然生成器让你能够编写出非常优雅的代 码,但请放心,无论编写什么程序,都完全可以不使用生成器。

生成器是一种使用普通函数语法定义的迭代器。生成器的工作原理到底是什么呢?通过示例 来说明最合适。下面先来看看如何创建和使用生成器,然后再看看幕后的情况。

9.7.1 创建生成器

生成器创建起来与函数一样简单。你现在肯定厌烦了老套的斐波那契数列,所以下面换换口 味,创建一个将嵌套列表展开的函数。这个函数将一个类似于下面的列表作为参数:

nested = [[1, 2], [3, 4], [5]]

换而言之,这是一个列表的列表。函数应按顺序提供这些数字,下面是一种解决方案:

def flatten(nested):

for sublist in nested:

for element in sublist:

yield element

这个函数的大部分代码都很简单。它首先迭代所提供嵌套列表中的所有子列表,然后按顺序 迭代每个子列表的元素。倘若最后一行为print(element),这个函数将容易理解得多,不是吗?

在这里,你没有见过的是yield语句。包含yield语句的函数都被称为生成器。这可不仅仅是 名称上的差别,生成器的行为与普通函数截然不同。差别在于,生成器不是使用return返回一个 值,而是可以生成多个值,每次一个。每次使用yield生成一个值后,函数都将冻结,即在此停 止执行,等待被重新唤醒。被重新唤醒后,函数将从停止的地方开始继续执行。

为使用所有的值,可对生成器进行迭代。

>>> nested = [[1, 2], [3, 4], [5]]

>>> for num in flatten(nested):

... print(num) ...

1 2 3 4 5

>>> list(flatten(nested)) [1, 2, 3, 4, 5]

简单生成器

在Python 2.4中,引入了一个类似于列表推导(参见第5章)的概念:生成器推导(也叫生 成器表达式)。其工作原理与列表推导相同,但不是创建一个列表(即不立即执行循环),而 是返回一个生成器,让你能够逐步执行计算。

>>> g = ((i + 2) ** 2 for i in range(2, 27))

>>> next(g) 16

如你所见,不同于列表推导,这里使用的是圆括号。在像这样的简单情形下,还不如使 用列表推导;但如果要包装可迭代对象(可能生成大量的值),使用列表推导将立即实例化一 个列表,从而丧失迭代的优势。

另一个好处是,直接在一对既有的圆括号内(如在函数调用中)使用生成器推导时,无需 再添加一对圆括号。换而言之,可编写下面这样非常漂亮的代码:

sum(i ** 2 for i in range(10))

1

def flatten(nested):

try:

for sublist in nested:

for element in flatten(sublist):

yield element

def flatten(nested):

try:

# 不迭代类似于字符串的对象:

try: nested + '' except TypeError: pass else: raise TypeError for sublist in nested:

——————————

① 感谢Alex Martelli指出了这个成例以及在这里使用它的重要性。

for element in flatten(sublist):

yield element except TypeError:

yield nested

如你所见,如果表达式nested + ''引发了TypeError异常,就忽略这种异常;如果没有引发 TypeError异常,内部try语句中的else子句将引发TypeError异常,这样将在外部的excpet子句中 原封不动地生成类似于字符串的对象。明白了吗?

下面的示例表明,这个版本也可用于字符串:

>>> list(flatten(['foo', ['bar', ['baz']]])) ['foo', 'bar', 'baz']

请注意,这里没有执行类型检查:我没有检查nested是否是字符串,而只是检查其行为是否 类似于字符串,即能否与字符串拼接。对于这种检查,一种更自然的替代方案是,使用isinstance 以及字符串和类似于字符串的对象的一些抽象超类,但遗憾的是没有这样的标准类。另外,即便 是对UserString来说,也无法检查其类型是否为str。

9.7.3 通用生成器

如果你按前面的例子做了,就差不多知道了如何使用生成器。你知道,生成器是包含关键字

yield的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器。每次请求值时,都

将执行生成器的代码,直到遇到yield或return。yield意味着应生成一个值,而return意味着生 成器应停止执行(即不再生成值;仅当在生成器调用return时,才能不提供任何参数)。

换而言之,生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。生成器的函数 是由def语句定义的,其中包含yield。生成器的迭代器是这个函数返回的结果。用不太准确的话 说,这两个实体通常被视为一个,通称为生成器。

>>> def simple_generator():

yield 1 ...

>>> simple_generator

<function simple_generator at 153b44>

>>> simple_generator()

<generator object at 1510b0>

对于生成器的函数返回的迭代器,可以像使用其他迭代器一样使用它。

9.7.4 生成器的方法

在生成器开始运行后,可使用生成器和外部之间的通信渠道向它提供值。这个通信渠道包含 如下两个端点。

外部世界:外部世界可访问生成器的方法send,这个方法类似于next,但接受一个参数(要 发送的“消息”,可以是任何对象)。

生成器:在挂起的生成器内部,yield可能用作表达式而不是语句。换而言之,当生成器

1 2 3 4 5

15 6 7 8 9 10 11 12 13 14

重新运行时,yield返回一个值——通过send从外部世界发送的值。如果使用的是next, yield将返回None。

请注意,仅当生成器被挂起(即遇到第一个yield)后,使用send(而不是next)才有意义。

要在此之前向生成器提供信息,可使用生成器的函数的参数。

注意 如果一定要在生成器刚启动时对其调用方法send,可向它传递参数None。 下面的示例很傻,但说明了这种机制:

def repeater(value):

while True:

new = (yield value)

if new is not None: value = new

下面使用了这个生成器:

>>> r = repeater(42)

>>> next(r) 42

>>> r.send("Hello, world!")

"Hello, world!"

注意到使用圆括号将yield表达式括起来了。在有些情况下,并非必须这样做,但小心驶得 万年船。如果要以某种方式使用返回值,就不管三七二十一,将其用圆括号括起吧。

生成器还包含另外两个方法。

方法throw:用于在生成器中(yield表达式处)引发异常,调用时可提供一个异常类型、一

个可选值和一个traceback对象。

方法close:用于停止生成器,调用时无需提供任何参数。

方 法close( 由 Python 垃 圾 收 集 器 在 需 要 时 调 用 ) 也 是 基 于 异 常 的 : 在yield处 引 发 GeneratorExit异常。因此如果要在生成器中提供一些清理代码,可将yield放在一条try/finally 语句中。如果愿意,也可捕获GeneratorExit异常,但随后必须重新引发它(可能在清理后)、引 发其他异常或直接返回。对生成器调用close后,再试图从它那里获取值将导致RuntimeError异常。

提示 有关生成器的方法以及它们是如何将生成器变成简单协同程序(coroutine)的详细信息,

请参阅“PEP 342”(www.python.org/dev/peps/pep-0342/)。

9.7.5 模拟生成器

如果你使用的是较老的Python版本,就无法使用生成器。下面是一个简单的解决方案,让你 能够使用普通函数模拟生成器。

首先,在函数体开头插入如下一行代码:

result = []

如果代码已使用名称result,应改用其他名称。(在任何情况下,使用更具描述性的名称都 是不错的主意。)接下来,将类似于yield some_expression的代码行替换为如下代码行:

yield some_expression with this:

result.append(some_expression)

最后,在函数末尾添加如下代码行:

return result

尽管使用这种方法并不能模拟所有的生成器,但可模拟大部分生成器。例如,这无法模拟无 穷生成器,因为显然不能将这种生成器的值都存储到一个列表中。

下面使用普通函数重写了生成器flatten:

def flatten(nested):

result = []

try:

# 不迭代类似于字符串的对象:

try: nested + '' except TypeError: pass else: raise TypeError for sublist in nested:

for element in flatten(sublist):

result.append(element) except TypeError:

result.append(nested) return result