Skip to content

Python yield 与生成器函数

yield 只能出现在函数体内。包含 yield 的函数不再是普通函数,而是生成器函数:调用它时返回一个生成器对象(一种迭代器),用于惰性地逐个产生值。

生成器会保存函数的执行位置与局部状态;每次请求下一个值时,从上次暂停处继续,而不是从头执行整个函数。下文即围绕生成器函数的写法展开。

yieldreturn 的区别

returnyield
行为结束函数,可选带回一个值暂停函数,向调用方产出一个值,保留现场
后续调用再次调用函数会重新执行通过迭代器 next() / send() 继续执行
典型用途一次性计算并返回结果数据量大或流式处理,按需、分块产出,省内存

需要一次性得到完整列表时,用 return 返回列表即可;当数据量很大或希望边算边用时,yield 更合适。

示例:从「整表返回」改为惰性生成

下面先用普通函数构造随机数列表(一次占满内存):

python
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)

输出(示意):

text
<class 'function'>
get_random_ints start
get_random_ints end
[4, 84, 27, ...]  # 具体数字随机

count 很大(例如十万、百万)时,列表会占用大量内存。

改成生成器函数:每次循环只 yield 一个随机数,不攒成大列表。

python
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)

输出(示意):

text
<class 'generator'>
get_random_ints start
70
15
...
get_random_ints end

要点:

  • nums_generator 的类型是 generator
  • 第一次迭代时才进入函数体,因此第一个 print("... start") 只执行一次。
  • 每次 yield 暂停;for 循环再次请求下一个值时从暂停处继续。
  • 全部值产完后,会继续执行 yield 之后的代码,因此 print("... end") 通常在循环结束后出现一次。

实际场景:读大文件——readlines() 与逐行 yield

方式一:一次性读入列表(大文件时内存与文件大小成正比):

python
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="")

方式二:生成器逐行产出(同一时间大致只保留当前行):

python
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() 的峰值内存随文件增大,而生成器方式峰值近似恒定。

bash
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

python
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)

输出:

text
<class 'generator'>
Processing 1
Processing 2
Processing 3
Processing 4

yield from:委托给子迭代器

yield from iterable 会把子迭代器(或任意可迭代对象)产生的值原样转发给调用方,并处理 send() / throw() 等委托语义(进阶用法)。

下面先写一个「手动转发」的包装器:

python
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 等价简写为:

python
def generate_ints(gen):
    yield from gen

需要把外层的 send() 转发给内层生成器时,yield from 能减少样板代码。例如内层生成器从 yield 接收数据并打印:

python
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 可写成:

python
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 把迭代/协程委托给子生成器,简化转发逻辑。

参考