使用 Python 的 yield 创建生成器函数
2025-01-03 20:02 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 的迭代发生,就可以使用生成器。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
2023-01-03 fn_dblog()和fn_full_dblog()的使用