理解Python的协程机制-Yield

简介:

根据PEP-0342 Coroutines via Enhanced Generators,原来仅仅用于生成器的yield关键字被扩展,成为Python协程实现的一部分。而之所以使用协程,主要是出于性能的考虑:一个活跃的Python线程大约占据8MB内存,而一个活跃线程只使用1KB不到内存。对于IO密集型的应用,显然轻量化的协程更适用。

概述

原来,yield是一个statement,即和return一样的语句,但是在PEP-0342后,yield statement被改造为了yield expression。其语法如下:

Yield expressions:

 
  1. yield_atom ::= "(" yield_expression ")"
  2. yield_expression ::= "yield" [expression_list]

yield表达式只能在函数体内使用,会导致该函数变为一个 生成器函数

当生成器函数被调用,它会返回一个 生成器 ,用以控制生成器函数的执行.

生成器的第一次执行,必须使用gen_func.next()方法,函数会执行到第一条yield的位置并返回该处yield语句之后的表达式值。

接下来,可以交替使用gen_func.nextgen_func.send方法对协程的执行。send方法会传入一个参数,该参数即yield表达式的值,可以在生成器函数里面被接收。而next则不传入参数。当send or next被执行时,生成器函数会从yield表达式处继续执行。直到下一次出现yield语句,再次返回yield之后表达式的值。

一个简单的例子:使用协程返回值

最简单的协程使用,通过nextsend控制协程的运行。

 
  1. def coroutine_print():
  2. while True:
  3. message = yield
  4. print message
  5. it = coroutine_print()
  6. next(it)
  7. it.send('Hello')
  8. it.send('World')
  9. # result:
  10. # Hello
  11. # World

稍微复杂一点:向协程发送数据计算平均值

稍微复杂的一个例子,使用yield的返回值。
假设我们希望执行一个流式计算,不断有数据到达,我们希望计算这些数据的平均数。

 
  1. def avg_coroutine():
  2. cnt = 0
  3. sum = 0
  4. new_value = yield
  5. while True:
  6. sum += new_value
  7. cnt += 1
  8. new_value = yield float(sum) / cnt
 
  1. In [6]: it = avg_coroutine()
  2. In [7]: it.next()
  3. In [8]: it.send(10)
  4. Out[8]: 10.0
  5. In [9]: it.send(20)
  6. Out[9]: 15.0
  7. In [10]: it.send(30)
  8. Out[10]: 20.0
  9. In [11]: it.send(110)
  10. Out[11]: 42.5

如本例所示,在使用协程时,首先调用next方法停止在第一条yield处,并使用send方法向协程内发送数据,并获取更新后的平均值。

使用throw方法与协程通信

一旦协程开启,仅仅通过send与yield只能对协程进行简单的控制。throw方法提供了在协程内引发异常的接口。通过主叫者调用throw在协程内引发异常,协程捕获异常的方式,可以实现主叫者与协程之间的通信。

需要注意的是,使用throw方法在协程内引发的异常,如果没有被捕获,或者内部又重新raise了不同的异常,那么这个异常会传播到主叫者。

同时throw方法同sendnext一样,都会使协程继续运行,并返回下一个yield表达式中的表达式值。且同样的,如果不存在下一个yield表达式协程就结束了,主叫方会收到StopIteration Exception

使用close方法来结束协程

close方法与throw方法类似,都是在协程暂停的位置引发一个异常,从而向协程发出控制信息。不同于throw方法可以抛出自定义异常,close方法会固定抛出GeneratorExit异常。当这个异常没有被捕获或者引发StopIteration Exception时,close方法会正常返回。这个比较好理解,协程设计者捕获了GeneratorExit异常并完成清理,保证清理干净了,所以最后不会再有yield返回值引发StopItertation。如果因为设计错误导致仍然有下一个yield,那么就会抛出RuntimeError.

Python文档中给出的应用四个协程API的样例。

 
  1. >>> def echo(value=None):
  2. ... print "Execution starts when 'next()' is called for the first time."
  3. ... try:
  4. ... while True:
  5. ... try:
  6. ... value = (yield value)
  7. ... except Exception, e:
  8. ... value = e
  9. ... finally:
  10. ... print "Don't forget to clean up when 'close()' is called."
  11. ...
  12. >>> generator = echo(1)
  13. >>> print generator.next()
  14. Execution starts when 'next()' is called for the first time.
  15. 1
  16. >>> print generator.next()
  17. None
  18. >>> print generator.send(2)
  19. 2
  20. >>> generator.throw(TypeError, "spam")
  21. TypeError('spam',)
  22. >>> generator.close()
  23. Don't forget to clean up when 'close()' is called.

这和原来的生成器。这时候使用gen_func.send()会报错,因为没有接受
当某个生成器函数被执行时,生成器函数会开始执行,直到第一条yield表达式为止。

这条表达式会有一个返回值,它取决于使用了生成器的哪个方法去恢复生成器函数的控制流。

next方法会恢复gen func的执行,这里yield表达式会返回值None。
而gen func会继续运行到下一条yield语句,并将yield后面的值反回。

send和next类似,不同的是她可以传入一个值作为yield表达式的计算值。如果用send去开启gen func的执行,那么参数必须为None,因为这时候没有yield语句在等待返回值。

Python文档中的描述

PEP 342 — Coroutines via Enhanced Generators
New in version 2.5.
The yield expression is only used when defining a generator function, and can only be used in the body of a function definition. Using a yield expression in a function definition is sufficient to cause that definition to create a generator function instead of a normal function.

When a generator function is called, it returns an iterator known as a generator. That generator then controls the execution of a generator function. The execution starts when one of the generator’s methods is called. At that time, the execution proceeds to the first yield expression, where it is suspended again, returning the value of expression_list to generator’s caller. By suspended we mean that all local state is retained, including the current bindings of local variables, the instruction pointer, and the internal evaluation stack. When the execution is resumed by calling one of the generator’s methods, the function can proceed exactly as if the yield expression was just another external call. The value of the yield expression after resuming depends on the method which resumed the execution.

All of this makes generator functions quite similar to coroutines; they yield multiple times, they have more than one entry point and their execution can be suspended. The only difference is that a generator function cannot control where should the execution continue after it yields; the control is always transferred to the generator’s caller.
5.2.10.1. Generator-iterator methods
This subsection describes the methods of a generator iterator. They can be used to control the execution of a generator function.
Note that calling any of the generator methods below when the generator is already executing raises a ValueError exception.
generator.next()
Starts the execution of a generator function or resumes it at the last executed yield expression. When a generator function is resumed with a next() method, the current yield expression always evaluates to None. The execution then continues to the next yield expression, where the generator is suspended again, and the value of the expression_list is returned to next()‘s caller. If the generator exits without yielding another value, a StopIteration exception is raised.

 
  1. generator.send(value)

Resumes the execution and “sends” a value into the generator function. The value argument becomes the result of the current yield expression. The send() method returns the next value yielded by the generator, or raises StopIteration if the generator exits without yielding another value. When send() is called to start the generator, it must be called with Noneas the argument, because there is no yield expression that could receive the value.

 
  1. generator.throw(type[, value[, traceback]])

Raises an exception of type type at the point where generator was paused, and returns the next value yielded by the generator function. If the generator exits without yielding another value, a StopIteration exception is raised. If the generator function does not catch the passed-in exception, or raises a different exception, then that exception propagates to the caller.

 
  1. generator.close()

Raises a GeneratorExit at the point where the generator function was paused. If the generator function then raises StopIteration (by exiting normally, or due to already being closed) or GeneratorExit (by not catching the exception), close returns to its caller. If the generator yields a value, a RuntimeError is raised. If the generator raises any other exception, it is propagated to the caller. close() does nothing if the generator has already exited due to an exception or normal exit.

Here is a simple example that demonstrates the behavior of generators and generator functions:

 
  1. >>> def echo(value=None):
  2. ... print "Execution starts when 'next()' is called for the first time."
  3. ... try:
  4. ... while True:
  5. ... try:
  6. ... value = (yield value)
  7. ... except Exception, e:
  8. ... value = e
  9. ... finally:
  10. ... print "Don't forget to clean up when 'close()' is called."
  11. ...
  12. >>> generator = echo(1)
  13. >>> print generator.next()
  14. # Execution starts when 'next()' is called for the first time.
  15. 1
  16. >>> print generator.next()
  17. None
  18. >>> print generator.send(2)
  19. 2
  20. >>> generator.throw(TypeError, "spam")
  21. TypeError('spam',)
  22. >>> generator.close()
  23. # Don't forget to clean up when 'close()' is called.
目录
相关文章
|
5月前
|
Go 调度 Python
Golang协程和Python协程用法上的那些“不一样”
本文对比了 Python 和 Go 语言中协程的区别,重点分析了调度机制和执行方式的不同。Go 的协程(goroutine)由运行时自动调度,启动后立即执行;而 Python 协程需通过 await 显式调度,依赖事件循环。文中通过代码示例展示了两种协程的实际运行效果。
257 7
|
5月前
|
传感器 数据采集 监控
Python生成器与迭代器:从内存优化到协程调度的深度实践
简介:本文深入解析Python迭代器与生成器的原理及应用,涵盖内存优化技巧、底层协议实现、生成器通信机制及异步编程场景。通过实例讲解如何高效处理大文件、构建数据流水线,并对比不同迭代方式的性能特点,助你编写低内存、高效率的Python代码。
258 0
|
4月前
|
数据采集 网络协议 API
协程+连接池:高并发Python爬虫的底层优化逻辑
协程+连接池:高并发Python爬虫的底层优化逻辑
|
6月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
7月前
|
测试技术 Python
Python测试报告生成:整合错误截图,重复用例执行策略,调整测试顺序及多断言机制。
如何组织这一切呢?你可以写一本名为“Python测试之道”的动作指南手册,或者创建一个包含测试策略、测试顺序、多断言机制的脚本库。只要你的测试剧本编写得足够独到,你的框架就会像一位执行任务的超级英雄,将任何潜伏于代码深处的错误无情地揪出来展现在光天化日之下。这些整理好的测试结果,不仅有利于团队协作,更像冒险故事中的精彩篇章,带给读者无尽的探索乐趣和深刻的思考。
187 10
|
12月前
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
1016 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
12月前
|
缓存 JSON 数据处理
Python进阶:深入理解import机制与importlib的妙用
本文深入解析了Python的`import`机制及其背后的原理,涵盖基本用法、模块缓存、导入搜索路径和导入钩子等内容。通过理解这些机制,开发者可以优化模块加载速度并确保代码的一致性。文章还介绍了`importlib`的强大功能,如动态模块导入、实现插件系统及重新加载模块,展示了如何利用这些特性编写更加灵活和高效的代码。掌握这些知识有助于提升编程技能,充分利用Python的强大功能。
784 4
|
UED 开发者 Python
Python中的异常处理机制
Python中的异常处理机制
192 2
|
监控 Java 开发者
Python的垃圾收集机制有哪些?
Python的垃圾收集机制有哪些?
128 2
|
调度 Python
python知识点100篇系列(20)-python协程与异步编程asyncio
【10月更文挑战第8天】协程(Coroutine)是一种用户态内的上下文切换技术,通过单线程实现代码块间的切换执行。Python中实现协程的方法包括yield、asyncio模块及async/await关键字。其中,async/await结合asyncio模块可更便捷地编写和管理协程,支持异步IO操作,提高程序并发性能。协程函数、协程对象、Task对象等是其核心概念。
253 3

推荐镜像

更多