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
此生成器函数跟前一个生成器表达式等效,它们输出相同的结果。
生成器表达式是函数括号内的唯一参数,则可以使用不带括号的生成器表达式,否则的话,就会出现两个重复括的号 ((
或 ))
。比如下面的实例
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
.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]
都加载进去。