Python中的生成器
生成器:generator
Python生成器简介
在本文中,我们将学习如何使用 Python 生成器轻松地创建迭代器、生成器与迭代器和普通函数有何不同,以及为什么应该使用生成器。
在 Python 中构建迭代器(iterator
)需要做大量的工作。我们必须使用 __iter__()
和 __next__()
方法实现一个类,跟踪内部状态,并在没有返回值时引发 StopIteration
异常。
这既冗长又违反直觉,在这种情况下,生成器(generator
)可以拯救世界了。Python 生成器是创建迭代器的一种简单方法。上面提到的所有工作都是由生成器自动处理的,也就是说自动实现了__iter__()
和 __next__()
方法。
简单来说,生成器是一个返回对象(迭代器)的函数,我们可以对其进行迭代(每次迭代一个值)。
Python生成器的创建
用 Python 创建生成器相当简单。这就像定义一个普通函数一样简单,但是使用 yield
语句而不是 return
语句。
如果一个函数包含至少一个 yield 语句(它可能包含其他 yield
语句或 return
语句) ,它就成为一个生成器函数。yield 和 return 都会从函数返回一些值。
不同之处在于,当 return 语句完全终止一个函数时,yield 语句暂停保存其所有状态的函数,并在以后的连续调用中继续执行。
生成器 VS 函数
下面是生成器函数与普通函数的区别。
- 生成器函数包含一个或多个 yield 语句。
- 生成器函数在调用时,返回一个对象(迭代器) ,但不会立即开始执行。
- 生成器函数自动实现迭代器的两个魔法方法,因此可以使用next()迭代这些项。
- 生成器函数一旦创建,函数将暂停并将控制转移到调用方。
- 生成器函数的局部变量及其状态在连续调用之间被记住。
- 当生成器函数终止时,在进一步调用时自动引发 StopIteration。
这里有一个例子来说明上述所有要点。我们有一个名为 my_gen() 的生成器函数,它包含几个 yield 语句。
# 一个简单的生成器函数
def my_gen():
n = 1
print('第一次打印')
# 生成器函数包含yield语句
yield n
n += 1
print('第二次打印')
yield n
n += 1
print('最后打印')
yield n
运行结果:
# 返回一个生成器,但不会立即执行
a = my_gen()
# 使用next()函数进行迭代操作
next(a)
# 输出:第一次打印
# 输出:1
# 一旦生成器函数yield,函数会暂停并将控制转移到调用方
# 局部变量及其状态在连续调用之间被记住
next(a)
# 输出:第二次打印
# 输出:2
next(a)
# 输出:最后打印
# 输出:3
# 最终,函数终止。后续如果继续调用则会引发StopIteration异常。
next(a) # 将引起 StopIteration 异常
在上面的例子中需要注意的一件有趣的事情是,变量 n 的值在每次调用之间都会被记住。
与普通函数不同,当函数 yield 时,局部变量不会被破坏。此外,生成器对象只能迭代一次。要重新启动进程,我们需要使用类似 a = my_gen()
这样的代码创建另一个生成器对象。
最后要注意的是,我们可以直接使用 for 循环的生成器。
这是因为 for 循环接受一个迭代器,并使用 next()
函数对其进行迭代。当 StopIteration
被引发时,它自动结束。
如需了解在 Python 中 for 循环实际是如何实现的,请参阅“python迭代器”一文。
示例:
# 一个简单的生成器函数
def my_gen():
n = 1
print('第一次打印')
# 生成器函数包含yield语句
yield n
n += 1
print('第二次打印')
yield n
n += 1
print('最后打印')
yield n
# 使用for循环遍历
for item in my_gen():
print(item)
当你运行这个程序时,输出结果会是:
第一次打印
1
第二次打印
2
最后打印
3
带循环的Python生成器
上面的例子用处不大,我们研究它只是为了搞清楚背后发生了什么。通常,生成器函数是通过具有适当终止条件的回路来实现的。
让我们再来看一个生成器的例子,它用来反转字符串。
# 生成器函数
def rev_str(my_str):
length = len(my_str)
for i in range(length - 1, -1, -1):
yield my_str[i]
# 用来反转字符串的for循环
for char in rev_str("hello"):
print(char)
执行后输出:
o
l
l
e
h
在这个例子中,我们使用 range()
函数给 for 循环提供逆序的索引值。
注意: 这个生成器函数不仅可以处理字符串,还可以处理列表、元组等其他类型的可迭代对象。
Python 生成器表达式
使用生成器表达式,可以很容易地动态创建简单的生成器,这使得构建生成器变得容易。
与创建匿名函数的 lambda 函数类似,生成器表达式创建匿名生成器函数。
生成器表达式的语法是:(item for item in iterable if condition)
类似于 Python 中的列表推导式,不同的是列表推导式使用方括号[ ]
,而生成器使用圆括号( )
代替。列表推导式和生成器表达式的主要区别在于,列表推导式生成整个列表,而生成器表达式生成一个项。
并且,生成器是延迟执行(只有在被要求的时候才生产项)。由于这个原因,生成器表达式比等效的列表推导式具有更高的内存效率。
# 初始化列表
my_list = [1, 3, 6, 10]
# 使用列表推导式将每一项进行平方
list_ = [x**2 for x in my_list]
# 使用生成器可以实现同样的效果
generator = (x**2 for x in my_list)
print(list_)
print(generator)
输出:
[1, 9, 36, 100]
<generator object <genexpr> at 0x7f5d4eb4bf50>
我们可以在上面看到,生成器表达式没有立即生成所需的结果。相反,它返回一个生成器对象,该对象仅按需生成项。
以下是我们如何开始从生成器获取元素项的方法:
# 初始化列表
my_list = [1, 3, 6, 10]
a = (x**2 for x in my_list)
print(next(a))
print(next(a))
print(next(a))
print(next(a))
next(a)
当我们运行上面的程序时,我们得到以下输出:
1
9
36
100
Traceback (most recent call last):
File "<string>", line 15, in <module>
StopIteration
生成器表达式可以作为函数参数使用。当以这种方式使用时,圆括号可以被删除。
sum(x**2 for x in my_list) # 输出:146
max(x**2 for x in my_list) # 输出:100
使用 Python 生成器
有几个原因使生成器成为一个强大的实现。
- 易于实现
与迭代器类相比,生成器可以以简洁明了的方式实现。下面是一个使用迭代器类实现2次幂序列的示例。
这是迭代器的代码量:
class PowTwo:
def __init__(self, max=0):
self.n = 0
self.max = max
def __iter__(self):
return self
def __next__(self):
if self.n > self.max:
raise StopIteration
result = 2 ** self.n
self.n += 1
return result
上面的程序很长而且令人抓狂。现在,让我们用一个生成器函数来做同样的事情。
这是生成器的代码(没有对比就没有伤害😂)
def PowTwoGen(max=0):
n = 0
while n < max:
yield 2 ** n
n += 1
由于生成器(generator)自动跟踪细节,实现起来简洁明了。
- 内存效率
返回序列的普通函数将在返回结果之前,在内存中创建整个序列。如果在序列中的项目数量非常大,对内存的消耗非常大。
相反,这种序列的生成器实现则对内存友好,因为它一次只生成一个项目,所以是首选的。
- 表示无限流
生成器是表示无限数据流的优秀媒介。无限流不能存储在内存中,而且由于生成器一次只生成一个项,因此它们可以表示无限的数据流。
下面的生成器函数可以生成所有的偶数(至少在理论上)。
def all_even():
n = 0
while True:
yield n
n += 2
- 管道生成器
多个生成器可用于管道一系列的操作。这是最好的说明使用一个例子。
假设我们有一个生成器,它生成 Fibonacci
斐波拉契数列中的数字。我们还有另一个平方数的生成器。
如果我们要求出斐波拉契数列中数字的平方和,我们可以通过以下方法,将生成器函数的输出结合在一起来实现。
# 斐波拉契数列生成器
def fibonacci_numbers(nums):
x, y = 0, 1
for _ in range(nums):
x, y = y, x+y
yield x
# 平方和生成器
def square(nums):
for num in nums:
yield num**2
print(sum(square(fibonacci_numbers(10))))
执行后输出:
4895
这种流水线操作非常有效,并且易于阅读。
是的,简直酷到没朋友!
---END