好的,我们开始今天的讲座。
生成器与协程:yield from
的本质与async/await
的语法糖
今天我们将深入探讨Python中生成器和协程的概念,重点分析yield from
的底层实现,以及async/await
语法糖的本质。理解这些概念对于编写高性能、可维护的并发程序至关重要。
1. 生成器:迭代器的简化实现
生成器是一种特殊的迭代器,它使用yield
语句来产生一系列值。与传统的函数不同,生成器函数不会一次性返回所有结果,而是每次调用yield
时暂停执行,并将yield
后面的表达式的值返回给调用者。再次调用生成器时,它会从上次暂停的地方继续执行。
def simple_generator(n):
for i in range(n):
yield i
gen = simple_generator(5)
print(next(gen)) # 输出: 0
print(next(gen)) # 输出: 1
print(next(gen)) # 输出: 2
print(next(gen)) # 输出: 3
print(next(gen)) # 输出: 4
try:
print(next(gen)) # 抛出 StopIteration 异常
except StopIteration:
print("Generator exhausted")
在这个例子中,simple_generator
函数就是一个生成器。每次调用next(gen)
都会产生一个新的值,直到生成器耗尽,抛出StopIteration
异常。
2. yield
语句的深入理解
yield
语句不仅仅是返回一个值,它还扮演着一个“暂停点”的角色。当生成器遇到yield
时,它会:
- 返回一个值: 将
yield
后面的表达式的值返回给调用者。 - 保存状态: 保存生成器当前的状态(包括局部变量、指令指针等)。
- 暂停执行: 暂停生成器的执行。
下次调用生成器时,它会从上次yield
语句之后的位置继续执行。此外,yield
还可以接收来自调用者的值。
def echo_generator():
value = yield
print(f"Received: {value}")
gen = echo_generator()
next(gen) # 启动生成器,直到第一个 yield
gen.send("Hello, world!") # 发送值给生成器
在这个例子中,next(gen)
启动生成器,使其执行到yield
语句。然后,gen.send("Hello, world!")
将字符串 "Hello, world!" 发送给生成器,并赋值给变量 value
。生成器继续执行,打印接收到的值。
3. yield from
:连接多个生成器
yield from
是 Python 3.3 引入的一个新特性,它允许你将一个生成器的执行委托给另一个生成器或可迭代对象。它的作用是简化代码,避免手动迭代子生成器。
def sub_generator(n):
for i in range(n):
yield i
def main_generator(n):
yield from sub_generator(n)
yield "Done!"
gen = main_generator(3)
for value in gen:
print(value)
输出:
0
1
2
Done!
在这个例子中,main_generator
使用 yield from
将迭代过程委托给 sub_generator
。yield from sub_generator(n)
相当于:
def main_generator_equivalent(n):
for i in sub_generator(n):
yield i
yield "Done!"
4. yield from
的底层实现:PEP 380
yield from
的实现比简单的迭代要复杂得多。它涉及到父生成器和子生成器之间的双向通信。PEP 380 详细描述了 yield from
的语义,主要包括以下几个方面:
- 迭代子生成器: 父生成器迭代子生成器,直到子生成器抛出
StopIteration
异常。 - 传递值: 父生成器可以通过
send()
方法将值传递给子生成器。 - 处理异常: 父生成器可以向子生成器抛出异常。
- 返回值: 子生成器的返回值可以传递给父生成器。
为了更清晰地理解 yield from
的底层实现,我们可以将其分解为一系列操作:
操作 | 描述 |
---|---|
result = yield from X |
其中 X 是一个可迭代对象。这个表达式会迭代 X 产生的每一个值。 |
迭代开始 | 首先,调用 iter(X) 获取一个迭代器。 |
迭代过程 | 对于迭代器中的每一个 item ,yield item 会将 item 返回给调用者。 |
send() |
如果调用者使用 send(value) 发送一个值,这个值会传递给迭代器,如果迭代器支持 send() 方法,则调用 iterator.send(value) 。否则,如果迭代器不支持 send() ,会抛出 TypeError 。 |
throw() |
如果调用者使用 throw(type, value, traceback) 抛出一个异常,这个异常会传递给迭代器,如果迭代器支持 throw() 方法,则调用 iterator.throw(type, value, traceback) 。否则,如果迭代器不支持 throw() ,会抛出 TypeError 。 |
close() |
如果迭代器有 close() 方法,并且在迭代过程中被垃圾回收,或者父生成器被关闭,则会调用 iterator.close() 。 |
迭代完成 | 当迭代器耗尽,抛出 StopIteration 异常时,yield from 表达式会返回 StopIteration 异常的 value 属性。如果 StopIteration 没有 value 属性,则返回 None 。 |
下面是一个模拟 yield from
行为的例子:
def emulate_yield_from(iterable):
iterator = iter(iterable)
try:
while True:
try:
yield next(iterator)
except StopIteration as e:
return e.value if hasattr(e, 'value') else None
except GeneratorExit:
if hasattr(iterator, 'close'):
iterator.close()
raise
except Exception as e:
if hasattr(iterator, 'throw'):
iterator.throw(type(e), e, sys.exc_info()[2])
else:
raise
import sys
def sub_generator():
yield 1
yield 2
return "Sub generator done"
def main_generator():
result = yield from sub_generator()
print(f"Sub generator returned: {result}")
yield "Main generator done"
gen = main_generator()
for value in gen:
print(value)
这个例子演示了 yield from
如何迭代子生成器,并获取子生成器的返回值。虽然这个例子只是一个简化版本,但它展示了 yield from
的基本工作原理。
5. 协程:并发编程的新范式
协程是一种用户态的轻量级线程,它允许你在单线程中实现并发。与线程不同,协程的切换由程序员显式控制,避免了线程切换的开销。
在Python中,协程可以通过生成器和 async/await
语法来实现。
6. 基于生成器的协程:asyncio
的前身
在 async/await
语法出现之前,Python 使用生成器来实现协程。asyncio
库的前身,tulip
,就是基于生成器的协程实现的。
import asyncio
@asyncio.coroutine
def my_coroutine():
print("Coroutine started")
yield from asyncio.sleep(1) # 模拟耗时操作
print("Coroutine finished")
loop = asyncio.get_event_loop()
loop.run_until_complete(my_coroutine())
loop.close()
在这个例子中,@asyncio.coroutine
装饰器将一个生成器函数转换为协程。yield from asyncio.sleep(1)
语句会暂停协程的执行,并将控制权交还给事件循环。事件循环在等待 1 秒后,会恢复协程的执行。
7. async/await
:协程的语法糖
async/await
是 Python 3.5 引入的语法糖,它简化了协程的编写。async
关键字用于声明一个协程函数,await
关键字用于等待一个协程的结果。
import asyncio
async def my_coroutine():
print("Coroutine started")
await asyncio.sleep(1) # 模拟耗时操作
print("Coroutine finished")
async def main():
await my_coroutine()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
这个例子与之前的例子功能相同,但使用了 async/await
语法。async def my_coroutine()
声明了一个协程函数,await asyncio.sleep(1)
语句等待 asyncio.sleep(1)
协程的结果。
8. async/await
的本质:生成器的封装
async/await
实际上是对生成器协程的封装。async
关键字将一个函数转换为一个生成器对象,await
关键字则相当于 yield from
。
async def my_coroutine():
print("Coroutine started")
await asyncio.sleep(1)
print("Coroutine finished")
这段代码等价于:
import asyncio
def my_coroutine():
print("Coroutine started")
yield asyncio.sleep(1)
print("Coroutine finished")
import types
my_coroutine = types.coroutine(my_coroutine) # 使用 types.coroutine 装饰器
#使用asyncio.ensure_future可以将一个生成器对象包装成Task对象,以便于放入事件循环中执行
async def main():
task = asyncio.ensure_future(my_coroutine())
await task
在这个例子中,types.coroutine
装饰器将 my_coroutine
函数转换为一个协程对象。await asyncio.sleep(1)
语句实际上是将 asyncio.sleep(1)
协程对象 yield
出去,等待事件循环处理。
9. 总结:生成器、yield from
与 async/await
的关系
- 生成器是迭代器的简化实现,使用
yield
语句产生值。 yield from
用于连接多个生成器,实现生成器的委托。async/await
是协程的语法糖,底层基于生成器和yield from
实现。
理解这些概念对于编写高效的并发程序至关重要。通过生成器和协程,我们可以在单线程中实现并发,提高程序的性能。
10. 深入理解协程与async/await
的价值
async/await
提供的不仅仅是语法的简洁,更重要的是它使得异步编程变得更加直观和易于理解。 协程,作为一种用户态的并发机制,可以有效利用CPU资源,避免线程切换的开销,在IO密集型任务中表现出色。同时,async/await
的设计也使得代码更易于维护和调试。
11. 协程的适用场景与限制
协程非常适合于IO密集型任务,例如网络请求、数据库操作等。然而,对于CPU密集型任务,协程并不能提高性能,因为在单线程中,CPU资源仍然是瓶颈。此外,在使用协程时,需要注意避免阻塞操作,否则会阻塞整个事件循环。
12. 未来发展方向:更强大的并发模型
Python的并发模型正在不断发展。除了生成器和协程,还有其他并发模型,例如多线程、多进程等。在未来,我们可以期待更强大的并发模型,例如基于Actor模型的并发框架,这将使得并发编程更加简单和高效。