Python高级技术之:`Python`的`generator`和`yield from`:如何实现复杂的协程通信。

各位观众老爷,大家好!今天咱们聊聊 Python 里的“发电机”和“传送门”—— generatoryield from,看看它们是如何在协程世界里搞事情的,实现那些让人头大的复杂通信。准备好了吗?系好安全带,咱们发车啦!

第一站:什么是 Generator?它为啥这么酷?

首先,我们得明白什么是 generator。 简单来说,generator 就像一个“迭代器工厂”,它不是一次性把所有结果都算出来丢给你,而是“按需生产”。 你要一个,它给你一个;你要两个,它给你两个。 这种“懒加载”的方式,在处理大数据或者无限序列时,简直不要太爽!

1.1 Generator 的两种创建方式

  • Generator 函数: 这是最常见的方式,函数里只要出现 yield 关键字,它就自动升级为 generator 函数。

    def my_generator(n):
        i = 0
        while i < n:
            yield i
            i += 1
    
    # 创建一个 generator 对象
    gen = my_generator(5)
    
    # 迭代 generator
    for num in gen:
        print(num)  # 输出 0 1 2 3 4

    在这个例子里,my_generator(5) 并不会立即执行 while 循环,而是返回一个 generator 对象。 只有在 for 循环里,每次迭代时,才会执行到 yield 语句,返回一个值,并暂停执行。 下次迭代时,从上次暂停的地方继续执行。

  • Generator 表达式: 这是一种更简洁的方式,类似于列表推导式,但是用圆括号 () 包裹。

    gen = (x * x for x in range(5))
    
    for num in gen:
        print(num)  # 输出 0 1 4 9 16

    generator 表达式也遵循“懒加载”原则,只有在迭代时才会计算值。

1.2 Generator 的特性

  • 可迭代性: generator 是可迭代的,可以用 for 循环遍历,也可以用 next() 函数获取下一个值。
  • 状态保持: generator 会记住上次执行的状态,下次迭代时从上次暂停的地方继续执行。
  • 节省内存: generator 不是一次性生成所有结果,而是按需生成,所以可以节省大量内存。
  • 单向性: 默认情况下, generator 只能向前迭代,不能后退。但是可以通过一些骚操作来实现双向通信,后面我们会讲到。

第二站:Yield 的魔力:暂停与恢复

yieldgenerator 的核心。 它的作用是:

  • 返回值: yield 会返回一个值给调用者。
  • 暂停: yield 会暂停 generator 函数的执行。
  • 恢复: 下次迭代时,generator 函数会从 yield 语句的下一行继续执行。

更重要的是,yield 不仅仅能返回值,还能接收值! 这就为协程通信打下了基础。

2.1 yield 的发送和接收

def echo_generator():
    value = yield
    print("Received:", value)

gen = echo_generator()
next(gen)  # 启动 generator,执行到 yield 语句暂停

gen.send("Hello, world!")  # 发送 "Hello, world!" 给 yield
# 输出:Received: Hello, world!

在这个例子中,next(gen) 用于启动 generator,让它执行到 yield 语句暂停。 然后,gen.send("Hello, world!") 将字符串 "Hello, world!" 发送给 yield 表达式,并赋值给变量 value。 最后,generator 函数继续执行,打印接收到的值。

2.2 Generator 的 close() 和 throw() 方法

除了 send() 方法,generator 还有 close()throw() 两个重要的方法。

  • close(): 用于关闭 generator。 关闭后,再次迭代会抛出 StopIteration 异常。
  • throw(): 用于在 generator 内部抛出一个异常。
def error_generator():
    try:
        yield
    except ValueError as e:
        print("Caught ValueError:", e)
    finally:
        print("Cleaning up...")

gen = error_generator()
next(gen)  # 启动 generator

gen.throw(ValueError("Something went wrong!"))
# 输出:Caught ValueError: Something went wrong!
#      Cleaning up...

gen.close() # 关闭 generator

在这个例子中,gen.throw(ValueError("Something went wrong!")) 会在 generator 内部抛出一个 ValueError 异常。 try...except 块捕获了这个异常,并打印了错误信息。 最后,finally 块中的代码会被执行,进行清理工作。

第三站:Yield From:协程界的传送门

yield from 是 Python 3.3 引入的一个语法糖,它可以将一个 generator(或者任何可迭代对象)“委托”给另一个 generator。 简单来说,yield from 就像一个传送门,把控制权和数据流从一个 generator 传递到另一个 generator

3.1 Yield From 的基本用法

def sub_generator(n):
    for i in range(n):
        yield i

def main_generator(n):
    yield from sub_generator(n)

gen = main_generator(3)

for num in gen:
    print(num)  # 输出 0 1 2

在这个例子中,main_generator 使用 yield from sub_generator(n) 将迭代过程委托给 sub_generator。 相当于把 sub_generator 的代码直接“嵌入”到 main_generator 中。

3.2 Yield From 的高级用法:双向通信

yield from 最强大的地方在于它可以实现双向通信。 主调方可以通过 send() 方法向子 generator 发送数据,子 generator 也可以通过 yield 向主调方返回数据。

def sub_generator():
    while True:
        try:
            value = yield
            print("Sub-generator received:", value)
        except Exception as e:
            print("Sub-generator caught exception:", e)
            break  # 退出循环
        else:
            if value is None: # 如果接收到None,则退出循环
              break

    return "Sub-generator finished"

def main_generator():
    result = yield from sub_generator()
    print("Main generator received:", result)

gen = main_generator()
next(gen)  # 启动 main_generator 和 sub_generator

gen.send("Hello from main")
gen.send("Another message")
gen.send(None) #告诉sub_generator退出循环
#gen.throw(Exception("Something went wrong")) # 抛出异常

try:
    next(gen) # 尝试继续执行,但是会抛出StopIteration异常
except StopIteration:
    print("Main generator finished")

# 输出:
# Sub-generator received: Hello from main
# Sub-generator received: Another message
# Sub-generator received: None
# Main generator received: Sub-generator finished
# Main generator finished

这个例子比较复杂,我们来一步步分析:

  1. main_generator 使用 yield from sub_generator() 将控制权交给 sub_generator
  2. sub_generator 进入一个无限循环,等待接收数据。
  3. gen.send("Hello from main") 将字符串 "Hello from main" 发送给 sub_generator
  4. sub_generator 接收到数据,并打印出来。
  5. gen.send(None) 发送 Nonesub_generator, sub_generator 接收到 None 后退出循环,并返回 "Sub-generator finished"。
  6. yield from 表达式接收到 sub_generator 的返回值,并赋值给 result
  7. main_generator 打印接收到的返回值。
  8. main_generator 执行完毕,抛出 StopIteration 异常。

3.3 Yield From 的应用场景

  • 简化代码: yield from 可以简化复杂的 generator 嵌套调用,使代码更易读、易维护。
  • 协程通信: yield from 可以实现协程之间的双向通信,构建复杂的并发程序。
  • 异步编程: yield from 可以与 async/await 结合使用,实现异步编程。

第四站:实战演练:构建一个简单的 Pipeline

为了更好地理解 generatoryield from 的用法,我们来构建一个简单的 Pipeline。 Pipeline 是一种常见的设计模式,用于将数据处理流程分解为多个阶段,每个阶段由一个独立的组件负责。

def source(items):
    """数据源,产生数据"""
    for item in items:
        print(f"Source: Producing {item}")
        yield item

def filter_stage(items, condition):
    """过滤器,过滤掉不满足条件的数据"""
    for item in items:
        if condition(item):
            print(f"Filter: Passing {item}")
            yield item
        else:
            print(f"Filter: Filtering out {item}")

def transform_stage(items, func):
    """转换器,对数据进行转换"""
    for item in items:
        transformed_item = func(item)
        print(f"Transform: Transforming {item} to {transformed_item}")
        yield transformed_item

def sink(items):
    """数据汇,消费数据"""
    for item in items:
        print(f"Sink: Consuming {item}")

# 数据
data = [1, 2, 3, 4, 5, 6]

# 条件:只保留偶数
condition = lambda x: x % 2 == 0

# 转换函数:将数字转换为字符串
transform = lambda x: str(x)

# 构建 Pipeline
pipeline = sink(transform_stage(filter_stage(source(data), condition), transform))
# 执行 Pipeline (实际上啥都没做,因为pipeline是个生成器,需要迭代)
#for _ in pipeline:
#    pass

# 使用 yield from 简化代码
def pipeline(items, condition, transform):
    yield from transform_stage(filter_stage(source(items), condition), transform)

# 执行 Pipeline
for item in pipeline(data, condition, transform):
    sink([item]) # sink也需要迭代

#输出结果
#Source: Producing 1
#Filter: Filtering out 1
#Source: Producing 2
#Filter: Passing 2
#Transform: Transforming 2 to 2
#Sink: Consuming 2
#Source: Producing 3
#Filter: Filtering out 3
#Source: Producing 4
#Filter: Passing 4
#Transform: Transforming 4 to 4
#Sink: Consuming 4
#Source: Producing 5
#Filter: Filtering out 5
#Source: Producing 6
#Filter: Passing 6
#Transform: Transforming 6 to 6
#Sink: Consuming 6

在这个例子中,我们定义了四个阶段:

  • source:数据源,产生数据。
  • filter_stage:过滤器,过滤掉不满足条件的数据。
  • transform_stage:转换器,对数据进行转换。
  • sink:数据汇,消费数据。

我们使用 generator 来实现每个阶段,并使用 yield from 将它们连接起来,构建成一个 Pipeline。 这样,我们就可以清晰地看到数据的流向,以及每个阶段的处理逻辑。

第五站:Generator 和协程:异步编程的基石

generator 是协程的基础。 在 Python 3.5 引入 async/await 语法之前,generator 曾经是实现协程的主要方式。 虽然现在 async/await 更加流行,但是理解 generator 的原理,仍然有助于我们更好地理解协程的本质。

5.1 基于 Generator 的协程

def coroutine():
    print("Coroutine started")
    value = yield
    print("Coroutine received:", value)
    return "Coroutine finished"

def main():
    co = coroutine()
    next(co)  # 启动协程
    result = co.send("Hello, world!")  # 发送数据给协程
    print("Main received:", result)

#main() # 抛出StopIteration,因为coroutine函数已经执行完毕

#输出
#Coroutine started
#Coroutine received: Hello, world!
#Main received: Coroutine finished

在这个例子中,coroutine 函数就是一个协程。 我们使用 next(co) 启动协程,然后使用 co.send("Hello, world!") 发送数据给协程。 协程接收到数据后,打印出来,并返回 "Coroutine finished"。

5.2 Generator 和 Async/Await 的关系

async/await 实际上是 generator 的语法糖。 async 关键字将一个函数声明为协程,await 关键字用于等待一个协程执行完成。

import asyncio

async def coroutine():
    print("Coroutine started")
    await asyncio.sleep(1)  # 模拟 IO 操作
    print("Coroutine finished")
    return "Coroutine result"

async def main():
    print("Main started")
    result = await coroutine()  # 等待协程执行完成
    print("Main received:", result)
    print("Main finished")

#asyncio.run(main()) # 需要在事件循环中运行
#输出
#Main started
#Coroutine started
#(等待 1 秒)
#Coroutine finished
#Main received: Coroutine result
#Main finished

在这个例子中,async def coroutine() 定义了一个协程函数。 await asyncio.sleep(1) 用于等待 1 秒钟,模拟 IO 操作。 await coroutine() 用于等待 coroutine 协程执行完成,并获取其返回值。

总结:

今天我们一起学习了 Python 的 generatoryield from。 它们是构建协程、实现复杂通信的利器。 希望通过今天的讲座,大家能够更好地理解它们的原理和用法,并在实际开发中灵活运用。

特性 Generator Yield From
作用 创建迭代器,按需生成数据 将一个 generator 委托给另一个 generator,实现双向通信
核心关键字 yield yield from
应用场景 处理大数据、无限序列、状态保持、协程 简化代码、协程通信、异步编程
优点 节省内存、可迭代、状态保持 简化代码、提高效率、实现复杂的协程通信
缺点 默认单向迭代,需要手动管理状态和异常 稍微复杂,需要理解其背后的原理

好了,今天的讲座就到这里。 感谢大家的收听! 记住,编程的路上,永远不要停止学习和探索! 咱们下回再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注