python 使用yield from

深入理解Python的yieldfrom
本文详细解析Python中yieldfrom语法的功能与应用,包括其如何处理StopIteration异常,建立调用方与子生成器间的双向通道,以及在生成器间的委托职责。通过具体示例,阐述yieldfrom如何简化生成器间的通信。

yield from 结构会在内部自动捕获 StopIteration 异常。这种处理方式与 for 循环处理 StopIteration 异常的方式一样:循环机制使用用户易于理解的方式处理异 常。对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把 value 属性 的值变成 yield from 表达式的值。

首先要知道,yield from 是全新的语言结构。它的作用比 yield 多很多,因此人们认为继 续使用那个关键字多少会引起误解。在其他语言中,类似的结构使用 await 关键字,这个 名称好多了,因为它传达了至关重要的一点:在生成器 gen 中使用 yield from subgen() 时,subgen 会获得控制权,把产出的值传给gen 的调用方,即调用方可以直接控制 subgen。与此同时,gen 会阻塞,等待 subgen 终止。

下面这俩函数是一样的。
在这里插入图片描述
yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因此,x 可以是任何可迭代的对象。

可是,如果 yield from 结构唯一的作用是替代产出值的嵌套 for 循环,这个结构很有可 能不会添加到 Python 语言中。yield from 结构的本质作用无法通过简单的可迭代对象说 明,而要发散思维,使用嵌套的生成器。

yield from 的主要功能是打开双向通道,把最外层的调用方最内层的子生成器连接起来, 这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间协程中添加 大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。

若想使用 yield from 结构,就要大幅改动代码。为了说明需要改动的部分,PEP 380 使用 了一些专门的术语。

委派生成器
包含 yield from <iterable> 表达式的生成器函数。

子生成器
yield from 表达式中<iterable> 部分获取的生成器。这就是PEP 380 的标题 (“ Syntax for Delegating to a Subgenerator”)中所说的“子生成器”(subgenerator)

调用方
PEP 380 使用“调用方”这个术语指代调用委派生成器的客户端代码。在不同的语境 中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,因为它调 用了子生成器)区分开。

PEP 380 经常使用“迭代器”这个词指代子生成器。这样会让人误解,因为委 派生成器也是迭代器。因此,我选择使用“子生成器”这个术语,与 PEP 380 的标题(“Syntax for Delegating to a Subgenerator”)保持一致。然而,子生成器可 能是简单的迭代器,只实现了 __next__ 方法;但是,yield from 也能处理这 种子生成器。不过,引入 yield from 结构的目的是为了支持实现了 __next__sendclosethrow 方法的生成器。

图 16-2 把该示例中各个相关的部分标识 出来了。
在这里插入图片描述
下面来看例子:

"""
yield from 计算平均值并输出统计报告
"""
import pysnooper
from collections import namedtuple


Result = namedtuple('Result', 'count average')


# 子生成器
# @pysnooper.snoop()
def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        # 至关重要的终止条件。如果不这么做,
        # 使用 yield from 调用这个协程的生成器会永远 阻塞。
        if term is None:
            break
        total += term
        count += 1
        average = total / count

    # 返回的 Result 会成为 grouper 函数中 yield from 表达式的值。
    return Result(count, average)


# 委派生成器
# @pysnooper.snoop()
def grouper(results, key):
    # 这个循环每次迭代时会新建一个 averager 实例;
    # 每个实例都是作为协程使用的生成器 对象。
    while True:
        # grouper 发送的每个值都会经由yield from 处理,
        # 通过管道传给averager 实例。
        # grouper 会在yield from 表达式处暂停,
        # 等待averager 实例处理客户端发来的值。
        # averager 实例运行完毕后,返回的值绑定到 results[key] 上。
        # while 循环会不断创建 averager 实例,处理更多的值
        results[key] = yield from averager()


# 客户端代码,即调用方
def main(data):
    results = {}
    for key, values in data.items():
        #  group 是调用grouper 函数得到的生成器对象,
        #  传给grouper 函数的第一个参数是 results,用于收集结果;
        #  第二个参数是某个键。group 作为协程使用
        group = grouper(results, key)
        next(group)
        for value in values:
            # 把各个 value 传给 grouper。
            # 传入的值最终到达 averager 函数中 term = yield 那一行;
            # grouper 永远不知道传入的值是什么。
            group.send(value)

        # 把 None 传入 grouper,
        # 导致当前的 averager 实例终止,
        # 也让 grouper 继续运行,再创 建一个 averager 实例,处理下一组值。
        group.send(None)  # 重要
    # print(results)
    report(results)


def report(results):
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit))


data = {
    'girls;kg':
        [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
    'girls;m':
        [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
    'boys;kg':
        [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
    'boys;m':
        [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}


if __name__ == '__main__':
    main(data)



运行结果:
在这里插入图片描述
最后一个标号前面有个注释——“重要!”,强调这行代码(group. send(None))至关重要:终止当前的 averager 实例,开始执行下一个。如果注释掉那一 行,这个脚本不会输出任何报告。此时,把 main 函数靠近末尾的 print(results) 那行的 注释去掉,你会发现,results 字典是空的。

下面简要说明示例 的运作方式,还会说明把 main 函数中调用 group.send(None) 那一 行代码(带有“重要!”注释的那一行)去掉会发生什么事。
1、外层for循环每次迭代会新建一个grouper实例,赋值给group变量;group是委派生成器。
2、调用 next(group),预激委派生成器 grouper,此时进入 while True 循环,调用子生成 器 averager 后,在 yield from 表达式处暂停。
3、内层 for 循环调用 group.send(value),直接把值传给子生成器 averager。同时,当前 的 grouper 实例(group)yield from 表达式处暂停。
4、内层循环结束后,group 实例依旧在 yield from 表达式处暂停,因此,grouper 函数定 义体中为 results[key] 赋值的语句还没有执行。
5、如果外层 for 循环的末尾没有 group.send(None),那么 averager 子生成器永远不会终止, 委派生成器 group 永远不会再次激活,因此永远不会为 results[key] 赋值。
6、外层 for 循环重新迭代时会新建一个 grouper 实例,然后绑定到 group 变量上。前一个 grouper 实例(以及它创建的尚未终止的 averager 子生成器实例)被垃圾回收程序回收。

这个试验想表明的关键一点是,如果子生成器不终止,委派生成器会在 yield from 表达式处永远暂停。如果是这样,程序不会向前执行,因为 yield from (与 yield 一样)把控制权转交给客户代码(即,委派生成器的调用方)了。 显然,肯定有任务无法完成。

示例 展示了 yield from 结构最简单的用法,只有一个委派生成器和一个子生成器。 因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派 生成器使用 yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器,以此类推。最终,这个链条要以一个只使用 yield 表达 式的简单生成器结束;不过,也能以任何可迭代的对象结束。

任何yield from 链条都必须由客户驱动,在最外层委派生成器上调用next(...) 函数 或 .send(...) 方法。可以隐式调用,例如使用 for 循环。

<think>我们在回答用户关于yield from使用方法和原理时,需要结合站内引用内容,并按照系统级指令进行回答。 引用[1]提到:yield from可以将生成器的部分控制权委托给另一个生成器,使得内部生成器的值直接传给外部调用者,并处理StopIteration异常。 引用[2]提到:在嵌套较多、需要传递参数和捕获异常时,yield from简化了代码。 引用[3]提到:yield from是协程实现的基础,将从生成器、yield说起,逐步揭开它的面纱。 引用[4]是关于yield的例子(读取大文件),但问题聚焦于yield from,所以可能作为背景参考。 引用[5]展示了生成器的底层机制(字节码、栈帧等),可用于解释原理。 回答结构: 1. 解释yield from是什么,它的基本用途。 2. 使用一个简单示例说明yield from使用方法。 3. 对比使用yieldyield from在嵌套生成器中的区别,展示yield from如何简化代码。 4. 深入原理:包括异常处理、值传递、以及与协程的关系。 5. 总结yield from的优势。 注意:所有行内表达式用$...$,独立公式用$$...$$,但本问题主要是代码示例,所以用代码块。 同时,生成相关问题。 根据引用内容,我们还可以讨论yield from在协程中的应用(引用[3]提到),但用户问题主要关注使用方法和原理,所以先集中在生成器部分。 </think>### Python 3中`yield from`的使用方法与原理 #### 一、基本概念与作用 `yield from`是Python 3.3+引入的语法,主要用于**简化生成器的嵌套调用**。它的核心功能是将生成器的控制权委托给另一个子生成器(或可迭代对象),实现值的透明传递和异常处理[^1]。 - **核心作用**: 1. 将外部生成器的`yield`操作委托给内部生成器 2. 自动处理`StopIteration`异常 3. 支持双向数据传递(值传入/传出) 4. 简化递归生成器的实现 #### 二、基础使用方法 ```python def sub_generator(): yield 1 yield 2 return "Done" def main_generator(): result = yield from sub_generator() # 委托控制权 yield f"Sub-generator returned: {result}" gen = main_generator() print(next(gen)) # 输出: 1 print(next(gen)) # 输出: 2 print(next(gen)) # 输出: "Sub-generator returned: Done" ``` - `yield from sub_generator()`会: 1. 将`main_generator`的`yield`操作完全交给`sub_generator` 2. 自动捕获`sub_generator`的`return`值(通过`StopIteration.value`) 3. 将返回值赋给`result` #### 三、对比传统嵌套生成器 **传统写法(无`yield from`)**: ```python def legacy_generator(): sub = sub_generator() try: while True: x = next(sub) yield x except StopIteration as e: result = e.value yield result ``` **缺陷**: - 需要手动迭代子生成器 - 显式处理`StopIteration`异常 - 代码冗余且易出错(如忘记异常处理)[^2] **`yield from`的优势**: - 代码量减少约50% - 避免手动异常处理 - 支持双向通信(`.send()`和`.throw()`直接传递到子生成器) #### 四、双向通信机制 `yield from`支持通过`.send()`向子生成器传递数据: ```python def child(): received = yield "Ready" yield f"Received: {received}" def parent(): output = yield from child() yield output p = parent() print(next(p)) # 输出: "Ready" print(p.send("Data")) # 输出: "Received: Data" ``` - `.send("Data")`直接将值注入`child()`的`received`变量 - 外部调用者与子生成器直接交互,无需中间层[^3] #### 五、底层原理 1. **异常传递**: - 子生成器的`StopIteration`被自动捕获,其`.value`作为`yield from`表达式结果 - 其他异常直接冒泡到外部生成器 2. **栈帧管理**: - 子生成器执行时,外部生成器的栈帧(`f_locals`)暂停更新 - 通过`gi_frame.f_lasti`可追踪字节码执行位置(见引用[5]) 3. **字节码优化**: - `yield from`会被编译为`GET_YIELD_FROM_ITER`和`YIELD_FROM`指令 - 比手动嵌套循环的字节码更高效 #### 六、典型应用场景 1. **递归生成器**(如树遍历) 2. **协程委托**(`asyncio`协程基础) 3. **大文件分块处理**(替代手动缓冲迭代,引用[4]思想的扩展) 4. **管道式数据处理**(多个生成器链式调用) > 总结:`yield from`通过语法糖形式实现了**生成器委托协议**,不仅简化了代码结构,还提供了完整的异常传播和双向通信机制,是Python协程体系的核心基础之一[^3]。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值