拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

《Python进阶》协程详解:用 send() 让 yield 从“产出数据“变为“消费数据“

《Python进阶》协程详解:用 send() 让 yield 从“产出数据“变为“消费数据“
  • 教程
  • 文档
  • 教育

【免费下载链接】interpy-zh

📘《Python进阶》(Intermediate Python - Chinese Version)

项目地址:https://gitcode.com/gh_mirrors/in/interpy-zh
点击查看免费下载

本文是《Python进阶》(interpy-zh 中文译本)"数据结构"章节中协程主题的深度指南。协程与生成器共享yield语法,却扮演着截然相反的角色:生成器是数据的生产者,协程则是数据的消费者。读完本文,你将掌握用next()启动协程、用send()向协程喂数据、用close()关闭协程的完整流程,并能读懂仓库中 code/2.7/23_coroutine.py 的实战实现,为后续学习异步编程打下基础。

协程与生成器:同源而殊途

在 Python 中,协程(Coroutine)和生成器(Generator)长得非常像——它们都使用yield关键字、都以函数形式编写,但两者的定位有本质区别:

  • 生成器是数据的生产者:它通过yield源源不断地"吐出"值,供外部消费;
  • 协程是数据的消费者:它通过yield接收外部"喂入"的值,再对这些值进行加工处理。

一句话概括:同一个yield,向外产出值时它是生成器,向内接收值时它就变成了协程。

第一步:回顾生成器的创建与使用

要理解协程,先要熟悉生成器。在 book/DataStructures/generators.md 中已经系统讲解了迭代器(Iterator)、可迭代对象(Iterable)与迭代(Iteration)三者的关系。这里回顾最经典的斐波那契生成器写法:

def fib(): a, b = 0, 1 while True: yield a a, b = b, a + b

配合for循环使用:

for i in fib(): print(i)

这样做有两个好处:

  1. 快:值在迭代过程中实时产出,不需要先构建完整序列;
  2. 省内存:斐波那契数列的每一项都是动态生成的,而不是被一次性存储到一个列表中——这正是 book/DataStructures/generators.md 强调的"生成器最佳应用场景:你不想同一时间将所有计算出来的大量结果集分配到内存当中"。

第二步:把yield变成接收通道——grep 协程

如果我们在生成器中把yield放在表达式位置(如line = (yield)),并去掉yield后面的产出值,函数就不再向外部产出数据,而是等待外部向它发送数据——它变成了一只协程。原文档给出的 Python 版grep就是最直观的例子:

def grep(pattern): print("Searching for", pattern) while True: line = (yield) if pattern in line: print(line)

对比斐波那契生成器,这里有三个关键变化:

对比维度生成器 fib协程 grep
yield的形态yield a(带产出值)(yield)(裸 yield,作为表达式)
数据流向函数体向外产出值外部向函数体注入值
角色数据生产者数据消费者

line = (yield)这行代码是整个协程的"接收口":外部发送进来的值会被yield表达式"接住"并赋值给line,随后if pattern in line会对接收到的每一行文本做模式匹配,命中则打印。整个while True循环让协程可以无限次地接收数据、处理数据,而不是处理完一次就结束。

第三步:用send()向协程喂数据

协程不再自带初始值,而是"从外部传值给它",这个传值动作由生成器协议的send()方法完成。原文档给出了完整的调用序列:

search = grep('coroutine') next(search) # output: Searching for coroutine search.send("I love you") search.send("Don't you love me?") search.send("I love coroutine instead!") # output: I love coroutine instead!

逐行拆解这段代码的执行过程:

  1. search = grep('coroutine'):调用协程函数不会立即执行函数体,只是创建一个协程对象;
  2. next(search):启动协程,函数体开始执行,先打印Searching for coroutine,随后执行到line = (yield)挂起等待;
  3. search.send("I love you"):将"I love you"发送进去,赋值给line,模式'coroutine'不在其中,不打印,循环再次挂起在yield;
  4. search.send("Don't you love me?"):同上,'coroutine'不在其中,不打印;
  5. search.send("I love coroutine instead!"):'coroutine'命中,打印I love coroutine instead!。

注意send()的语义:发送的值会被yield接收,同时send()本身也会推进协程继续执行到下一个yield挂起点——这正是它和"纯传值"不同的地方。仓库中 code/2.7/23_coroutine.py 完整复刻了这段逻辑(Python 2.7 版本,使用print语句),读者可以直接执行验证:

python code/2.7/23_coroutine.py

该文件把调用过程封装进了main(),运行结果与上文输出完全一致。

第四步:为什么必须先调用next()?

原文档特别强调了一个初学者最容易困惑的问题:"发送的值会被yield接收。我们为什么要运行next()方法呢?这样做正是为了启动一个协程。"

原因在于:协程函数体内的代码并不会在创建对象时立刻执行。就像生成器一样,代码是惰性的——必须通过next()推进到第一个yield表达式处,协程才真正"就位",之后send()发送的值才有接收者。换句话说:

  • next(search)让协程执行到yield处并挂起;
  • 此后每次send(value)都会唤醒协程,让yield表达式的值变为value,继续执行到下一个yield再次挂起。

原文档的结论是:必须通过next()方法来执行yield表达式,协程才能响应后续的send()调用。在 Python 3 中,send(None)与next()等价,也可以用来启动协程——next()本质就是send(None)的语法糖(这是生成器协议的标准行为,可在 Python 官方文档中确认)。

第五步:用close()关闭协程

当协程不再需要时,可以调用close()方法关闭它。关闭后协程将释放自身,再次对它send()会抛出StopIteration异常。原文档示例:

search = grep('coroutine') search.close()

close()的作用原理是:在协程挂起的yield处抛出GeneratorExit异常,如果协程没有捕获它(本例中while True循环会因此终止),协程便正式结束。这保证了一个"永远while True"的协程能够被外部主动终止,而不会泄漏资源。

结合仓库源码:协程与生成器的完整知识地图

在《Python进阶》仓库中,协程并不是孤立的知识点,它与多个章节互为表里:

  • book/DataStructures/generators.md:协程的上游知识。这里详细讲解了可迭代对象、迭代器、迭代三者的区别,以及next()、iter()的用法和StopIteration异常的含义。理解"生成器只能迭代一次""for循环会自动捕获StopIteration"这些特性,是理解协程挂起/唤醒机制的前提;
  • book/DataStructures/README.md:协程所属的"数据结构"章节入口,本主题在该章节中排在生成器之后(nav_order: 2),正是"先生成器、后协程"的学习脉络;
  • book/Syntax/context_managers.md:协程的应用延伸。该章节展示了用@contextmanager装饰器配合生成器实现上下文管理器:函数体内yield f之前的代码在进入with块时执行,yield之后的代码在退出时执行——这正是把"函数暂停/恢复"能力(与协程同源的机制)用于资源管理的经典范式;
  • code/2.7/23_coroutine.py:协程的可执行样例。仓库 code/README.md 说明,code/2.7/目录下的代码为 Python 2.7 专用版本,而书文中夹带的代码则保证兼容 Python 2/3,读者可按需取舍。

从协程走向更广阔的异步世界

本主题只是协程的入门。原文档最后推荐了 David Beazley 关于协程的著名演讲(A Curious Course on Coroutines and Concurrency),其中涵盖协程的分发(dispatch)、流水线(pipeline)、扇出(fan-out)等高级模式。若想继续深入,可以沿着两条路线:

  1. 阅读本仓库相邻章节:先巩固 book/DataStructures/generators.md 的生成器协议,再通过 book/Syntax/context_managers.md 观察yield挂起机制在实际工程中的应用;
  2. 对比 Python 3 的现代写法:理解"消费者协程 + 显式推进"这一经典模型后,再去看yield from和async/await就会轻松许多——它们解决的是同一类问题,只是语法和调度方式更高级。

小结

本文完整还原了《Python进阶》协程章节的讲解脉络,并补充了仓库源码层面的印证:

  • 生成器用yield产出值,协程用(yield)接收值;
  • next()负责启动协程并推进到第一个挂起点,是send()生效的前提;
  • send(value)既向协程注入数据,又推进协程执行到下一个yield;
  • close()用于主动终止一个无限循环的协程;
  • 实战代码见 code/2.7/23_coroutine.py,相关理论见 book/DataStructures/generators.md。

掌握"启动—喂值—挂起—再喂值—关闭"这一完整生命周期,你就真正理解了 Python 协程的核心机制,也拿到了进入异步编程世界的第一把钥匙。

  • 教程
  • 文档
  • 教育

【免费下载链接】interpy-zh

📘《Python进阶》(Intermediate Python - Chinese Version)

项目地址:https://gitcode.com/gh_mirrors/in/interpy-zh
点击查看免费下载

相关推荐

上一篇:Texture-vs-Shape模型训练秘籍:超参数设置与Stylized-ImageNet训练技巧终极指南
下一篇:StatsD 对接 Graphite 配置完全指南:Storage Schemas 与 Storage Aggregation 实战详解

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表