欢迎光临
我们一直在努力

Python 迭代器与生成器:用 yield 写出更优雅的代码

引言

你有没有想过,for x in [1, 2, 3] 背后到底发生了什么?为什么列表、字典、字符串、文件对象都能用 for 循环遍历,而自定义的类却不行?为什么处理一个 10GB 的日志文件时,用 for line in f 内存占用恒定,而 f.readlines() 却会把内存撑爆?

这些问题的答案都指向同一个核心概念:迭代器(Iterator)。而在迭代器的基础上,Python 又提供了更优雅的语法糖——生成器(Generator),用 yield 关键字就能轻松实现惰性计算、无限序列和数据管道,让代码既简洁又高效。

本文将从 for 循环的底层原理讲起,系统梳理可迭代对象、迭代器、生成器的关系与区别,深入讲解 yield 的各种用法,并结合实际场景展示如何用生成器写出更优雅的 Python 代码。


一、可迭代对象与迭代器:for 循环的底层原理

1.1 什么是可迭代对象

可迭代对象(Iterable) 是实现了 __iter__() 方法(或 __getitem__() 方法)的对象,能够被 for 循环遍历。列表、元组、字符串、字典、集合、文件对象都是可迭代对象。

from collections.abc import Iterable

print(isinstance([1, 2, 3], Iterable)) # True
print(isinstance("hello", Iterable)) # True
print(isinstance({"a": 1}, Iterable)) # True
print(isinstance(123, Iterable)) # False,整数不可迭代

1.2 什么是迭代器

迭代器(Iterator) 是实现了迭代器协议的对象,即同时实现了:

  • __iter__():返回迭代器自身
  • __next__():返回下一个元素,没有元素时抛出 StopIteration

迭代器是一个"数据流",只能往前遍历,不能回头,遍历完就空了。

from collections.abc import Iterator

lst = [1, 2, 3]
print(isinstance(lst, Iterator)) # False,列表是可迭代对象,不是迭代器

it = iter(lst) # 调用 iter() 获取迭代器
print(isinstance(it, Iterator)) # True

print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
# print(next(it)) # StopIteration,没有更多元素

1.3 for 循环的本质

for x in iterable 实际上做了两件事:

  • 调用 iter(iterable) 获取迭代器;
  • 不断调用 next(iterator) 获取元素,直到抛出 StopIteration 时结束循环。
  • # for 循环
    for x in [1, 2, 3]:
    print(x)

    # 等价于下面的手动实现
    it = iter([1, 2, 3])
    while True:
    try:
    x = next(it)
    print(x)
    except StopIteration:
    break

    理解了这个原理,你就明白为什么文件对象可以逐行读取大文件而不占内存——文件对象本身就是迭代器,next(f) 每次只读一行。

    1.4 可迭代对象 vs 迭代器

    特性可迭代对象迭代器
    必须实现 __iter__() __iter__() + __next__()
    能否多次遍历 可以(如列表) 只能一次,遍历完就空
    内存占用 通常存储全部元素 惰性计算,只存当前状态
    例子 list、str、dict、tuple 文件对象、生成器

    # 可迭代对象可以多次遍历
    lst = [1, 2, 3]
    print(list(lst)) # [1, 2, 3]
    print(list(lst)) # [1, 2, 3],还能再遍历

    # 迭代器只能遍历一次
    it = iter([1, 2, 3])
    print(list(it)) # [1, 2, 3]
    print(list(it)) # [],已经空了


    二、自定义迭代器:实现迭代器协议

    要让自己的类支持 for 循环,只需实现 __iter__() 和 __next__():

    class Countdown:
    """从 n 倒数到 1 的迭代器"""
    def __init__(self, start):
    self.current = start

    def __iter__(self):
    return self # 返回自身,因为自身就是迭代器

    def __next__(self):
    if self.current <= 0:
    raise StopIteration
    value = self.current
    self.current -= 1
    return value

    # 使用
    for num in Countdown(5):
    print(num)
    # 5 4 3 2 1

    # 也可以手动调用
    cd = Countdown(3)
    print(next(cd)) # 3
    print(next(cd)) # 2
    print(next(cd)) # 1

    自定义迭代器虽然强大,但需要维护状态(self.current)、写两个方法,比较繁琐。有没有更简单的方式?有——生成器。


    三、生成器基础:yield 的魔法

    3.1 什么是生成器

    生成器(Generator) 是一种特殊的迭代器,用函数语法实现,不需要写 __iter__ 和 __next__,只需用 yield 关键字返回值。

    生成器函数和普通函数的区别:

    • 普通函数用 return 返回值,返回后函数结束;
    • 生成器函数用 yield 返回值,返回后函数暂停,下次调用 next() 时从暂停处继续执行。

    def countdown(start):
    """生成器版本的倒数"""
    current = start
    while current > 0:
    yield current # 暂停并返回 current
    current -= 1

    # 调用生成器函数返回生成器对象,不会执行函数体
    gen = countdown(5)
    print(type(gen)) # <class 'generator'>

    # 用 next() 驱动执行
    print(next(gen)) # 5
    print(next(gen)) # 4
    print(next(gen)) # 3

    # 也可以用 for 循环(生成器是迭代器)
    for num in countdown(5):
    print(num)
    # 5 4 3 2 1

    对比自定义迭代器类,生成器函数简洁了太多——状态自动保存在局部变量中,yield 自动处理暂停和恢复。

    3.2 生成器的执行流程

    def simple_gen():
    print("开始执行")
    yield 1
    print("第一次 yield 之后")
    yield 2
    print("第二次 yield 之后")
    yield 3
    print("结束")

    gen = simple_gen()
    print("— 调用生成器函数后,函数体还没执行 —")

    print(next(gen))
    # 开始执行
    # 1

    print(next(gen))
    # 第一次 yield 之后
    # 2

    print(next(gen))
    # 第二次 yield 之后
    # 3

    # print(next(gen))
    # 结束
    # StopIteration

    关键点:

    • 调用生成器函数不会执行函数体,只返回生成器对象。
    • 每次 next() 执行到 yield 处暂停,返回 yield 后面的值。
    • 再次 next() 从暂停处继续,直到函数结束抛出 StopIteration。

    3.3 生成器表达式

    和列表推导式类似,生成器用圆括号创建生成器表达式:

    # 列表推导式:一次性生成所有元素,占用内存
    lst = [x ** 2 for x in range(1000000)]
    print(type(lst)) # <class 'list'>
    print(lst.__sizeof__()) # 占用大量内存

    # 生成器表达式:惰性计算,不占内存
    gen = (x ** 2 for x in range(1000000))
    print(type(gen)) # <class 'generator'>
    print(gen.__sizeof__()) # 几乎不占内存

    # 使用时才计算
    print(next(gen)) # 0
    print(next(gen)) # 1
    print(sum(gen)) # 可以直接传给 sum、list 等函数

    生成器表达式和列表推导式语法几乎一样,只是把 [] 换成 ()。当数据量大、只需遍历一次时,优先用生成器表达式。

    3.4 生成器的惰性计算优势

    生成器最大的价值是惰性计算——只在需要时才生成下一个值,不提前存储所有数据。这使得生成器特别适合:

    • 处理大数据集/大文件
    • 表示无限序列
    • 构建数据处理管道

    # 无限斐波那契数列(用列表不可能实现,因为无限)
    def fibonacci():
    a, b = 0, 1
    while True:
    yield a
    a, b = b, a + b

    # 取前 10 个
    fib = fibonacci()
    for _ in range(10):
    print(next(fib), end=" ")
    # 0 1 1 2 3 5 8 13 21 34


    四、生成器进阶:send、throw 与 yield from

    4.1 send:向生成器发送数据

    yield 不仅能向外输出数据,还能接收外部传入的数据。用 send(value) 可以把值发送给生成器,yield 表达式的返回值就是发送的值。

    def echo_generator():
    print("生成器启动")
    while True:
    received = yield # yield 在这里暂停,接收 send 传来的值
    print(f"收到: {received}")

    gen = echo_generator()
    next(gen) # 必须先 next() 启动,执行到第一个 yield
    # 生成器启动

    gen.send("你好") # 收到: 你好
    gen.send("世界") # 收到: 世界
    gen.send(123) # 收到: 123

    注意:第一次必须用 next(gen) 或 gen.send(None) 启动生成器,因为还没执行到 yield 处,无法接收值。

    send 让生成器不仅能"拉取"数据,还能"推送"数据,这是协程(coroutine)的基础。

    4.2 throw:向生成器抛出异常

    throw(exc_type) 可以在生成器暂停处抛出一个异常,生成器可以捕获并处理:

    def safe_generator():
    while True:
    try:
    received = yield
    print(f"处理: {received}")
    except ValueError as e:
    print(f"捕获到 ValueError: {e}")

    gen = safe_generator()
    next(gen)
    gen.send("正常数据") # 处理: 正常数据
    gen.throw(ValueError("出错了")) # 捕获到 ValueError: 出错了
    gen.send("继续处理") # 处理: 继续处理,生成器还活着

    4.3 close:关闭生成器

    close() 可以主动关闭生成器,之后再 next() 会抛出 StopIteration:

    gen = (x for x in range(5))
    print(next(gen)) # 0
    gen.close()
    # print(next(gen)) # StopIteration

    4.4 yield from:委托给子生成器

    yield from 可以把一个生成器的迭代委托给另一个可迭代对象,简化嵌套生成器的写法:

    # 不用 yield from
    def nested_gen():
    for x in [1, 2, 3]:
    yield x
    for x in (4, 5, 6):
    yield x

    print(list(nested_gen())) # [1, 2, 3, 4, 5, 6]

    # 用 yield from,更简洁
    def nested_gen2():
    yield from [1, 2, 3]
    yield from (4, 5, 6)

    print(list(nested_gen2())) # [1, 2, 3, 4, 5, 6]

    yield from 不只是语法糖,它还自动处理了 send、throw、close 在子生成器和调用者之间的转发,是构建复杂生成器管道和协程的重要工具。

    # yield from 自动转发 send
    def sub_gen():
    while True:
    x = yield
    print(f"子生成器收到: {x}")

    def main_gen():
    yield from sub_gen() # 委托给子生成器

    gen = main_gen()
    next(gen)
    gen.send("hello") # 子生成器收到: hello


    五、实际应用场景

    5.1 大文件逐行处理

    def read_large_file(filepath):
    """逐行读取大文件,内存占用恒定"""
    with open(filepath, "r", encoding="utf-8") as f:
    for line in f:
    yield line.strip()

    # 处理 10GB 的日志文件
    for line in read_large_file("huge.log"):
    if "ERROR" in line:
    process_error(line)

    5.2 数据处理管道

    生成器可以像 Unix 管道一样串联起来,每一步都是一个生成器,数据流式传递:

    def read_lines(filepath):
    with open(filepath, "r", encoding="utf-8") as f:
    for line in f:
    yield line

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

    def parse_lines(lines):
    for line in lines:
    parts = line.split("|")
    yield {"time": parts[0], "level": parts[1], "msg": parts[2]}

    # 构建管道:读文件 → 过滤错误 → 解析
    lines = read_lines("app.log")
    errors = filter_errors(lines)
    records = parse_lines(errors)

    # 消费数据
    for record in records:
    save_to_db(record)

    整个管道是惰性的,只有在 for record in records 时才真正逐行处理,内存占用极低。

    5.3 分页数据获取

    def fetch_all_pages(api_url, page_size=100):
    """自动翻页获取所有数据"""
    page = 1
    while True:
    data = requests.get(api_url, params={"page": page, "size": page_size}).json()
    if not data["items"]:
    break
    for item in data["items"]:
    yield item
    page += 1

    # 使用方不需要关心翻页逻辑
    for item in fetch_all_pages("https://api.example.com/items"):
    process(item)

    5.4 生成器替代递归(避免栈溢出)

    某些递归算法可以用生成器改写,避免递归深度过大导致栈溢出:

    # 递归版二叉树中序遍历(深度大时栈溢出)
    def inorder_recursive(node):
    if node is None:
    return
    inorder_recursive(node.left)
    yield node.value
    inorder_recursive(node.right)

    # 生成器 + 显式栈版(不会栈溢出)
    def inorder_generator(root):
    stack = []
    node = root
    while stack or node:
    while node:
    stack.append(node)
    node = node.left
    node = stack.pop()
    yield node.value
    node = node.right


    六、常见坑点

    坑点 1:生成器只能遍历一次

    gen = (x for x in range(3))
    print(list(gen)) # [0, 1, 2]
    print(list(gen)) # [],已经空了!

    # 如果需要多次遍历,用列表,或每次重新创建生成器

    坑点 2:生成器表达式作为函数参数时括号可省略

    # sum() 接受一个可迭代对象,生成器表达式的括号可以省略
    print(sum(x ** 2 for x in range(10))) # 285

    # 但如果有其他参数,不能省略
    # print(sum(x ** 2 for x in range(10), 10)) # 语法错误
    print(sum((x ** 2 for x in range(10)), 10)) # 正确,295

    坑点 3:在生成器中用 return

    生成器函数中的 return value 会终止生成器,value 会作为 StopIteration 的 value 属性,而不是被 next() 返回:

    def gen_with_return():
    yield 1
    yield 2
    return "结束了"

    gen = gen_with_return()
    print(next(gen)) # 1
    print(next(gen)) # 2
    try:
    next(gen)
    except StopIteration as e:
    print(e.value) # 结束了,return 的值在这里

    坑点 4:生成器不是列表,没有 len()、不能下标访问

    gen = (x for x in range(10))
    # len(gen) # TypeError,生成器没有长度
    # gen[0] # TypeError,不能下标访问
    # 只能迭代,或转成 list(gen) 后操作

    坑点 5:闭包中的生成器延迟绑定

    # 经典坑:生成器表达式中的变量延迟绑定
    def create_generators():
    return (lambda x: i * x for i in range(5))

    gens = create_generators()
    print([g(2) for g in gens]) # [8, 8, 8, 8, 8],不是 [0, 2, 4, 6, 8]!
    # i 在生成器执行时才查找,此时 i 已经是 4


    七、最佳实践总结

  • 大数据用生成器,小数据用列表:数据量大、只需遍历一次时用生成器节省内存;需要多次访问、下标访问、长度时用列表。
  • 优先生成器表达式:简单的惰性序列用 (x for x in …),复杂逻辑用生成器函数。
  • 生成器函数用 yield,不要用 return 返回数据:return 只用于终止生成器,数据用 yield 返回。
  • 构建数据管道用生成器串联:每一步一个生成器,流式处理,内存友好。
  • send/throw 用于协程场景:普通迭代不需要,双向通信时才用。
  • yield from 委托子生成器:嵌套生成器用 yield from,自动处理转发。
  • 注意生成器只能遍历一次:消费完就空了,需要多次用列表或重新创建。
  • 不要在生成器中做耗时的 IO 而不处理异常:生成器内部异常会传播到 next() 调用处,做好异常处理。

  • 结语

    迭代器是 Python for 循环的底层基石,生成器则是迭代器的优雅实现——用 yield 关键字,你就能以函数的简洁语法,获得惰性计算、无限序列、数据管道等强大能力。

    理解了迭代器协议,你就能让自己的类支持 for 循环;掌握了生成器,你就能写出内存友好、逻辑清晰的数据流处理代码。而 send、throw、yield from 这些进阶特性,更是 Python 协程和异步编程的基础。

    记住一句话:当你需要处理大量数据且只需遍历一次时,先想想能不能用生成器。

    赞(0)
    未经允许不得转载:171主机测评 » Python 迭代器与生成器:用 yield 写出更优雅的代码
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址