深入理解 Python 生成器

一、生成器的准确定位:它不是"特殊列表",而是"惰性迭代器构造器"

生成器最准确的定义是:

生成器函数是包含 yield 的函数;调用它不会立刻执行函数体,而是返回一个生成器对象。这个对象实现了迭代器协议,可以在每次请求下一个值时继续执行,直到再次遇到 yield 或最终结束。

先看最基础的例子:

复制代码
def count_up_to(n):
    current = 1
    while current <= n:
        yield current
        current += 1

gen = count_up_to(3)

print(gen)          # <generator object count_up_to at ...>
print(next(gen))    # 1
print(next(gen))    # 2
print(next(gen))    # 3

这个例子里最重要的事实不是它输出了 1、2、3,而是下面两点:

  1. 调用 count_up_to(3) 时,函数体没有一次性执行完。
  2. 返回值不是列表,而是生成器对象。

如果把同样逻辑写成普通函数:

复制代码
def count_up_to_list(n):
    result = []
    current = 1
    while current <= n:
        result.append(current)
        current += 1
    return result

data = count_up_to_list(3)
print(data)         # [1, 2, 3]

这里的差异不是"语法不同"这么简单,而是计算模型不同:

  1. 列表方案是立即求值。
  2. 生成器方案是按需求值。
  3. 列表一次性占用完整结果的内存。
  4. 生成器只在迭代推进时产生当前值。

因此,生成器不是"另一种容器",而是一种延迟计算机制。


二、为什么 yield 会改变函数语义

普通函数的执行模型很简单:调用,执行到底,return 返回,栈帧销毁。

一旦函数体内出现 yield,这个函数就不再是普通函数,而会被编译为生成器函数。它的执行语义变成:

  1. 调用时不立即运行主体逻辑。
  2. 返回生成器对象。
  3. 每次被 next 或 for 驱动时,从上次暂停位置继续执行。
  4. 遇到 yield 暂停,并把值返回给外部。
  5. 执行完毕时抛出 StopIteration。

看一个更能说明"暂停/恢复"本质的例子:

复制代码
def demo():
    print("step 1")
    yield "A"
    print("step 2")
    yield "B"
    print("step 3")

gen = demo()

print(next(gen))
print(next(gen))
try:
    print(next(gen))
except StopIteration:
    print("generator finished")

运行顺序是:

  1. 第一次 next 时,打印 step 1,返回 A。
  2. 第二次 next 时,从上次 yield 之后继续,打印 step 2,返回 B。
  3. 第三次 next 时,继续执行,打印 step 3,然后结束并抛出 StopIteration。

这说明 yield 的核心作用不是"返回值",而是"挂起当前执行现场"。


三、生成器与迭代器的关系:生成器是迭代器的一种实现

理解生成器,不能绕开迭代器协议。Python 的迭代器协议要求对象具备两个特征:

  1. iter 方法,返回迭代器自身或另一个迭代器。
  2. next 方法,每次返回下一个值,没有值时抛出 StopIteration。

可以手写一个迭代器类:

复制代码
class CountIterator:
    def __init__(self, n):
        self.n = n
        self.current = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.current > self.n:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

it = CountIterator(3)
for item in it:
    print(item)

而用生成器实现同样逻辑:

复制代码
def count_generator(n):
    current = 1
    while current <= n:
        yield current
        current += 1

for item in count_generator(3):
    print(item)

两者的语义完全一致,但生成器版本更短、更接近问题本身。这里可以得到一个非常重要的结论:

生成器不是对迭代器协议的替代,而是对迭代器协议的高级语法封装。

也正因为如此,凡是能接受迭代器的地方,通常都能接受生成器:

复制代码
total = sum(x for x in range(5))
print(total)    # 10

四、生成器对象保存了什么状态

生成器之所以能暂停后恢复,是因为它不是简单保存"上一个值",而是保存整个执行现场。包括但不限于:

  1. 局部变量当前值。
  2. 指令执行位置。
  3. 当前 try、for、while 等控制流上下文。
  4. 必要的异常处理状态。

看一个例子:

复制代码
def accumulate():
    total = 0
    for i in range(3):
        total += i
        yield total

gen = accumulate()
print(next(gen))    # 0
print(next(gen))    # 1
print(next(gen))    # 3

这里 total 会在多次恢复执行时持续存在。如果换成普通函数,函数返回后其局部状态早已消失;而生成器会在每次挂起时保留上下文。

可以把它理解成:

生成器对象是"携带执行现场的可恢复函数实例"。

这也是它比单纯"回调式逐个返回值"更强大的原因。


五、for 循环为什么天然支持生成器

很多人会用 next 手动驱动生成器,但实际工程里最常见的是 for。原因是 for 本质上就是迭代器协议的消费器。

例如:

复制代码
def squares(n):
    for i in range(n):
        yield i * i

for value in squares(5):
    print(value)

for 循环内部做的事情,本质上等价于:

复制代码
gen = squares(5)
while True:
    try:
        value = next(gen)
        print(value)
    except StopIteration:
        break

因此,生成器并不是"for 的特殊对象",而是因为它实现了迭代器协议,所以能被 for 自然消费。

这个认知非常关键,因为它意味着生成器不仅可以配合 for,也可以配合所有接受可迭代对象的标准库函数:

复制代码
print(list(squares(5)))        # [0, 1, 4, 9, 16]
print(tuple(squares(5)))       # (0, 1, 4, 9, 16)
print(max(squares(5)))         # 16

六、惰性计算是生成器最核心的工程价值

生成器的最大价值往往不是"语法优雅",而是惰性求值。所谓惰性求值,就是:

只有当调用方真正需要下一个值时,生成器才继续执行并产出该值。

看一个大数据场景:

复制代码
def read_large_file(path):
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            yield line.rstrip("\n")

for line in read_large_file("huge.log"):
    if "ERROR" in line:
        print(line)

如果这里返回列表,就意味着必须先把整个文件全部读入内存,再开始过滤。对于大文件,这会带来明显问题:

  1. 内存占用高。
  2. 首次结果出现慢。
  3. 无法做真正流式处理。

而生成器实现的是:

  1. 读一行。
  2. 处理一行。
  3. 继续下一行。

这类模式在日志处理、数据库游标消费、网络流、消息队列、爬虫管道、机器学习数据迭代中都非常常见。

再看列表方案与生成器方案的对比:

复制代码
def even_squares_list(n):
    result = []
    for i in range(n):
        if i % 2 == 0:
            result.append(i * i)
    return result

def even_squares_gen(n):
    for i in range(n):
        if i % 2 == 0:
            yield i * i

print(even_squares_list(10))
print(list(even_squares_gen(10)))

小数据下两者输出相同,但规模扩大后,生成器的内存优势会越来越明显。


七、生成器只能遍历一次,这既是特性,也是限制

生成器是一次性消费对象。遍历结束后,不能自动重置。

示例:

复制代码
def numbers():
    for i in range(3):
        yield i

gen = numbers()

print(list(gen))    # [0, 1, 2]
print(list(gen))    # []

第二次为空,不是 bug,而是因为生成器已经耗尽。

这与列表不同:

复制代码
data = [0, 1, 2]
print(list(data))   # [0, 1, 2]
print(list(data))   # [0, 1, 2]

因此,工程上要区分两个问题:

  1. 我需要的是一次性流式消费,还是可重复访问的数据集合。
  2. 我需要的是节省内存,还是多次重用结果。

如果需要多次遍历,通常有三种选择:

  1. 重新创建生成器。
  2. 先把结果物化成列表或元组。
  3. 设计成返回"生成器工厂",而不是返回单个已创建的生成器。

例如:

复制代码
def number_stream():
    for i in range(3):
        yield i

print(list(number_stream()))
print(list(number_stream()))

这才是可重复使用的形式,因为每次都重新创建了一个新生成器。


八、return 在生成器里意味着什么

普通函数中的 return 表示"返回某个结果并结束"。在生成器里,return 仍然表示结束,但它的语义更细:

  1. return 会终止生成器。
  2. 它不会像普通函数那样把值直接交给 for 循环。
  3. 如果写成 return value,这个值会附着在 StopIteration.value 上。

看例子:

复制代码
def gen():
    yield 1
    yield 2
    return "done"

g = gen()

print(next(g))
print(next(g))

try:
    next(g)
except StopIteration as e:
    print("stopped with:", e.value)

输出中,done 不会被 for 自动接收,但底层是可见的。

这说明一个重要事实:

生成器既可以"产出数据",也可以"最终返回一个结束值",只是这个结束值通常只有底层调用方才会关心。

这也正是后面理解 yield from 的关键前提。


九、send:生成器不只是产出值,也可以接收值

如果说普通迭代器是"单向流",那么生成器通过 send 获得了"双向通信"能力。

先看最小例子:

复制代码
def receiver():
    print("generator started")
    value = yield "ready"
    print("received:", value)
    yield "finished"

gen = receiver()

print(next(gen))            # 启动生成器,得到 ready
print(gen.send(42))         # 向 yield 表达式送入 42

这里需要精确理解一句话:

yield 既能把值送给外部,也能作为表达式接收外部通过 send 传回来的值。

也就是说:

复制代码
value = yield "ready"

这句分成两半理解:

  1. 先把 ready 产出给外部。
  2. 等下次恢复执行时,把 send 传入的参数赋给 value。

更完整的例子:

复制代码
def accumulator():
    total = 0
    while True:
        number = yield total
        if number is None:
            break
        total += number

gen = accumulator()

print(next(gen))        # 0
print(gen.send(10))     # 10
print(gen.send(5))      # 15

try:
    gen.send(None)
except StopIteration:
    print("accumulator stopped")

这个例子展示了生成器的另一个身份:

它不仅可以是数据源,也可以是状态机。


十、为什么第一次 send 不能直接发送非 None 值

这是生成器使用中一个非常经典的细节。下面代码会报错:

复制代码
def echo():
    value = yield
    print(value)

gen = echo()
gen.send(123)

原因是生成器刚创建时,尚未执行到第一个 yield 位置,因此没有"挂起点"可以接收外部值。必须先把它推进到第一个 yield。

正确写法:

复制代码
def echo():
    value = yield
    print("received:", value)

gen = echo()
next(gen)
try:
    gen.send(123)
except StopIteration:
    pass

所以规则是:

第一次恢复生成器时,只能用 next(gen) 或 gen.send(None)。

这是语言语义,不是实现偶然。


十一、throw:向生成器内部注入异常

生成器不仅能接收正常数据,也能从外部接收异常。这个能力由 throw 提供。

示例:

复制代码
def worker():
    try:
        yield 1
        yield 2
    except ValueError as e:
        yield f"caught: {e}"
    yield 3

gen = worker()

print(next(gen))                    # 1
print(gen.throw(ValueError("bad"))) # caught: bad
print(next(gen))                    # 3

这里发生的事情是:

  1. 生成器先产出 1。
  2. 外部不是继续 next,而是向挂起点抛入 ValueError。
  3. 生成器内部的 try/except 捕获该异常。
  4. 然后继续运行。

throw 的工程价值在于:它允许外部调度器把错误信号注入协作式执行流程中。虽然日常业务代码不常直接使用,但在早期协程框架、任务调度器、控制流库中非常重要。


十二、close:显式终止生成器

close 用于通知生成器应尽快结束。其本质是向生成器内部注入 GeneratorExit。

看例子:

复制代码
def sample():
    try:
        yield 1
        yield 2
    finally:
        print("cleaning up")

gen = sample()
print(next(gen))
gen.close()

输出会触发 finally 中的清理逻辑。

这说明生成器不仅是"会暂停的函数",还是"可管理生命周期的执行体"。当它内部持有文件句柄、网络连接、锁、事务上下文时,这一点尤其重要。

例如:

复制代码
def read_lines(path):
    f = open(path, "r", encoding="utf-8")
    try:
        for line in f:
            yield line
    finally:
        print("closing file")
        f.close()

如果外部提前停止消费,显式 close 可以让资源及时释放。


十三、yield from:真正的生成器委托机制

很多文章把 yield from 解释成"把子可迭代对象一个个 yield 出去",这只是表层现象。它更准确的定义是:

yield from 把当前生成器的控制权委托给另一个迭代器或生成器,并自动转发 next、send、throw、close,同时还能接收子生成器的最终返回值。

先看最简单的形式:

复制代码
def sub():
    yield 1
    yield 2

def main():
    yield 0
    yield from sub()
    yield 3

print(list(main()))    # [0, 1, 2, 3]

如果不用 yield from,需要手写:

复制代码
def main_manual():
    yield 0
    for item in sub():
        yield item
    yield 3

但这只是"值的转发"。yield from 的真正强大之处,在于它还能处理 return 值:

复制代码
def sub():
    yield 1
    yield 2
    return "sub finished"

def main():
    result = yield from sub()
    yield f"result was: {result}"

print(list(main()))

输出中最后一项会拿到子生成器 return 的结果。

这说明 yield from 不只是展开序列,而是在语言层面建立了一条"父生成器与子生成器之间的完整委托通道"。


十四、yield from 与手写 for 循环的本质区别

为了看清这个差异,比较两段代码。

第一种,手写 for:

复制代码
def sub():
    received = yield 1
    yield f"sub got {received}"

def outer_manual():
    for item in sub():
        yield item

第二种,yield from:

复制代码
def sub():
    received = yield 1
    yield f"sub got {received}"

def outer_delegate():
    yield from sub()

对于 send、throw、close 来说,两者行为不同。yield from 会把这些操作自动继续传递给 sub;而手写 for 只是普通地把 next 拿到的值再 yield 出来,无法完整复现委托语义。

因此,严谨地说:

yield from 不是一个简写循环,而是生成器协议的组合器。


十五、生成器表达式:语法紧凑,但语义仍然是惰性迭代

生成器表达式写法与列表推导式很像:

复制代码
squares_list = [x * x for x in range(5)]
squares_gen = (x * x for x in range(5))

print(squares_list)         # [0, 1, 4, 9, 16]
print(squares_gen)          # <generator object ...>
print(list(squares_gen))    # [0, 1, 4, 9, 16]

两者的关键差异不是括号,而是求值时机:

  1. 列表推导式会立即构造完整列表。
  2. 生成器表达式只在消费时逐项计算。

这在链式处理中很常见:

复制代码
total = sum(x * x for x in range(1_000_000))
print(total)

这里如果写成列表推导式:

复制代码
total = sum([x * x for x in range(1_000_000)])

通常就会额外创建一个庞大中间列表,而实际上 sum 并不需要一次拿到所有值。

所以一个常见实践是:

如果最终消费者是 sum、any、all、max、min、join 之外的某些支持迭代的聚合器,并且不需要中间结果复用,优先考虑生成器表达式。


十六、生成器管道:把复杂处理拆成一条数据流

生成器在工程上非常适合构建流式处理管道。每一层只做一件事,并通过 yield 向下游传递。

示例:读取日志、过滤错误、提取时间戳。

复制代码
def read_lines(lines):
    for line in lines:
        yield line.strip()

def filter_errors(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

def extract_timestamps(lines):
    for line in lines:
        parts = line.split(" ", 1)
        yield parts[0]

raw_lines = [
    "2026-04-20 INFO startup",
    "2026-04-20 ERROR database down",
    "2026-04-20 ERROR timeout",
]

pipeline = extract_timestamps(filter_errors(read_lines(raw_lines)))

for item in pipeline:
    print(item)

这种风格的优势是:

  1. 每层职责单一。
  2. 中间结果不必一次性物化。
  3. 可以自然拼接。
  4. 适合超大数据流。

如果用列表中间态实现,往往会写成:

  1. 先清洗成一个列表。
  2. 再过滤成另一个列表。
  3. 再映射成第三个列表。

这样会产生更多中间对象和内存压力。


十七、生成器适合表达有限流,也适合表达无限流

生成器并不要求必须有限。它非常适合描述概念上"没有终点"的序列。

例如斐波那契无限流:

复制代码
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

gen = fibonacci()

for _ in range(10):
    print(next(gen))

普通列表无法自然表示"无限序列",但生成器可以,因为它不需要提前构造所有值。

这也说明生成器非常适合以下场景:

  1. 实时数据源。
  2. 事件流。
  3. 概念上的无限序列。
  4. 外部驱动、随取随算的计算流。

当然,这也要求消费端必须有终止条件,否则会无限运行。


十八、生成器里的异常传播机制

生成器内部出现异常时,和普通函数一样会向外传播;不同之处在于,它可能在多次恢复执行中的某一次才抛出。

示例:

复制代码
def broken():
    yield 1
    raise RuntimeError("something went wrong")

gen = broken()

print(next(gen))
try:
    print(next(gen))
except RuntimeError as e:
    print("caught:", e)

这说明生成器的错误并不是在创建对象时出现,而是在执行推进到相应位置时才暴露。

因此,在工程上要非常明确:

  1. 创建生成器对象通常不代表逻辑已成功。
  2. 真正的失败可能发生在消费阶段。
  3. 生成器 API 的调用方必须对迭代过程中的异常负责。

这个特性和数据库游标、网络流、延迟计算系统非常一致。


十九、生成器与上下文资源:不要忽视中途停止消费的问题

看一个例子:

复制代码
def data_source():
    print("open resource")
    try:
        for i in range(5):
            yield i
    finally:
        print("close resource")

gen = data_source()
print(next(gen))
print(next(gen))
gen.close()

如果外部不再继续消费,但又没有让生成器正常结束,那么内部资源可能会延迟释放。因此写生成器时,凡是涉及资源管理,都应尽量使用 try/finally 或 with。

例如:

复制代码
def read_file(path):
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            yield line

这里即便消费提前结束,只要生成器被关闭或最终被回收,with 的退出逻辑都更清晰。

原则上讲:

生成器非常适合流式资源使用,但也要求开发者更认真处理生命周期。


二十、生成器不是并发模型,但它启发了协程模型

很多现代 Python 开发者接触协程时首先想到 async 和 await,但从语言演化史看,生成器是协程的重要前身。特别是 send、throw、close、yield from 这些能力,直接铺垫了后来的协程协议。

下面这个例子不是现代推荐写法,但能帮助理解生成器为什么被视为"协作式控制流"的基础:

复制代码
def task():
    print("task step 1")
    yield
    print("task step 2")
    yield
    print("task step 3")

t = task()
next(t)
next(t)
try:
    next(t)
except StopIteration:
    pass

这个任务不是线程,也不是进程,它只是一个可被调度器反复恢复的执行体。早期很多协程库就是基于这类思想构建的。

不过必须明确:

  1. 普通生成器不是现代异步编程的完整替代。
  2. 现代异步代码应优先使用 async def 和 await。
  3. 但理解生成器,有助于更深刻地理解协程为何能够暂停和恢复。

二十一、异步生成器:生成器思想在异步世界的延伸

Python 还提供了异步生成器,用于异步上下文中的流式产出。

示例:

复制代码
import asyncio

async def async_counter():
    for i in range(3):
        await asyncio.sleep(0.1)
        yield i

async def main():
    async for item in async_counter():
        print(item)

asyncio.run(main())

异步生成器与普通生成器的关系可以概括为:

  1. 普通生成器服务于同步迭代。
  2. 异步生成器服务于异步迭代。
  3. 一个通过 for 消费。
  4. 一个通过 async for 消费。

这说明生成器背后的思想非常稳定:

无论同步还是异步,本质都是"把序列生产改造成可暂停、可恢复、按需产出的过程"。


二十二、常见误区一:把生成器当成"性能一定更高"的方案

生成器经常能减少内存占用,但它并不意味着所有场景都更快。

看例子:

复制代码
def list_version():
    return [x * 2 for x in range(1000)]

def gen_version():
    return (x * 2 for x in range(1000))

如果最终你马上就要把生成器全部转成列表:

复制代码
data = list(gen_version())

那你仍然会物化整个结果,甚至还多了一层生成器调度开销。

所以应当严谨地说:

  1. 生成器通常更省内存。
  2. 生成器不保证绝对更快。
  3. 如果最终必须完整落地全部数据,列表有时反而更直接。
  4. 生成器最适合"边生产边消费"。

二十三、常见误区二:在调试时误以为生成器"没有执行"

初学者常见疑问是:

"我明明调用了函数,为什么函数体里的打印没执行?"

例子:

复制代码
def demo():
    print("running")
    yield 1

gen = demo()
print("created")

只会打印 created,而不会打印 running。因为生成器函数被调用时只是创建生成器对象,没有推进执行。

真正执行是在:

复制代码
print(next(gen))

之后才会打印 running。

因此,调试生成器时必须分清两个时刻:

  1. 创建时刻。
  2. 消费时刻。

很多"逻辑没生效"的误判,本质上只是没有触发消费。


二十四、常见误区三:把生成器返回给多个消费者共享

因为生成器是单次消费对象,所以把同一个生成器传给多个地方时,经常会出现"有人读完了,别人就没了"的问题。

示例:

复制代码
def source():
    for i in range(5):
        yield i

gen = source()

print(next(gen))     # 0
print(list(gen))     # [1, 2, 3, 4]
print(list(gen))     # []

如果多个模块共享同一个生成器对象,就相当于共享一个会不断前进的游标。

解决方式通常是:

  1. 共享可重建的生成器工厂。
  2. 共享已经物化的数据。
  3. 明确约定只有单一消费者。

在接口设计上,这一点必须提前说明,否则非常容易引入隐蔽 bug。


二十五、生成器与列表推导式该如何选择

这个问题没有绝对答案,但有相对稳定的判断标准。

适合使用列表的场景

  1. 数据量不大。
  2. 需要多次遍历。
  3. 需要随机访问或切片。
  4. 需要立即看到完整结果。

示例:

复制代码
values = [x * x for x in range(10)]
print(values[3])
print(values[-1])

适合使用生成器的场景

  1. 数据量大或可能无限。
  2. 只需要单次流式消费。
  3. 处理过程适合流水线。
  4. 希望尽快得到首批结果,而不是等待全部完成。

示例:

复制代码
values = (x * x for x in range(10_000_000))
print(next(values))
print(next(values))

所以真正的选择依据不是"哪种语法更高级",而是数据访问模式。


二十六、工程上如何写出可维护的生成器

生成器很强,但也容易被写成难调试的"隐式控制流"。比较稳妥的实践包括:

1. 让每个生成器职责单一

复制代码
def filter_positive(numbers):
    for n in numbers:
        if n > 0:
            yield n

这种风格比一个生成器内部既过滤、又映射、又统计、又记录日志更容易维护。

2. 明确说明是否一次性消费

复制代码
def load_records():
    for i in range(3):
        yield {"id": i}

调用方需要知道这是流,而不是静态集合。

3. 涉及资源时始终考虑 finally 或 with

复制代码
def open_and_stream(path):
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            yield line

4. 不要为"炫技"而滥用 send 和 throw

这些机制很强,但会显著增加认知负担。大多数业务代码只需要普通 yield 即可。

5. 对外接口尽量稳定

如果某个函数对调用方来说更像"数据集合",而不是"数据流",那么直接返回列表可能更符合语义。


二十七、一个完整案例:用生成器构建日志处理流水线

下面给出一个稍完整的工程化例子,展示生成器如何组合出清晰的数据处理管道。

复制代码
def read_lines(lines):
    for line in lines:
        yield line.strip()

def parse_log(lines):
    for line in lines:
        parts = line.split(" ", 2)
        if len(parts) == 3:
            date, level, message = parts
            yield {
                "date": date,
                "level": level,
                "message": message,
            }

def filter_level(records, target_level):
    for record in records:
        if record["level"] == target_level:
            yield record

def format_messages(records):
    for record in records:
        yield f'{record["date"]}: {record["message"]}'

raw_data = [
    "2026-04-20 INFO startup complete",
    "2026-04-20 ERROR database unavailable",
    "2026-04-20 WARNING retrying",
    "2026-04-20 ERROR timeout happened",
]

pipeline = format_messages(
    filter_level(
        parse_log(
            read_lines(raw_data)
        ),
        "ERROR"
    )
)

for item in pipeline:
    print(item)

这个例子体现出生成器在工程中的几个核心价值:

  1. 每一层输入输出都统一为可迭代流。
  2. 数据处理过程自然分层。
  3. 中间结果无需全部落地。
  4. 上游和下游耦合度低。

这就是生成器真正擅长的场景:面向流,而不是面向块。


二十八、从语言设计角度再看生成器

如果从更抽象的角度总结,生成器解决的是一个经典问题:

如何把"一个会逐步产生结果的过程",表达成"一个可被统一消费的对象"。

传统函数更适合表达"输入一次,输出一次"的映射关系;而现实世界里大量问题并不是这样:

  1. 文件是一行一行读的。
  2. 网络响应是一块一块到的。
  3. 日志是一条一条处理的。
  4. 事件是一件一件发生的。
  5. 无限序列理论上没有完整终点。

生成器的优雅之处在于,它没有发明一套完全陌生的新模型,而是在普通函数基础上,只通过 yield 就把"过程"改造成了"可恢复的迭代器"。

这也是为什么生成器一直是 Python 最具代表性的语言特性之一。


结论

生成器的本质,不是"更省内存的列表替代品",而是"把逐步计算过程对象化、迭代化、惰性化"的语言机制。理解生成器,应当把握以下几个层次:

  1. 语法层面:函数中出现 yield,就会变成生成器函数。
  2. 对象层面:调用生成器函数得到的是生成器对象,它实现了迭代器协议。
  3. 执行层面:生成器可以暂停和恢复,保存完整执行现场。
  4. 通信层面:除了 next 取值,还可以用 send、throw、close 进行控制。
  5. 组合层面:yield from 提供完整委托机制。
  6. 工程层面:生成器最适合流式处理、大数据迭代、无限序列和处理管道。
  7. 边界层面:它是一次性消费对象,不适合所有场景,也不天然保证更快。

如果只记一句话,最值得记住的是:

生成器让"计算结果"不再必须一次性存在,而可以作为"按需推进的过程"被消费。

相关推荐
郝学胜-神的一滴2 小时前
系统设计:新鲜事系统扩展与优化
java·python·职场和发展·php·软件工程·软件构建
思绪无限2 小时前
YOLOv5至YOLOv12升级:零售柜商品检测软件的设计与实现(完整代码+界面+数据集项目)
人工智能·python·深度学习·目标检测·计算机视觉·零售柜商品检测·yolov12
zl_dfq2 小时前
Python学习6 之 【Lambda表达式、列表与元组、推导式】
python
kishu_iOS&AI2 小时前
深度学习 —— 正则化&批量归一化BN
人工智能·pytorch·python·深度学习
天天进步20152 小时前
Python全栈项目实战:自建高效多媒体处理工具
开发语言·python
waterHBO2 小时前
python + fast-wahisper 读取麦克风,实现语音转录,而且是实时转录。
开发语言·python
reasonsummer2 小时前
【教学类-160-09】20260417 AI视频培训-练习010“豆包AI视频《熊猫找朋友》+豆包图片风格:水墨画”
python·音视频·豆包视频
JaydenAI2 小时前
[FastMCP设计、原理与应用-15]挂载一个MCP服务器就像挂载一个目录一样容易
python·ai编程·ai agent·mcp·fastmcp
甄心爱学习2 小时前
【项目实训(个人4)】
前端·vue.js·python