Python yield 与生成器函数
yield 只能出现在函数体内。包含 yield 的函数不再是普通函数,而是生成器函数:调用它时返回一个生成器对象(一种迭代器),用于惰性地逐个产生值。
生成器会保存函数的执行位置与局部状态;每次请求下一个值时,从上次暂停处继续,而不是从头执行整个函数。下文即围绕生成器函数的写法展开。
yield 与 return 的区别
return | yield | |
|---|---|---|
| 行为 | 结束函数,可选带回一个值 | 暂停函数,向调用方产出一个值,保留现场 |
| 后续调用 | 再次调用函数会重新执行 | 通过迭代器 next() / send() 继续执行 |
| 典型用途 | 一次性计算并返回结果 | 数据量大或流式处理,按需、分块产出,省内存 |
需要一次性得到完整列表时,用 return 返回列表即可;当数据量很大或希望边算边用时,yield 更合适。
示例:从「整表返回」改为惰性生成
下面先用普通函数构造随机数列表(一次占满内存):
from random import randint
def get_random_ints(count, begin, end):
print("get_random_ints start")
list_numbers = []
for x in range(count):
list_numbers.append(randint(begin, end))
print("get_random_ints end")
return list_numbers
print(type(get_random_ints))
nums = get_random_ints(10, 0, 100)
print(nums)输出(示意):
<class 'function'>
get_random_ints start
get_random_ints end
[4, 84, 27, ...] # 具体数字随机当 count 很大(例如十万、百万)时,列表会占用大量内存。
改成生成器函数:每次循环只 yield 一个随机数,不攒成大列表。
from random import randint
def get_random_ints(count, begin, end):
print("get_random_ints start")
for x in range(count):
yield randint(begin, end)
print("get_random_ints end")
nums_generator = get_random_ints(10, 0, 100)
print(type(nums_generator))
for i in nums_generator:
print(i)输出(示意):
<class 'generator'>
get_random_ints start
70
15
...
get_random_ints end要点:
nums_generator的类型是generator。- 第一次迭代时才进入函数体,因此第一个
print("... start")只执行一次。 - 每次
yield暂停;for循环再次请求下一个值时从暂停处继续。 - 全部值产完后,会继续执行
yield之后的代码,因此print("... end")通常在循环结束后出现一次。
实际场景:读大文件——readlines() 与逐行 yield
方式一:一次性读入列表(大文件时内存与文件大小成正比):
import sys
def read_file(file_name):
with open(file_name, "r", encoding="utf-8") as text_file:
return text_file.readlines()
file_lines = read_file(sys.argv[1])
print(type(file_lines))
print(len(file_lines))
for line in file_lines:
print(line, end="")方式二:生成器逐行产出(同一时间大致只保留当前行):
import sys
def read_file_yield(file_name):
with open(file_name, "r", encoding="utf-8") as text_file:
while True:
line_data = text_file.readline()
if not line_data:
break
yield line_data
file_data = read_file_yield(sys.argv[1])
print(type(file_data))
for line in file_data:
print(line, end="")说明:教学上用来对比「一次性列表」与「惰性生成」。日常更推荐直接 for line in open(...): 或 with open(...) as f: for line in f:,本身也是按行迭代、内存友好。
与 resource 模块的对比(Unix/Linux)
下面两段脚本曾在 Unix/Linux 上用 resource.getrusage 观察峰值内存与时间(Python 3.7)。Windows 上通常无法使用 resource 模块,此处仅说明思路:对大文件,readlines() 的峰值内存随文件增大,而生成器方式峰值近似恒定。
du -sh abc.txt abcd.txt abcde.txt abcdef.txt
# 示例输出:
# 4.0K abc.txt
# 324K abcd.txt
# 26M abcde.txt
# 263M abcdef.txt| 文件大小 | return + readlines()(约) | 生成器逐行 yield(约) |
|---|---|---|
| 4 KB | 内存约 5.3 MB,时间约 0.023 s | 内存约 5.4 MB,时间约 0.027 s |
| 324 KB | 内存约 10 MB,时间约 0.28 s | 内存约 5.4 MB,时间约 0.32 s |
| 26 MB | 内存约 393 MB,时间约 27 s | 内存约 5.5 MB,时间约 30 s |
| 263 MB | 内存约 3.65 GB,时间约 274 s | 内存约 5.6 MB,时间约 293 s |
生成器版每次迭代要多一点状态切换,时间可能略长,但内存在大文件下差异显著:readlines() 与文件大小同阶,生成器近似常量级(与缓冲区、解释器开销有关)。
send():向生成器传入数据
生成器不仅可以向外 yield 值,还可以通过 send(value) 从外部传入值;该值会成为 yield 表达式的结果。
首次必须先 send(None)(或 next(gen)),让执行推进到第一个 yield;否则会对「刚启动的生成器」发送非 None 而触发 TypeError。
def processor():
while True:
value = yield
print(f"Processing {value}")
data_processor = processor()
print(type(data_processor))
data_processor.send(None)
for x in range(1, 5):
data_processor.send(x)输出:
<class 'generator'>
Processing 1
Processing 2
Processing 3
Processing 4yield from:委托给子迭代器
yield from iterable 会把子迭代器(或任意可迭代对象)产生的值原样转发给调用方,并处理 send() / throw() 等委托语义(进阶用法)。
下面先写一个「手动转发」的包装器:
from random import randint
def get_random_ints(count, begin, end):
print("get_random_ints start")
for x in range(count):
yield randint(begin, end)
print("get_random_ints end")
def generate_ints(gen):
for x in gen:
yield x用 yield from 等价简写为:
def generate_ints(gen):
yield from gen需要把外层的 send() 转发给内层生成器时,yield from 能减少样板代码。例如内层生成器从 yield 接收数据并打印:
def printer():
while True:
data = yield
print("Processing", data)
def printer_wrapper(gen):
gen.send(None)
while True:
x = yield
gen.send(x)
pr = printer_wrapper(printer())
pr.send(None)
for x in range(1, 5):
pr.send(x)用 yield from 可写成:
def printer_wrapper(gen):
yield from gen
pr = printer_wrapper(printer())
pr.send(None)
for x in range(1, 5):
pr.send(x)小结
- 含
yield的函数是生成器函数,调用得到生成器迭代器,惰性产出、可暂停/恢复,适合大数据与管道式处理。 return结束函数;yield产出值并挂起,配合next()/for/send()继续。send(None)(或next())用于启动到第一个yield;再send(值)会赋给yield左侧(若写成x = yield)。yield from把迭代/协程委托给子生成器,简化转发逻辑。