课程
Python 函数并不总是包含 return 语句。生成器函数是指使用 yield 关键字而不是 return 的函数。
这类函数会产生生成器迭代器,它们是表示数据流的对象。迭代器所代表的元素只有在需要时才会被创建并产出。这种求值方式通常称为惰性求值(lazy evaluation)。
在处理大型数据集时,生成器提供了一种内存高效的替代方案,避免将数据存储在列表、元组以及其他数据结构中(这些结构需要为每个元素占用内存空间)。生成器函数还可以创建无限迭代器,而像列表和元组这类及早求值的结构无法做到这一点。
在开始之前,让我们回顾一下函数与生成器之间的差异:
|
特性 |
函数 |
生成器 |
|
值的产生 |
一次性返回所有值 |
按需一次产出一个值 |
|
执行方式 |
在返回前完整执行 |
每次产出后暂停,下一次请求值时再恢复 |
|
关键字 |
return |
yield |
|
内存使用 |
可能较高,需要在内存中存储整个序列 |
较低,仅保存当前值和下一步的状态 |
|
迭代 |
可多次迭代,但需要存储整个序列 |
为单次遍历设计,对大型或无限序列更高效 |
使用 Python 的 yield 创建生成器函数
在 Python 中,generator 既可以指生成器迭代器,也可以指生成器函数。它们在 Python 中是不同但相关的对象。为避免混淆,本教程经常使用完整术语。
我们先来探索生成器函数。生成器函数看起来与常规函数类似,但它包含 yield 关键字而不是 return。
当 Python 程序调用生成器函数时,它会创建一个生成器迭代器。迭代器按需产出一个值,并在需要下一个值之前暂停执行。我们来看一个示例,以解释这一概念,并展示常规函数与生成器函数之间的差异。
使用常规函数
首先,定义一个包含 return 语句的常规函数。该函数接收一个单词序列和一个字母,返回一个列表,其中包含每个单词中该字母出现的次数:
def find_letter_occurrences(words, letter):
output = []
for word in words:
output.append(word.count(letter))
return output
print(
find_letter_occurrences(["apple", "banana", "cherry"], "a")
)
[1, 3, 0]
该函数输出的列表为 1、3 和 0,因为 apple 中有一个 a,banana 中有三个 a,cherry 中没有 a。这个函数还可以重构为使用列表推导式,而不是初始化空列表并使用 .append():
def find_letter_occurrences(words, letter):
return [word.count(letter) for word in words]
该常规函数在每次调用时都会返回包含所有结果的列表。然而,如果单词列表很大,调用这个常规函数会增加内存需求,因为程序会创建并存储一个与原列表同等大小的新列表。如果该函数在多个输入上被反复使用,或者类似的函数对原始数据执行其他操作,内存压力会迅速增加。
使用生成器函数
可以改用生成器函数:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print(output)
<generator object find_letter_occurrences at 0x102935e00>
该函数包含yield 关键字而非 return。这个生成器函数在被调用时返回一个生成器对象,并将其赋给 output。该对象是一个迭代器,它并不包含每个单词中字母出现次数的数据。相反,生成器会在需要时创建并产出这些值。让我们从这个生成器迭代器中取出第一个值:
print(next(output))
1
内置函数 next() 是从迭代器中获取下一个值的一种方式。我们稍后会介绍其他方式。
生成器函数中的代码会执行,直到程序运行到包含 yield 关键字的那一行。在此示例中,for 循环开始第一轮迭代,获取列表 words 的第一个元素。字符串方法 .count() 返回一个整数,本例中为 1,因为 apple 中有一个 a。生成器产出该值,next(output) 返回这一值。
此时生成器 output 会暂停执行。因此,生成器完成了 for 循环的第一次迭代,并找出了列表首个单词中字母 a 的出现次数。它现在在等待下一次被需要。
如果再次以 output 作为参数调用内置的 next(),生成器会从暂停处恢复执行:
print(next(output))
3
生成器从 for 循环第一次迭代中包含 yield 的那一行继续。由于 for 循环中没有更多代码,它会回到循环顶部,获取列表 words 的第二个元素。本例中 .count() 返回 3,并产出该值。生成器再次在此处暂停。
第三次调用 next() 会恢复执行:
print(next(output))
0
第二次迭代到达 for 循环的末尾,然后进入第三次迭代。代码再次运行到包含 yield 的那一行,这次产出整数 0,因为 cherry 中没有 a。
生成器再次暂停。只有当我们第四次调用 next() 时,程序才会决定该生成器的命运:
print(next(output))
Traceback (most recent call last):
...
StopIteration
执行从 for 循环第三次迭代的尾部恢复。然而,循环已完成遍历,因为列表 words 中不再有元素。生成器会抛出 StopIteration 异常。
在大多数用例中,生成器的元素并不是通过直接调用 next() 来获取,而是通过其他迭代过程来访问。StopIteration 异常会发出迭代结束的信号。我们将在本教程下一部分进一步探讨。
当生成器的操作可以用一个表达式表示时,Python 还有另一种创建生成器迭代器的方式,就像前面的示例一样。生成器迭代器 output 可以使用生成器表达式来创建:
words = ["apple", "banana", "cherry"]
letter = "a"
output = (word.count(letter) for word in words)
print(next(output))
print(next(output))
print(next(output))
print(next(output))
1
3
0
Traceback (most recent call last):
...
StopIteration
赋给 output 的圆括号中的表达式是生成器表达式,它创建了一个与生成器函数 find_letter_occurrences() 所产生的迭代器类似的生成器迭代器。
我们用另一个生成器函数的示例来结束本节,以突出每次需要元素时执行如何暂停与恢复:
def show_status():
print("Start")
yield
print("Middle")
yield
print("End")
yield
status = show_status()
next(status)
Start
这个生成器函数没有循环。相反,它包含三行带有 yield 关键字的代码。调用生成器函数 show_status() 会创建生成器迭代器 status。程序第一次调用 next(status) 时,生成器开始执行,打印字符串 "Start",并在第一个 yield 表达式后暂停。由于 yield 关键字后没有对象,生成器会产出 None。
只有在第二次调用 next() 时,程序才会打印字符串 "Middle":
next(status)
Middle
生成器在第二个 yield 表达式后暂停。第三次调用 next() 会打印最后一个字符串 "End":
next(status)
End
生成器在最后一个 yield 表达式处暂停。下一次程序从该生成器迭代器请求值时,它会抛出 StopIteration 异常:
next(status)
Traceback (most recent call last):
...
StopIteration
我们将在下一节探索更多使用生成器的方法。
使用生成器迭代器
生成器函数会创建生成器迭代器,而迭代器是可迭代的。我们来拆解这句话。每次程序调用生成器函数,都会创建一个迭代器。由于迭代器是可迭代对象,它们可以用于 for 循环和其他迭代过程。
因此,内置函数 next() 并不是访问迭代器元素的唯一方式。本节将探索与生成器配合使用的其他方式。
结合 Python 的迭代协议使用生成器迭代器
让我们回顾一下本教程前面部分的一个生成器函数:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
for value in output:
print(value)
1
3
0
这段代码没有多次使用 next(),而是在 for 循环中使用生成器迭代器 output。由于迭代器是可迭代对象,它们可以放入 for 循环。该循环会从生成器迭代器中取出项目,直到不再有剩余值。
与列表和元组等数据结构不同,迭代器只能使用一次。如果我们尝试第二次运行相同的 for 循环,代码不会再次打印出这些值:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print("First attempt:")
for value in output:
print(value)
print("Second attempt:")
for value in output:
print(value)
First attempt:
1
3
0
Second attempt:
第一次 for 循环已经耗尽了该迭代器,因此它不再能产出任何值。如果在迭代器耗尽后还需要再次使用生成器,我们必须通过生成器函数再创建一个新的生成器迭代器。
同一程序中也可以同时存在多个生成器迭代器:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
first_output = find_letter_occurrences(words, letter)
second_output = find_letter_occurrences(words, letter)
print("First value of first_output:")
print(next(first_output))
print("Values of second_output:")
for value in second_output:
print(value)
print("Remaining values of first_output:")
for value in first_output:
print(value)
First value of first_output:
1
Values of second_output:
1
3
0
Remaining values of first_output:
3
0
生成器函数 find_letter_occurrences() 创建了两个生成器迭代器:first_output 和 second_output。尽管两个迭代器都引用列表 words 中相同的数据,但它们彼此独立推进。
该示例使用 next() 从 first_output 中获取第一个值。生成器迭代器产出 1,并在此处暂停。接下来,程序遍历 second_output。由于该生成器尚未产出任何值,循环会遍历第二个迭代器产出的所有值。最后,另一个 for 循环遍历 first_output。不过,该迭代器先前已经产出了第一个值,循环只会遍历该迭代器中剩余的值。
for 循环并不是遍历生成器迭代器的唯一过程:
print(*find_letter_occurrences(words, letter))
print(sorted(find_letter_occurrences(words, letter)))
1 3 0
[0, 1, 3]
在这些示例中,程序直接调用生成器函数来创建并使用生成器迭代器,而不是先将其赋给变量。第一个示例使用星号解包迭代器。这个过程依赖与 for 循环相同的迭代协议。
第二个示例将生成器迭代器传给内置的 sorted(),它需要一个可迭代对象作为参数。生成器是可迭代对象,因此在发生 Python 迭代的场景中都可以使用它们。
创建无限迭代器
生成器会产出一个值并暂停,直到需要下一个值。每次代码从迭代器请求一个值,生成器函数内部的代码都会执行,直到下一个 yield 表达式被求值。到目前为止,本教程中的示例都只有有限个 yield 表达式。然而,通过在生成器函数中使用 while 循环,可以创建一个产出无限多个值的生成器。以下示例中,生成器会从传入的颜色列表中随机产出颜色:
import random
def get_color(colors):
while True:
yield random.choice(colors)
output_colors = get_color(["red", "green", "blue"])
print("First two colors:")
print(next(output_colors))
print(next(output_colors))
print("Next 10 colors using a 'for' loop:")
for _ in range(10):
print(next(output_colors))
First two colors:
green
red
Next 10 colors using a 'for' loop:
blue
green
green
green
red
red
red
blue
green
red
生成器函数 get_color() 在 while 循环中包含一个 yield 表达式。因此,每当需要下一个值时,代码总会遇到另一个 yield 表达式。生成器迭代器 output_colors 会从输入列表中随机选择并无限产出颜色。这个生成器永远不会被耗尽。
无法创建无限的列表或元组等数据结构。生成器使程序能够创建无限可迭代对象。请注意,如果在 for 循环中直接使用该生成器迭代器,循环将永远运行下去。
进阶生成器概念
在 Python 中,生成器还有更高级的用法。本节将探索其中的一些。
向生成器发送对象
生成器还可以接收在求值过程中使用的附加数据。包含 yield 关键字的语句是一个表达式,会被求值为某个值。这个值可以在生成器函数内部赋给变量。我们先用一个基础示例来演示这一概念:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(next(output))
print(next(output))
1
The yield expression evaluates to: None
2
The yield expression evaluates to: None
Traceback (most recent call last):
...
StopIteration
Python 的 yield 关键字会创建一个求值为某个值的表达式。不过,该表达式在生成器函数内部的取值并不是生成器产出的那个对象。考虑第一次 yield 表达式。生成器产出整数 1。因此,第一次调用 print(next(output)) 会显示 1,并使生成器暂停执行。
然而,生成器内部的 yield 表达式会被求值为一个对象,并将该对象赋给变量名 value。在这个示例中,yield 将 None 赋值给 value。对于生成器函数中的第二个 yield 也会重复这一过程。第三次调用 next() 的目的是确保生成器函数中的所有代码都被执行。
让我们将第二次和第三次对 next() 的调用替换为 .send(),这是生成器类中的一个方法:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(output.send("Here's a value"))
print(output.send("Here's another value"))
1
The yield expression evaluates to: Here's a value
2
The yield expression evaluates to: Here's another value
Traceback (most recent call last):
...
StopIteration
生成器函数保持不变。通过调用 next() 启动生成器,代码运行直到第一次产出整数 1。第二次不再使用 next(),程序改为调用 output.send()。该方法会向生成器发送一个对象。本例中对象是字符串。生成器函数中的 yield 表达式会被求值为这个字符串,并赋给 value。因此,生成器可以在其代码中使用该字符串。
第二次调用 .send() 会向生成器发送一个新对象,并同样赋给变量 value。由于没有更多的 yield 表达式,最后一次 print() 调用后生成器会抛出 StopIteration。
我们再看一个使用 .send() 的示例。下面的生成器会显示账户余额,同时余额可以被更新:
def get_balance(start_balance):
balance = start_balance
while True:
amount = yield balance
if amount is not None:
balance += amount
current_balance = get_balance(100)
print(next(current_balance))
print(current_balance.send(10))
print(current_balance.send(-20))
print(next(current_balance))
100
110
90
90
该生成器函数在调用时需要一个起始余额。balance 的值会在生成器执行过程中发生变化。任何通过 .send() 发送到生成器的对象,都会赋给 amount。如果生成器在没有接收任何发送对象的情况下产出一个值,则该变量为 None;否则它将包含通过 .send() 发送的对象。
生成器迭代器 current_balance 以 100 美元的余额开始。通过调用 next() 启动生成器,代码开始执行,直到第一次产出值。
一旦生成器启动,就可以使用 .send() 而不是 next() 来重新开始执行。生成器会将发送的值累加到余额中。如果没有发送任何值,例如再次调用 next(),生成器将产出未变化的余额。
直接从另一个可迭代对象中产出
Python 生成器还可以使用 yield from 语法,直接从另一个生成器或可迭代对象中产出值。我们来看一个从嵌套列表中产出值的生成器函数示例:
def flatten(nested_list):
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
nested_list = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(nested_list)))
[1, 2, 3, 4, 5, 6, 7]
该生成器函数接收一个列表,该列表中可以包含嵌套列表。for 循环遍历列表中的各个项目。外层列表中的每个项目要么是一个值(本例为整数),要么是另一个列表。当项目不是列表时,生成器直接产出该项目。
然而,当项目是列表时,生成器会使用内层列表作为参数递归调用生成器函数 flatten()。这会创建另一个以该内层列表为数据源的生成器迭代器。如果这里使用 yield 表达式,第一个生成器会产出第二个生成器本身。而通过使用 yield from,第一个生成器会直接产出来自第二个生成器的值。
总结:yield vs. return
包含 return 与 yield 的函数定义看起来相似,但其行为不同。让我们总结关键差异:
|
常规函数 |
生成器函数 |
|
|
关键字 |
return(若未显式使用则为隐式返回) |
yield |
|
调用时 |
执行代码直到遇到 return,然后返回最终值 |
创建一个生成器迭代器 |
|
终止方式 |
由 return 语句终止 |
由 yield 暂停,可在稍后恢复 |
|
返回值 |
单个对象(可为数据结构) |
生成器迭代器 |
|
yield 表达式 |
不适用(创建的是语句) |
求值为 None 或通过 .send() 发送的值 |
|
使用场景 |
适合返回最终结果 |
适合创建数据流,尤其是大型或无限序列 |
结语
Python 的 yield 关键字用于在函数中定义生成器函数。这些函数在被调用时会创建生成器迭代器。生成器是 Python 中惰性求值的一个例子:表达式在需要其值时才被求值,而不是在执行表达式时立即求值。因此,yield 表达式有助于创建数据流,使值能够按需生成而无需将其存储在内存中。
在需要进行大量操作的大型数据集场景中,效率至关重要。Python 的生成器迭代器是高效处理海量数据的核心工具之一。
如果您想进一步学习 Python,请查看这个Python Developer 职业路径。
我在马耳他大学完成了本科阶段的物理与数学学习。随后前往伦敦,并在帝国理工学院取得物理学博士学位,研究用于成像人类视网膜的新型光学技术。现在,我专注于撰写与传播 Python 相关内容,并教授 Python。
