Python 生成器与协程:yield from
与 async/await
的语法糖本质
大家好!今天我们来深入探讨 Python 中两个强大的并发编程工具:生成器和协程。我们将重点关注 yield from
语句和 async/await
关键字,揭示它们背后的语法糖本质。
1. 生成器:可迭代的迭代器
首先,让我们回顾一下生成器的概念。生成器是一种特殊的迭代器,它使用 yield
关键字来产生值。与传统的函数不同,生成器函数在调用时不会立即执行,而是返回一个生成器对象。只有在迭代这个生成器对象时,函数体内的代码才会执行,直到遇到 yield
语句。
def my_generator(n):
"""一个简单的生成器函数。"""
for i in range(n):
yield i
# 创建生成器对象
gen = my_generator(5)
# 迭代生成器对象
for value in gen:
print(value) # 输出 0, 1, 2, 3, 4
在这个例子中,my_generator(5)
返回一个生成器对象。当我们使用 for
循环迭代 gen
时,my_generator
函数体内的代码会执行,每次遇到 yield i
时,都会产生一个值,并暂停函数的执行。下次迭代时,函数会从上次暂停的位置继续执行。
生成器的优点:
- 节省内存: 生成器一次只产生一个值,而不是一次性生成所有值,因此可以节省大量内存。尤其是在处理大数据集时,生成器的优势更加明显。
- 延迟计算: 生成器可以延迟计算,只有在需要时才产生值。这可以避免不必要的计算,提高程序的效率。
- 代码简洁: 生成器可以使用简洁的代码实现复杂的迭代逻辑。
2. yield from
:委托生成器
yield from
是 Python 3.3 引入的一个语法糖,用于简化生成器的嵌套调用。它可以将一个生成器的迭代过程委托给另一个生成器,从而避免编写大量的重复代码。
def sub_generator(n):
"""子生成器。"""
for i in range(n):
yield i
def main_generator(m, n):
"""主生成器,使用 yield from 委托给子生成器。"""
yield from sub_generator(m)
yield from sub_generator(n)
# 创建主生成器对象
gen = main_generator(2, 3)
# 迭代主生成器对象
for value in gen:
print(value) # 输出 0, 1, 0, 1, 2
在这个例子中,main_generator
使用 yield from sub_generator(m)
将迭代 sub_generator(m)
的过程委托给了 sub_generator
。实际上,yield from sub_generator(m)
等价于以下代码:
# 等价于 yield from sub_generator(m)
for item in sub_generator(m):
yield item
但 yield from
更简洁、更高效,并且可以处理更复杂的情况,例如子生成器抛出异常或返回结果。
yield from
的工作原理:
yield from
语句实现了双向通道,允许调用者和子生成器之间直接进行交互。它涉及到以下几个操作:
- 委派: 主生成器将迭代操作委托给子生成器。
- 产生值: 子生成器产生的每个值都会直接传递给调用者。
- 接收值: 如果调用者使用
send()
方法发送值给主生成器,该值会被传递给子生成器。 - 抛出异常: 如果调用者使用
throw()
方法向主生成器抛出异常,该异常会被传递给子生成器。 - 关闭: 如果调用者使用
close()
方法关闭主生成器,该方法会被传递给子生成器。 - 返回值: 当子生成器执行完毕时,它的返回值会被传递给主生成器,并可以通过
yield from
表达式获取。
表格:yield from
的操作与对应的行为
操作 | 行为 |
---|---|
yield |
子生成器产生一个值,该值被传递给调用者。 |
send(value) |
调用者发送一个值给主生成器,如果子生成器正在等待接收值,则该值会被传递给子生成器。否则,该值会被忽略。 |
throw(exc) |
调用者向主生成器抛出一个异常,如果子生成器正在等待接收异常,则该异常会被传递给子生成器。否则,该异常会被重新抛出。 |
close() |
调用者关闭主生成器,这会导致子生成器也被关闭。 |
子生成器完成 | 子生成器执行完毕,它的返回值会被传递给主生成器,并可以通过 yield from 表达式获取。 |
3. 协程:轻量级的线程
协程是一种轻量级的线程,可以在单线程中实现并发执行。与线程不同,协程的切换是由程序员显式控制的,而不是由操作系统调度。这使得协程的切换速度非常快,并且可以避免线程切换的开销。
在 Python 中,协程可以通过生成器来实现。我们可以使用 yield
关键字来暂停协程的执行,并将控制权交给另一个协程。当需要恢复协程的执行时,我们可以使用 send()
方法向协程发送一个值,从而激活协程。
def my_coroutine():
"""一个简单的协程。"""
print("Coroutine started")
value = yield
print(f"Coroutine received value: {value}")
yield value * 2
print("Coroutine finished")
# 创建协程对象
coro = my_coroutine()
# 启动协程
next(coro) # 输出 "Coroutine started"
# 向协程发送值
result = coro.send(10) # 输出 "Coroutine received value: 10"
print(f"Coroutine yielded: {result}") # 输出 "Coroutine yielded: 20"
# 关闭协程
try:
coro.send(None)
except StopIteration:
pass
在这个例子中,my_coroutine
函数是一个协程。我们首先使用 next(coro)
启动协程,然后使用 coro.send(10)
向协程发送值。协程接收到值后,会打印一条消息,并产生一个新的值。最后,我们使用 coro.send(None)
关闭协程。
4. async/await
:协程的语法糖
async/await
是 Python 3.5 引入的关键字,用于简化协程的编写。它们是基于生成器的协程的语法糖,使得协程的代码更加易读、易懂。
async
关键字用于定义一个异步函数,异步函数可以包含 await
表达式。await
表达式用于暂停异步函数的执行,直到一个 awaitable 对象完成。awaitable 对象可以是协程、Task 或 Future。
import asyncio
async def my_async_function(n):
"""一个简单的异步函数。"""
print(f"Async function started with {n}")
await asyncio.sleep(1) # 模拟耗时操作
print(f"Async function finished with {n}")
return n * 2
async def main():
"""主函数,调用异步函数。"""
task1 = asyncio.create_task(my_async_function(10))
task2 = asyncio.create_task(my_async_function(20))
result1 = await task1
result2 = await task2
print(f"Result 1: {result1}") # 输出 "Result 1: 20"
print(f"Result 2: {result2}") # 输出 "Result 2: 40"
# 运行事件循环
asyncio.run(main())
在这个例子中,my_async_function
是一个异步函数,它使用 await asyncio.sleep(1)
暂停执行,模拟一个耗时操作。main
函数创建了两个 Task 对象,分别对应于 my_async_function(10)
和 my_async_function(20)
的执行。然后,main
函数使用 await task1
和 await task2
等待 Task 对象完成,并获取结果。
async/await
的本质:
async def
声明的函数本质上仍然是生成器,但它返回的是一个协程对象。await
关键字的作用是暂停协程的执行,并将控制权交给事件循环。当 awaitable 对象完成时,事件循环会恢复协程的执行,并将结果传递给协程。
async/await
使得协程的代码更加简洁、易读。例如,以下代码:
async def my_async_function():
result = await some_awaitable()
return result
在底层,会被转换为类似于以下使用生成器的代码:
def my_async_function():
gen = some_awaitable()
try:
yield gen
except StopIteration as e:
return e.value
async/await
关键字隐藏了生成器的底层细节,使得开发者可以更加专注于编写异步逻辑。
表格:async/await
与生成器的对应关系
async/await |
生成器 |
---|---|
async def |
返回协程对象的生成器函数 |
await |
yield from awaitable 对象,并处理结果 |
5. 事件循环:协程的调度器
事件循环是协程的核心组成部分。它负责调度协程的执行,处理 I/O 事件,以及执行其他异步任务。
asyncio
模块提供了事件循环的实现。我们可以使用 asyncio.get_event_loop()
获取当前事件循环,并使用 loop.run_until_complete()
运行一个协程,直到它完成。
import asyncio
async def my_coroutine():
print("Coroutine started")
await asyncio.sleep(1)
print("Coroutine finished")
# 获取事件循环
loop = asyncio.get_event_loop()
# 运行协程
loop.run_until_complete(my_coroutine())
# 关闭事件循环
loop.close()
在这个例子中,我们首先获取事件循环,然后使用 loop.run_until_complete(my_coroutine())
运行 my_coroutine
协程。事件循环会调度 my_coroutine
的执行,并处理 asyncio.sleep(1)
产生的 I/O 事件。当 my_coroutine
完成时,loop.run_until_complete()
会返回,然后我们使用 loop.close()
关闭事件循环。
总结:生成器、yield from
、协程和 async/await
的关系
- 生成器是可迭代的迭代器,使用
yield
关键字产生值。 yield from
用于简化生成器的嵌套调用,将迭代过程委托给另一个生成器。- 协程是一种轻量级的线程,可以在单线程中实现并发执行。
async/await
是协程的语法糖,使得协程的代码更加易读、易懂。它们基于生成器实现,但隐藏了底层细节。- 事件循环是协程的调度器,负责调度协程的执行,处理 I/O 事件。
6. 实际应用案例
让我们看几个实际应用案例,说明生成器和协程的强大之处。
案例 1:读取大型文件
def read_large_file(file_path):
"""使用生成器逐行读取大型文件。"""
with open(file_path, 'r') as f:
for line in f:
yield line
# 使用生成器读取大型文件
for line in read_large_file('large_file.txt'):
# 处理每一行数据
process_line(line)
这个例子使用生成器逐行读取大型文件,避免一次性将整个文件加载到内存中。这可以节省大量内存,提高程序的效率。
案例 2:并发下载多个网页
import asyncio
import aiohttp
async def download_webpage(url):
"""使用协程并发下载网页。"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def main():
"""主函数,并发下载多个网页。"""
urls = ['https://www.example.com', 'https://www.python.org', 'https://www.google.com']
tasks = [asyncio.create_task(download_webpage(url)) for url in urls]
results = await asyncio.gather(*tasks)
for url, result in zip(urls, results):
print(f"Downloaded {url}: {len(result)} bytes")
# 运行事件循环
asyncio.run(main())
这个例子使用协程并发下载多个网页,可以显著提高下载速度。asyncio.gather(*tasks)
用于并发执行多个 Task 对象,并等待所有 Task 对象完成。
案例 3:实现简单的 Web 服务器
import asyncio
async def handle_client(reader, writer):
"""处理客户端连接。"""
data = await reader.read(100)
message = data.decode()
addr = writer.get_extra_info('peername')
print(f"Received {message!r} from {addr!r}")
print(f"Send: {message!r}")
writer.write(data)
await writer.drain()
print("Close the connection")
writer.close()
async def main():
"""主函数,启动 Web 服务器。"""
server = await asyncio.start_server(
handle_client, '127.0.0.1', 8888)
addr = server.sockets[0].getsockname()
print(f'Serving on {addr}')
async with server:
await server.serve_forever()
# 运行事件循环
asyncio.run(main())
这个例子使用协程实现了一个简单的 Web 服务器,可以处理多个客户端连接。asyncio.start_server()
用于启动服务器,handle_client()
协程用于处理每个客户端连接。
总结:关键概念的再强调
生成器提供了一种高效的迭代方式,yield from
简化了生成器的嵌套调用,协程实现了并发执行,async/await
提供了更简洁的协程语法,而事件循环是协程调度的核心。
7. 最佳实践与注意事项
在使用生成器和协程时,有一些最佳实践和注意事项需要牢记:
- 避免阻塞操作: 协程的优势在于并发执行,因此应避免在协程中执行阻塞操作,例如 I/O 操作。可以使用异步 I/O 库,例如
aiohttp
和asyncpg
,来执行异步 I/O 操作。 - 正确处理异常: 在协程中,需要正确处理异常,避免异常导致程序崩溃。可以使用
try...except
语句来捕获异常,并进行处理。 - 避免死锁: 在使用协程进行并发编程时,需要注意避免死锁。死锁是指两个或多个协程互相等待对方释放资源,导致程序无法继续执行。可以使用锁或其他同步机制来避免死锁。
- 理解事件循环: 理解事件循环的工作原理对于编写高效的协程代码至关重要。需要了解事件循环如何调度协程的执行,以及如何处理 I/O 事件。
- 选择合适的并发模型: 协程适用于 I/O 密集型应用,例如 Web 服务器和网络爬虫。对于 CPU 密集型应用,使用多线程或多进程可能更合适。
8. 总结:高效并发编程的强大工具
生成器和协程是 Python 中强大的并发编程工具。它们可以帮助我们编写高效、可扩展的应用程序。通过理解 yield from
和 async/await
的语法糖本质,我们可以更好地利用这些工具,构建高性能的系统。掌握这些技术,能够写出更健壮,更高效的Python代码。