Python生成器与协程:从迭代器到异步编程的进阶之路

一. 为什么需要生成器?

在Python编程中,我们经常需要处理大量数据或无限序列。如果一次性将所有数据加载到内存中,可能导致内存溢出。生成器(Generator)提供了一种惰性求值(Lazy Evaluation)的机制:它们可以逐个产生值,而不是一次性构建整个序列。

举个例子:读取一个10GB的日志文件,如果用`readlines()`会将整个文件加载到内存,而使用生成器逐行读取则只需要少量内存。生成器也是Python实现协程(Coroutine)的基础,而协程又是现代异步编程的核心。

本文将带你从最基础的迭代器概念开始,逐步深入生成器的底层原理、协程的演变,再到实战中的高阶应用。

二 . 迭代器协议与可迭代对象

理解生成器之前,必须先搞懂迭代器(Iterator)和可迭代对象(Iterable)。

2.1 可迭代对象(Iterable)

任何实现了`iter()`方法的对象都是可迭代对象。`iter()`应该返回一个迭代器。常见的可迭代对象包括:列表、元组、字符串、字典、集合等。

python 复制代码
# 判断是否为可迭代对象
from collections.abc import Iterable

print(isinstance([1, 2, 3], Iterable))  # True
print(isinstance("hello", Iterable))    # True
print(isinstance(123, Iterable))        # False

2.2 迭代器(Iterator)

迭代器实现了两个方法:`iter()` 和 `next()`。`iter()`返回自身,`next()`返回下一个元素,当没有元素时抛出`StopIteration`异常。

python 复制代码
class MyRange:
    """自定义一个可迭代对象(同时也是迭代器)"""
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current >= self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

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

# 手动迭代
it = MyRange(1, 3)
print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 抛出StopIteration

手动实现迭代器比较繁琐,生成器则提供了简便的创建方式。

三. 生成器函数:`yield` 的魔法

生成器函数是指包含`yield`关键字的函数。当调用生成器函数时,它不会立即执行函数体,而是返回一个生成器对象(属于迭代器的一种)。

3.1 第一个生成器

python 复制代码
class MyRange:
    """自定义一个可迭代对象(同时也是迭代器)"""
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current >= self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

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

# 手动迭代
it = MyRange(1, 3)
print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 抛出StopIteration

每次调用`next()`,生成器函数会从上一次`yield`暂停的地方继续执行,直到遇到下一个`yield`或函数返回。

3.2 生成器的常见用法:遍历大数据

python 复制代码
def read_large_file(file_path):
    """逐行读取大文件,每次只加载一行到内存"""
    with open(file_path, 'r', encoding='utf-8') as f:
        for line in f:
            yield line.strip()
# 使用示例(假设有一个10GB的日志文件)
for line in read_large_file('huge_log.txt'):
    process_line(line)  # 每次处理一行,内存占用极小

如果没有生成器,传统的`readlines()`会一次性将所有行读入列表,导致内存爆炸。

3.3 生成器的惰性特性

生成器不会计算所有值,而是按需计算。这在构造无限序列时非常有用。

python 复制代码
def fibonacci():
    """无限斐波那契数列生成器"""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

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

3.4 生成器与列表推导式的对比

列表推导式会立即生成整个列表,而生成器表达式则是惰性的。

python 复制代码
import sys

# 列表推导式:占用大量内存
list_squares = [x**2 for x in range(1000000)]
print(sys.getsizeof(list_squares))  # 约 8MB+

# 生成器表达式:几乎不占内存
gen_squares = (x**2 for x in range(1000000))
print(sys.getsizeof(gen_squares))   # 约 120 字节

# 但结果相同
print(next(gen_squares))  # 0
print(next(gen_squares))  # 1

四. 生成器表达式

生成器表达式语法与列表推导式类似,但使用圆括号而不是方括号。它是创建简单生成器的便捷方式。

python 复制代码
# 语法: (expression for item in iterable if condition)
even_gen = (x for x in range(10) if x % 2 == 0)

for num in even_gen:
    print(num)  # 0 2 4 6 8

生成器表达式可以组合使用,例如从多个源生成:

pairs = ((x, y) for x in range(3) for y in range(2))

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

注意:生成器表达式只能使用一次,迭代完毕后不能再使用。

gen = (x for x in range(3))

print(sum(gen)) # 3

print(sum(gen)) # 0,因为已经耗尽

五. 深入生成器:`send()`、`throw()` 与 `close()`

生成器不仅仅是`yield`值的单向管道。通过额外的方法,我们可以与生成器进行双向通信。

5.1 `send(value)` 方法

`send()`可以将一个值发送给生成器,成为`yield`表达式的返回值。首次调用必须传入`None`或者直接调用`next()`来启动生成器。

python 复制代码
def echo_generator():
    received = yield "准备就绪"
    print(f"收到: {received}")
    received = yield f"已收到: {received}"
    print(f"再次收到: {received}")
    yield "结束"

gen = echo_generator()
print(next(gen))           # 准备就绪(启动生成器)
print(gen.send("Hello"))   # 发送Hello,输出"收到: Hello",然后返回"已收到: Hello"
print(gen.send("World"))   # 发送World,输出"再次收到: World",然后返回"结束"
# next(gen)  # StopIteration

更实用的例子:使用生成器实现可交互的计数器。

python 复制代码
def accumulative_counter():
    """累加计数器,发送新的数值进行累加"""
    total = 0
    while True:
        increment = yield total
        if increment is None:
            break
        total += increment

counter = accumulative_counter()
next(counter)  # 启动,返回初始total=0
print(counter.send(5))   # 5
print(counter.send(3))   # 8
print(counter.send(2))   # 10

5.2 `throw(type, value=None, traceback=None)` 方法

在生成器当前暂停的`yield`处抛出一个异常。如果生成器内部捕获了该异常并继续执行,则返回下一个yield的值;否则异常会传播。

python 复制代码
def catch_exception_gen():
    try:
        yield "正常运行"
    except ValueError:
        print("捕获到ValueError")
        yield "处理异常后继续"
    yield "结束"

gen = catch_exception_gen()
print(next(gen))           # 正常运行
print(gen.throw(ValueError))  # 捕获到ValueError \n 处理异常后继续
print(next(gen))           # 结束

5.3 `close()` 方法

停止生成器。在生成器内部,如果在`yield`处被关闭,会抛出`GeneratorExit`异常(不会被捕获,除非主动捕获并最终`return`)。

python 复制代码
def generator_with_cleanup():
    try:
        yield 1
        yield 2
    except GeneratorExit:
        print("执行清理工作")
        return

gen = generator_with_cleanup()
print(next(gen))  # 1
gen.close()       # 执行清理工作
# 再调用next(gen) 会抛出StopIteration

六. 协程基础:从生成器到协程的演变

生成器可以暂停和恢复执行,而协程(Coroutine)更进一步:它可以让生成器不仅向外产出值,还能从外部接收值。因此,生成器可以当作协程使用。在Python 3.5之前,协程就是基于生成器实现的。

6.1 简单协程实现任务切换

下面展示一个使用生成器实现的多任务协作调度示例:

python 复制代码
def task1():
    for i in range(3):
        print(f"任务1 执行第{i}步")
        yield

def task2():
    for i in range(3):
        print(f"任务2 执行第{i}步")
        yield

def scheduler(*tasks):
    """简单任务调度器,轮流执行每个任务一步"""
    tasks = list(tasks)  # 每个任务都是一个生成器
    while tasks:
        task = tasks.pop(0)
        try:
            next(task)
            tasks.append(task)  # 未完成的任务放回队尾
        except StopIteration:
            pass

scheduler(task1(), task2())

输出:

```

任务1 执行第0步

任务2 执行第0步

任务1 执行第1步

任务2 执行第1步

任务1 执行第2步

任务2 执行第2步

```

这展示了协作式多任务的基本思想,也是`asyncio`事件循环的雏形。

6.2 使用`send`实现协程之间的双向通信

python 复制代码
def printer():
    """协程:打印接收到的消息"""
    while True:
        msg = yield
        print(f"打印: {msg}")
def repeater(count):
    """协程:重复发送消息"""
    for i in range(count):
        msg = yield
        yield f"重复: {msg}"

# 创建两个协程
p = printer()
next(p)  # 启动

r = repeater(2)
next(r)  # 启动

# 发送消息给repeater,再转发给printer
p.send(r.send("Hello"))
p.send(r.send("World"))

注意:`yield`既可以产出值(右侧),也可以接收值(左侧赋值)。这体现了生成器的双向能力。

七. `yield from`:委托生成器

Python 3.3引入了`yield from`语法,用于将迭代过程委托给另一个生成器(或任何可迭代对象)。它简化了生成器的嵌套和扁平化,也为协程提供了更好的支持。

7.1 基本用法:扁平化嵌套生成器

python 复制代码
def sub_generator():
    yield 1
    yield 2
    yield 3
def delegating_generator():
    yield "开始"
    yield from sub_generator()  # 委托
    yield "结束"

for value in delegating_generator():
    print(value)  # 开始 1 2 3 结束

不使用`yield from`的等效写法:

def old_way():

yield "开始"

for value in sub_generator():

yield value

yield "结束"

7.2 扁平化嵌套列表

`yield from`非常适合递归地处理嵌套结构:

python 复制代码
def flatten(nested_list):
    """扁平化任意嵌套的列表"""
    for item in nested_list:
        if isinstance(item, list):
            yield from flatten(item)  # 递归委托
        else:
            yield item

nested = [1, [2, [3, 4], 5], 6]
print(list(flatten(nested)))  # [1, 2, 3, 4, 5, 6]

7.3 `yield from` 在协程中的作用

当`yield from`应用于生成器对象时,它会建立双向通道,将调用方的`send()`、`throw()` 和 `close()` 直接传递给子生成器。这使得子生成器可以完全替代当前的生成器,是实现协程委托的关键。

python 复制代码
def sub_coroutine():
    try:
        while True:
            x = yield
            print(f"子协程收到: {x}")
    except GeneratorExit:
        print("子协程清理")

def main_coroutine():
    yield from sub_coroutine()  # 委托
    print("主协程结束")

main = main_coroutine()
next(main)   # 启动,进入子协程
main.send("Hello")  # 子协程收到: Hello
main.send("World")  # 子协程收到: World
main.close()        # 子协程清理

八. 异步编程基石:`asyncio` 与原生协程(`async/await`)

从Python 3.5开始,引入了`async`和`await`关键字,用于定义原生协程和等待异步操作。虽然它们与生成器在底层有关联,但语法和语义更清晰,且不能包含`yield`(但可以使用`await`等待另一个协程)。

8.1 原生协程的定义与调用

python 复制代码
import asyncio
async def say_hello():
    print("Hello")
    await asyncio.sleep(1)
    print("World")
    return "完成"

# 不能直接调用协程函数,必须通过事件循环执行
# say_hello()  # 返回一个协程对象,不会执行

async def main():
    result = await say_hello()
    print(result)

# 运行事件循环
asyncio.run(main())

8.2 并发执行多个任务

python 复制代码
import asyncio

async def task(name, delay):
    print(f"任务{name} 开始,延迟{delay}秒")
    await asyncio.sleep(delay)
    print(f"任务{name} 完成")
    return f"{name}的结果"

async def main():
    # 创建多个协程任务
    tasks = [
        asyncio.create_task(task("A", 2)),
        asyncio.create_task(task("B", 1)),
        asyncio.create_task(task("C", 3))
    ]
    # 等待所有任务完成
    results = await asyncio.gather(*tasks)
    print(results)

asyncio.run(main())

输出顺序:任务B最先完成,然后A,最后C,体现了并发执行。

8.3 将阻塞代码转为异步

使用`loop.run_in_executor`将同步阻塞函数放到线程池中执行,避免阻塞事件循环。

python 复制代码
import asyncio
import time

def blocking_io():
    time.sleep(2)  # 模拟阻塞操作
    return "阻塞结果"

async def async_operation():
    loop = asyncio.get_running_loop()
    # 在线程池中执行阻塞函数
    result = await loop.run_in_executor(None, blocking_io)
    print(result)

asyncio.run(async_operation())

8.4 原生协程与生成器协程的关系

实际上,原生协程底层仍然使用了生成器机制,但更严格:原生协程不能使用`yield`,只能使用`await`。我们可以通过`inspect`模块检测:

python 复制代码
import asyncio
import inspect

async def native_coro():
    await asyncio.sleep(0)

def generator_coro():
    yield

print(inspect.isgeneratorfunction(generator_coro))  # True
print(inspect.iscoroutinefunction(native_coro))     # True

九. 实战应用场景

9.1 大数据流式处理

假设有一个10GB的CSV文件,我们想按需处理每一行,并过滤、转换,最后写入另一个文件。生成器链式处理非常优雅:

python 复制代码
import csv

def read_csv(file_path):
    with open(file_path, 'r') as f:
        reader = csv.reader(f)
        for row in reader:
            yield row

def filter_rows(rows, predicate):
    for row in rows:
        if predicate(row):
            yield row

def transform_rows(rows, transformer):
    for row in rows:
        yield transformer(row)

def write_csv(rows, output_path):
    with open(output_path, 'w', newline='') as f:
        writer = csv.writer(f)
        for row in rows:
            writer.writerow(row)

# 使用流水线
rows = read_csv('large_input.csv')
filtered = filter_rows(rows, lambda r: int(r[2]) > 1000)
transformed = transform_rows(filtered, lambda r: [r[0], r[1], str(int(r[2]) * 2)])
write_csv(transformed, 'output.csv')

整个过程是惰性的,内存中始终只保存一行数据。

9.2 无限序列生成

在数学计算或游戏开发中,经常需要无穷序列:

python 复制代码
def primes():
    """无限素数生成器(埃拉托色尼筛法)"""
    yield 2
    yield 3
    primes_so_far = [2, 3]
    num = 5
    while True:
        is_prime = True
        for p in primes_so_far:
            if p * p > num:
                break
            if num % p == 0:
                is_prime = False
                break
        if is_prime:
            primes_so_far.append(num)
            yield num
        num += 2

# 取前20个素数
from itertools import islice
first_20 = list(islice(primes(), 20))
print(first_20)

9.3 协程实现状态机

状态机在游戏AI、网络协议解析中很常见。用协程实现非常直观:

python 复制代码
def state_machine():
    """简单的门状态机:closed -> opened -> closed"""
    state = "closed"
    while True:
        if state == "closed":
            event = yield "门已关闭"
            if event == "open":
                state = "opened"
        elif state == "opened":
            event = yield "门已打开"
            if event == "close":
                state = "closed"

sm = state_machine()
next(sm)  # 启动
print(sm.send("open"))   # 门已打开
print(sm.send("close"))  # 门已关闭

9.4 简化异步回调(将回调包装为协程)

在异步编程中,有些老的库仍然使用回调函数。可以使用`asyncio.Future`将其包装成协程:

python 复制代码
import asyncio

def old_callback_based_api(callback):
    """模拟旧式回调API,1秒后调用callback"""
    import threading
    def worker():
        import time
        time.sleep(1)
        callback("数据结果")
    threading.Thread(target=worker).start()

async def wrapper_api():
    loop = asyncio.get_running_loop()
    future = loop.create_future()
    
    def set_future(result):
        future.set_result(result)
    
    old_callback_based_api(set_future)
    result = await future
    return result

async def main():
    data = await wrapper_api()
    print(data)  # 数据结果

asyncio.run(main())

十. 总结

本文详细讲解了Python生成器与协程的完整知识体系:

  • 生成器基础:包含`yield`的函数,返回生成器对象,支持惰性求值。

  • 迭代器协议:生成器是迭代器的子集。

  • 生成器表达式:简洁的惰性序列创建方式。

  • 双向通信:`send()`, `throw()`, `close()`方法让生成器可以接收外部数据。

  • `yield from`:委托生成器,扁平化嵌套,简化协程代理。

  • 原生协程:`async/await`语法,是当前异步编程的标准。

  • 实战应用:流式处理、无限序列、状态机、轻量级调度器等。

  • 性能对比:内存节省巨大,速度略微下降,适合I/O密集与大数据场景。

生成器和协程是Python高级编程的利器。掌握它们,你就能写出更高效、更优雅的代码。无论是在数据处理、网络爬虫、Web后端还是游戏开发中,生成器和协程都会让你事半功倍。

希望这篇详细的教程能够帮助你彻底理解Python生成器与协程,并在实际项目中灵活运用。

相关推荐
Java后端的Ai之路1 小时前
大模型数据飞轮核心技术一篇讲透:原理、架构、企业级案例与2026最全实践指南
人工智能·python·架构·数据飞轮
测试员周周2 小时前
【AI测试功能3】AI功能测试的三层架构:单元测试 → 集成测试 → E2E测试——AI系统测试金字塔实战指南
开发语言·人工智能·python·功能测试·架构·单元测试·集成测试
lly2024062 小时前
AppML 案例原型
开发语言
jllllyuz2 小时前
MATLAB 回声抵消(AEC)、噪声抑制(NS)、自动增益控制(AGC)完整实现
开发语言·matlab
froginwe112 小时前
Vue.js 计算属性
开发语言
05候补工程师2 小时前
【408 从零到一】线性表逻辑特征、存储结构对比与 C/C++ 动态内存分配避坑指南
c语言·开发语言·数据结构·c++·考研
yongui478342 小时前
MATLAB 使用遗传算法求解微电网优化配置数学模型
开发语言·matlab
郝学胜-神的一滴2 小时前
Python 抽象基类深度解析:从简易模拟到 abc 模块的优雅实践
开发语言·python·pycharm
Python伍六七2 小时前
给予Python开发的【16款高效办公自动化工具合集】,告别低效加班!
开发语言·python·自动化