Python异步编程中如何使用yield from实现协程?

异步编程之使用 yield from

yield from 是 Python3.3 后新加的语言结构。yield from 的主要功能是打开双向通道,把最外层的调用方法与最内层的子生成器连接起来。这两者就可以进行发送值和返回值了,yeild from 结构的本质是简化嵌套的生产器,不理解这个是什么意思的话,下面我将用几个例子来对其使用方法进行讲解。

简化 for 循环中的 yeild

首先看一个

def gene():
    for c in 'AB':
        yield c  #遇到 yeild 程序返回循环,下次从 yeild 后面开始。
    for i in range(3):
        yield i 
if __name__=="__main__":
    list(gene())#list 内部会预激生成器

输出

['A','B','0','1', '2']

上面的代码可以简写成

def gene():
     yield from 'ab' 
     yield from range(3)
if __name__=="__main__":
    list(gene()) 

通过上面的代码我们可以知道,yield from 可以简化 for 循环里的 yield 表达式。当然 yeild from 的功能不仅仅是可以简化 for 循环而已,要是这样的话也就不值得,单独写一篇文章来介绍了。

我们仔细观察,简化后的式子有两个 yeild from,同样的也就是说如果有 10 个 for 循环的 yeild 生成式,我们需要写 10 个 yeild from,此时我们要记得在 python 中如果重复的代码出现了两次以及以上就该考虑优化了。好了接下来我们看一个优化后的例子。

通过 yield from 链接可迭代对象

def chain(*args):
    for i in args:
        # for m in i:
        #  yield m
        yield from i
p = list(chain("1234", "AB", [1, 2, 3, 4, 5]))
print(p)

输出

['1', '2', '3', '4', 'A', 'B', 1, 2, 3, 4, 5]

这里对之前的例子做了个优化处理,通过*args 可变参数,配合后面的 for 循环进行了多个可迭代对象的链接处理。下面来看一个复杂点的例子:(来自 Python cookbook 3,github 源码地址 https://github.com/dabeaz/python-cookbook/blob/master/src/4/how_to_flatten_a_nested_sequence/example.py)

扁平化处理嵌套型的数据

# Example of flattening a nested sequence using subgenerators

from collections import Iterable

def flatten(items, ignore_types=(str, bytes)): for x in items: if isinstance(x, Iterable) and not isinstance(x, ignore_types): yield from flatten(x) else: yield x

items = [1, 2, [3, 4, [5, 6], 7], 8]

Produces 1 2 3 4 5 6 7 8

for x in flatten(items): print(x)

items = [‘Dave’, ‘Paula’, [‘Thomas’, ‘Lewis’]] for x in flatten(items): print(x)

接下来通过说一下开篇提到的子生产器和调用方以及新的词委托生成器。

了解几个概念

yield from x 表达式对 x 对象做的第一件事是,调用 iter(x),从中获取一个迭代器。所以 x 是可迭代对象。上面的例子中的 x 如果是可迭代对象就会执行,yield from flatten(x).

PEP380 的标题是 ” syntax for delegating to subgenerator “(把指责委托给子生成.器的句法)。由此我们可以知道,yield from 是可以实现嵌套生成器的使用。

yield from 在看接下来的代码之前我们必须知道这几个概念:

委派生成器

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

子生成器

从 yield from <iterable> 部分获取的生成器,含义 yield 的。</iterable>

调用方

调用委派生成器的客户端(调用方)代码,也就是运行入口。

ok,了解了这些我们看接下来的一个例子。

使用 yeild from 写一个异步爬虫

import requests
from collections import namedtuple  ①

Response = namedtuple(“rs”, ‘url status’) ②

#子生产器 def fecth(): ③ res=[] while 1: url = yield ④ if url is None: ⑤ break req = requests.get(url) res.append(Response(url=url, status=req.status_code)) return res

#委派生成器 def url_list(l, key): while 1: ⑥ l[key] = yield from fecth() ⑦

#调用方 def main(): l = {} u = [“http://www.baidu.com”, “http://www.cnblogs.com”] for index, url in enumerate(u): if index == 0: ul = url_list(l, index) next(ul) ⑧ ul.send(url)⑨ ul.send(None)⑩ return l

if name == ‘main’: res = main() print(res)

接下来对上面的标准进行解释: ① 引入一个具名元组,可以后面实现一个简单的类。 ② 对请求参数做一个格式化处理,后面通过获取属性即可。 ③一个协程,通过 requests 模块可以发起网络请求。 ④main 函数的发送的值绑定到这里的 url 上 ⑤ url 为 None 即没有 url 的时候结束循环的。 ⑥这个循环每次都会新建一个 fetch 实例,每个实例都是作为协程使用的生成器对象。 ⑦ url_list 发送的每个值都会经由 yield from 处理,然后传给 fetch 实例。url_list 会在 yield from 表达式处暂停,等待 fetch 实例处理客户端发来的值。fetch 实例运行完毕后,返回的值绑定到 l[key] 上。while 循环会不断创建 fetch 实例,处理更多的值。 ⑧激活 url_list 生成器 ⑨把各个 url 以及其序列号 index,传给 url_list 传入的值最终到达 fetch 函数中,url_list 并不知道传入的是什么,同时 url_list 实例在 yield from 处暂停。直到 fetch 的一个实例处理完才进行赋值。 ⑩关键的一步,ul 把 None 传入 url_list,传入的值最终到达 fetch 函数中,导致当前实例终止。然后继续创建下一个实例。如果没有 ul.send(None),那么 fetch 子生成器永远不会终止,因为 ul.send()发送的值实际是在 fetch 实例中进行,委派生成器也永远不会在此激活,也就不会为 l[key]赋值

参考资料:

流畅的 python 第 16 章 PEP 380-- Syntax for Delegating to a Subgenerator How Python 3.3 "yield from" construct works


Python异步编程中如何使用yield from实现协程?

16 回复

好像要淘汰了


在Python异步编程中,yield fromasyncio 库出现前,用于实现协程和任务链的关键语法。它的核心作用是将子生成器的控制流委托给父生成器,让父生成器能直接与子生成器交互,简化了嵌套生成器的代码。

简单来说,yield from generator 会:

  1. 暂停当前协程(父生成器)。
  2. 将执行权交给 generator(子生成器)。
  3. 子生成器产出的值直接通过父生成器产出。
  4. 子生成器返回时,其 return 的值会成为 yield from 表达式的值。

一个经典的生产者-消费者模型示例:

def producer():
    """生产者协程"""
    for i in range(1, 4):
        print(f'[生产者] 生产了 {i}')
        # 产出数据,并等待消费者返回处理结果
        result = yield i
        print(f'[生产者] 收到消费者反馈: {result}')

def consumer():
    """消费者协程"""
    # 启动生产者
    prod = producer()
    # 发送None初始化生成器,获取第一个值
    value = next(prod)
    
    while True:
        print(f'[消费者] 消费了 {value}')
        # 模拟处理过程
        feedback = f'已处理 {value}'
        try:
            # 关键:将控制权委托给生产者,并传递反馈值
            value = prod.send(feedback)
        except StopIteration:
            # 生产者结束
            print('[消费者] 生产者已结束')
            break

# 运行消费者
consumer()

输出:

[生产者] 生产了 1
[消费者] 消费了 1
[生产者] 收到消费者反馈: 已处理 1
[生产者] 生产了 2
[消费者] 消费了 2
[生产者] 收到消费者反馈: 已处理 2
[生产者] 生产了 3
[消费者] 消费了 3
[生产者] 收到消费者反馈: 已处理 3
[消费者] 生产者已结束

关键点解释:

  • consumer() 中的 value = prod.send(feedback)feedback 发送给 producer(),并接收其下一个 yield 的值。
  • producer() 中的 result = yield i 在产出 i 后,会等待外部通过 .send() 发送的值,并将其赋值给 result
  • producer() 循环结束(没有更多 yield)时,会抛出 StopIterationconsumer() 捕获后结束循环。

在现代异步编程中的角色: 在 Python 3.4 引入 asyncio 时,正是使用 @asyncio.coroutine 装饰器和 yield from 来定义协程的。例如:

import asyncio

@asyncio.coroutine
def old_style_coroutine():
    yield from asyncio.sleep(1)
    return "Done"

但从 Python 3.5 开始,官方推荐使用更清晰、功能更强大的 async/await 语法来替代 yield from。上面的代码用现代写法是:

async def modern_coroutine():
    await asyncio.sleep(1)
    return "Done"

总结:现在写新项目直接用 async/await,了解 yield from 主要是为了读懂旧代码。

都 8102 年了,还用 yield ? async/ await 了解一下?

可是你这个异步爬虫不分明是同步的吗。。。。。。

说的好,异步呢?

用 requests 写,
不管怎么弄都是同步的.
需要用 aiohttp 才行.

为什么要用过渡方法不用 await ?

就很僵硬……幸好没公众号

兄弟你是不是忘发公众号了

啥僵硬

没错已经淘汰了 但是我想在理解 asyncio 之前 从 yield from 的思想学起比较好 学的是一种思路

嗯嗯 这个例子主要是直到 yield from 的工作原理,后期会用 asyncio+aiohttp 等 异步例子,还在学习中。

楼主请稍认真点,目测第一个例子的输出 [‘A’,‘B’,‘0’,‘1’, ‘2’] 是错的。

#12 hhhhhhhhh 明明根本连输出都不会有

看见贴长文的就不知道怎么说 感觉很难受

chain 那个函数能一行搞定,还没怎么这样写过呢

回到顶部