代码改变世界

使用 Python 的 yield 创建生成器函数

  abce  阅读(75)  评论(0编辑  收藏  举报

 

Python 中的 yield 关键字将常规函数转换为生成器,它可以按需生成一系列值,而不是一次性计算所有值。

Python 函数并不总是有返回语句。生成器函数是用 yield 关键字代替 return 的函数。这些函数产生生成器迭代器,它是表示数据流的对象。迭代器所代表的元素只有在需要时才会被创建和产生。这种类型的评估通常被称为懒评估。

 

在处理大型数据集时,生成器提供了一种节省内存的替代方法,而不是将数据存储在列表、元组和其他数据结构中,因为这些数据结构中的每个元素都需要占用内存空间。生成器函数还可以创建无限迭代器,而急于求值的列表和元组等结构则无法做到这一点。

 

在开始之前,我们先来回顾一下函数和生成器之间的区别:

特征

函数

生成器

生成的值

一次返回所有的值

根据需要,每次yeild一个值

执行

在返回值之前全部执行完成

每次yeild之后都会暂停,请求下一个值的时候再恢复

关键字

return

yield

内存使用

所有结果存在内存中,内存占用会很高

只是存储当前的值和下一次的状态,内存占用会很低

迭代

可能有多个迭代,但是要存储所有的结果

单次迭代,对于大的或无限序列很有用

 

使用 Python 的 yield 创建生成器函数

Python 中的生成器一词可以指生成器迭代器生成器函数。它们是 Python 中不同但相关的对象。

 

先来探讨一下生成器函数。生成器函数与普通函数相似,但包含 yield 关键字,而不是 return。

 

当 Python 程序调用一个生成器函数时,它会创建一个生成器迭代器。迭代器按需产生一个值,并暂停执行,直到需要另一个值。让我们看一个例子来解释这个概念,并演示常规函数和生成器函数的区别。

 

使用常规函数

首先,定义一个包含return语句的常规函数。该函数接受一个单词序列和一个字母,并返回一个包含每个单词中字母出现次数的列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的 list,因为 apple 中有一个 a,banana 中出现了三个 a,而 cherry 中没有。同样的函数可以重构为使用列表表达式,而不是初始化一个空列表并使用 .append():

1
2
def find_letter_occurrences(words, letter):
    return [word.count(letter) for word in words]

只要调用这个常规函数,它就会返回一个包含所有结果的列表。但是,如果单词列表很大,调用这个常规函数就会对内存提出要求,因为程序会创建并存储一个与原始列表大小相同的新列表。如果该函数在多个输入参数上重复使用,或类似函数在原始数据上执行其他操作,内存压力就会迅速增加。

使用生成器函数

可以使用生成器函数来代替:

1
2
3
4
5
6
7
8
9
10
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。该对象是一个迭代器。它不包含代表每个单词中字母出现次数的数据。相反,生成器会在需要时创建并生成这些值。让我们从生成器迭代器中获取第一个值:

1
2
3
print(next(output))
##输出结果
1

内置函数 next() 是从迭代器中获取下一个值的一种方法。

 

生成器函数中的代码会一直执行,直到程序到达带有 yield 关键字的那一行。

 

如果以output为参数再次调用内置的 next(),生成器将从暂停的位置继续执行:

1
2
3
print(next(output))
##输出结果
3

执行到这里,生成器再次暂停。

 

第三次调用 next() 时将继续执行:

1
2
3
print(next(output))
##输出结果
0

代码再次执行到 yield 行,这次的结果是整数 0,因为 cherry 中没有出现 a。

 

生成器再次暂停。只有当我们第四次调用 next() 时,程序才会决定生成器的命运:

1
2
3
4
5
print(next(output))
##输出结果
Traceback (most recent call last):
  ...
StopIteration

由于列表单词中已没有元素,循环的迭代已经结束。生成器会引发 StopIteration 异常。

 

在大多数情况下,生成器元素不会直接使用 next() 访问,而是通过另一个迭代过程访问。StopIteration 异常标志着迭代过程的结束。

 

当生成器迭代器的操作可以用一个表达式表示时,Python 还有另一种创建生成器迭代器的方法,就像前面的例子一样。生成器迭代器的输出可以使用 generator 表达式来创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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))
##执行结果
<generator object <genexpr> at 0x00000249AC834AC0>
1
3
0
Traceback (most recent call last):
  ...
StopIteration

赋值给output的括号中的表达式是一个生成器表达式,它创建的生成器迭代器与生成器函数 find_letter_occurrences() 生成的迭代器类似。

 

最后,以另一个生成器函数为例,重点说明每次需要元素时,执行是如何暂停和恢复的:

1
2
3
4
5
6
7
8
9
10
11
12
def show_status():
    print("Start")
    yield
    print("Middle")
    yield
    print("End")
    yield
status = show_status()
next(status)
##运行结果
<generator object show_status at 0x0000018D410D4AC0>
Start

这个生成器函数没有循环。相反,它包含三行带有 yield 关键字的代码。代码在调用生成器函数 show_status() 时创建了一个生成器迭代器 status。程序第一次调用 next(status) 时,生成器开始执行。它会打印字符串 “Start”,并在第一个 yield 表达式后暂停。由于 yield 关键字后没有对象,因此生成器输出 None。

 

只有在第二次调用 next() 时,程序才会打印字符串 “Middle”:

1
2
3
next(status)
##运行结果
Middle

生成器在第二个 yield 表达式后暂停。第三次调用 next() 会打印出最终字符串 “End”:

1
2
3
next(status)
##运行结果
End

生成器在最后一个 yield 表达式时暂停。下一次程序从生成器迭代器中请求值时,它将引发 StopIteration 异常:

1
2
3
4
5
next(status)
##运行结果
Traceback (most recent call last):
  ...
StopIteration

使用生成器迭代器

生成器函数创建生成器迭代器,而迭代器是可迭代的。程序每次调用生成器函数,都会创建一个迭代器。由于迭代器是可迭代的,因此可以在 for 循环和其他迭代过程中使用。

 

因此,next() 内置函数并不是访问迭代器中元素的唯一方法。本节将探讨使用迭代器的其他方法。

在生成器迭代器中使用 Python 的迭代协议

重温一下前面的生成器函数:

1
2
3
4
5
6
7
8
9
10
11
12
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 循环中使用生成器迭代器输出。由于迭代器是可迭代的,因此可以在 for 循环中使用。循环从生成器迭代器中获取项目,直到没有任何值为止。

 

与列表和元组等数据结构不同,迭代器只能使用一次。如果我们第二次尝试运行相同的 for 循环,代码不会再次打印出值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 循环耗尽,因此不能再产生值。如果在生成器迭代器耗尽后再次需要它,我们必须从生成器函数中创建另一个生成器迭代器。

 

程序中也有可能同时存在多个生成器迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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。虽然两个迭代器都引用了列表单词中的相同数据,但它们的进程是相互独立的。

 

本示例使用 next() 从 first_output 中获取第一个值。此时,生成器迭代器产生 1 并暂停。程序在 second_output 中循环。由于这个生成器还没有产生任何值,因此循环会遍历第二个迭代器产生的所有值。最后,另一个 for 循环遍历 first_output。但是,这个迭代器已经在程序的早期产生了第一个值。这个循环会遍历这个迭代器中的其余值。

 

for 循环并不是唯一可以用来遍历生成器迭代器的:

1
2
3
4
5
print(*find_letter_occurrences(words, letter))
print(sorted(find_letter_occurrences(words, letter)))
##运行结果
1 3 0
[0, 1, 3]

在这些示例中,程序直接调用生成器函数来创建和使用生成器迭代器,而不是将其赋值给变量。在第一个示例中,迭代器使用星形符号解包。这一过程依赖于与 for 循环相同的迭代协议。

 

在第二个示例中,生成器迭代器被传递给内置的 sorted(),它需要一个可迭代参数。生成器是可迭代的,因此,只要 Python 的迭代发生,就可以使用生成器。

 

相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
历史上的今天:
2023-01-03 fn_dblog()和fn_full_dblog()的使用
点击右上角即可分享
微信分享提示