Python 发生器

Jinku Hu 2023年1月30日
  1. 介绍
  2. 重置生成器
  3. 迭代
  4. next() 函数
  5. 生成器表达式
Python 发生器

Python 发生器是一种惰性迭代器,它通过发生器函数(使用 yield 关键字)来创建,或者通过发生器表达式(比如 an_expression for x in an_iterator )。

介绍

生成器表达式类似于列表、字典或者集合的推导式,但是用括号括起来。但当它们用作函数调用的唯一参数时,不必非得加括号,比如

expressionEg = (x ** 2 for x in range(10))

此例生成从 0 开始的前 10 个数字的平方。

生成器函数类似于常规函数,除了它们在主体中有一个或多个 yield 语句。这些函数不能返回(return)任何值(但如果你想提前停止生成器,则允许使用空 return)。

def function():
    for x in range(10):
        yield x ** 2

此生成器函数跟前一个生成器表达式等效,它们输出相同的结果。

Info
所有生成器表达式都有自己的等效函数,反之亦然。

生成器表达式是函数括号内的唯一参数,则可以使用不带括号的生成器表达式,否则的话,就会出现两个重复括的号 (())。比如下面的实例

sum(i for i in range(10) if i % 2 == 0)  # Output: 20
any(x == 0 for x in foo)  # Output: True or False depending on foo
type(a > b for a in foo if a % 2 == 1)  # Output: <class 'generator'>

我们可以用上面的来代替下面的表达,

sum((i for i in range(10) if i % 2 == 0))
any((x == 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))

调用生成器函数会生成一个生成器对象,以后可以对其进行迭代。与其他类型的迭代器不同,生成器对象只能遍历一次。

g1 = function()
print(g1)  # Out: <generator object function at 0x1012e1888>

请注意,生成器的主体不会立即执行:当你 function() 在上面的示例中调用时,它只会返回生成器对象,而不去执行第一个 print 语句。这使得生成器比返回列表的函数消耗更少的内存,并且它能够实现生成无限长的序列。

出于这个原因,生成器通常用于数据科学以及涉及大量数据的其他环境。另一个优点是其他代码可以立即使用生成器产生的值,而无需等待生成完整的序列。

但是,如果你需要多次使用生成器生成的值,并且如果生成它们的成本高于存储,则将 list 生成的值存储为比重新生成序列更好。有关详细信息,请参阅下面的重置生成器

通常,生成器对象用于循环或任何需要迭代的函数中:

for x in g1:
    print("Received", x)

# Output
# Received 0
# Received 1
# Received 4
# Received 9
# Received 16
# Received 25
# Received 36
# Received 49
# Received 64
# Received 81

arr1 = list(g1)
# arr1 = [], 因为上面的循环已经把所有的数据给用光了
g2 = function()
arr2 = list(g2)  # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

由于生成器对象是迭代器,因此可以使用 next() 函数手动迭代它们。这样将在每次后续调用时逐个返回产生的值。

在这种情况下,每次对生成器调用 next() 时,Python 都会在生成器函数体中执行语句,直到它到达下一个 yield 语句。此时它返回 yield 命令的参数,并记住该发生点。next() 再次调用时将从该点恢复执行并继续直到下一个 yield 语句。

如果 Python 到达生成器函数的末尾而不再遇到 yield,将会引发 StopIteration 异常(这是正常的,所有迭代器都以相同的方式运行)。

g3 = function()
a = next(g3)  # a becomes 0
b = next(g3)  # b becomes 1
c = next(g3)  # c becomes 2
...
j = next(g3)  # Raises StopIteration, j remains undefined
注意
在 Python 2 中,生成器对象具有 .next() 可用于手动迭代生成值的方法。在 Python 3 中,此方法已被替换 .__next__()

重置生成器

请记住,你只能迭代一次生成器生成的对象。如果你已经在脚本中迭代了对象,那么任何进一步的尝试都会产生 None

如果需要多次使用生成器生成的对象,可以再次定义生成器函数并再次使用它,或者,也可以在首次使用时将生成器函数的输出存储在列表中。如果要处理大量数据,重新定义生成器函数将是一个不错的选择,因为存储所有数据项的列表将占用大量磁盘空间。相反,如果最初生成各个元素的成本很高,我们更愿意将生成的项目存储在列表中,以便可以重复使用它们。

迭代

生成器对象支持迭代器协议。也就是说,它提供了一个 next() 方法(Python 3.x 中为 __next__()),用于逐步执行它,并且它的 __iter__ 方法返回自己。这意味着生成器可以在任何支持通用可迭代对象的语言构造中使用。

# 简单的 Python 2.x 中 xrange()函数的实现
def xrange(n):
    i = 0
    while i < n:
        yield i
        i += 1


# 循环
for i in xrange(10):
    print(i)  # prints the values 0, 1, ..., 9

# unpacking
a, b, c = xrange(3)  # 0, 1, 2

# building a list
l = list(xrange(10))  # [0, 1, ..., 9]

next() 函数

内置的 next() 函数是一个方便的包装函数,它可以用于从任何迭代器中获得数据(包括生成器迭代),并提供在迭代结束时的默认值。

def nums():
    yield 1
    yield 2
    yield 3


generator = nums()

next(generator, None)  # 1
next(generator, None)  # 2
next(generator, None)  # 3
next(generator, None)  # None
next(generator, None)  # None
# .

语法是 next(iterator[, default])。如果迭代器结束并且已经给定了默认值,则返回它。如果未提供默认值,则会引发 StopIteration 错误。

生成器表达式

可以使用类似于推导式的语法来创建生成器迭代器。

generator = (i * 2 for i in range(3))

next(generator)  # 0
next(generator)  # 2
next(generator)  # 4
next(generator)  # raises StopIteration

如果函数不一定需要传递列表给它,那可以通过在函数调用中放置生成器表达式来使得代码变得简洁(并提高可读性)。函数调用的括号隐式地使里面的表达式成为生成器表达式。

sum(i ** 2 for i in range(4))  # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14

此外,你还可以节省内存,因为迭代器允许 Python 根据需要来获取数据,而不是将整个列表 [0, 1, 2, 3] 都加载进去。

作者: Jinku Hu
Jinku Hu avatar Jinku Hu avatar

DelftStack.com 创始人。Jinku 在机器人和汽车行业工作了8多年。他在自动测试、远程测试及从耐久性测试中创建报告时磨练了自己的编程技能。他拥有电气/电子工程背景,但他也扩展了自己的兴趣到嵌入式电子、嵌入式编程以及前端和后端编程。

LinkedIn Facebook