Python的协程与生成器:深入解析`yield from`与`async/await`的底层机制。

Python协程与生成器:深入解析yield fromasync/await

大家好,今天我们来深入探讨Python中的协程和生成器,特别是yield fromasync/await这两个关键特性。我们将从生成器开始,逐步过渡到协程,并剖析它们背后的机制。

1. 生成器:迭代器的进化

在理解协程之前,我们必须先掌握生成器的概念。生成器是一种特殊的迭代器,它使用yield语句来产生值,而不是使用return语句。

1.1 生成器函数与生成器对象

一个包含yield语句的函数被称为生成器函数。调用生成器函数不会立即执行函数体,而是返回一个生成器对象。

def my_generator(n):
  for i in range(n):
    yield i

gen = my_generator(3)
print(gen)  # 输出: <generator object my_generator at 0x...>

1.2 生成器的工作方式

生成器对象通过next()函数(或者在for循环中使用)来逐个产生值。每次调用next(),生成器函数会执行到下一个yield语句,产生一个值并暂停。下次调用next()时,从上次暂停的地方继续执行。当函数执行完毕或者遇到return语句时,会抛出StopIteration异常。

print(next(gen))  # 输出: 0
print(next(gen))  # 输出: 1
print(next(gen))  # 输出: 2
try:
  print(next(gen))
except StopIteration:
  print("Iteration finished")  # 输出: Iteration finished

1.3 生成器的优势

生成器的主要优势在于惰性求值。它们只在需要时才产生值,而不是一次性生成所有值。这可以节省大量内存,尤其是在处理大型数据集时。

def infinite_sequence():
  num = 0
  while True:
    yield num
    num += 1

# 使用生成器而不必担心内存溢出
for i in infinite_sequence():
  if i > 10:
    break
  print(i)

2. yield from:连接生成器的桥梁

yield from 是 Python 3.3 引入的语法糖,它允许我们将一个生成器的产生过程委托给另一个生成器。

2.1 yield from 的基本用法

def sub_generator(n):
  for i in range(n):
    yield i

def main_generator(n):
  yield from sub_generator(n)
  yield "Done"

for i in main_generator(3):
  print(i)

# 输出:
# 0
# 1
# 2
# Done

在这个例子中,main_generator 使用 yield from 将产生值的任务委托给了 sub_generatormain_generator 实际上并没有直接产生 0, 1, 2 这些值,而是由 sub_generator 产生的。

2.2 yield from 的等价形式

yield from iterable 实际上可以展开成以下代码:

def equivalent_yield_from(iterable):
  it = iter(iterable)
  try:
    while True:
      yield next(it)
  except StopIteration:
    pass

这段代码首先获取可迭代对象 iterable 的迭代器。然后,它不断地从迭代器中获取下一个值,并使用 yield 产生该值。当迭代器抛出 StopIteration 异常时,循环结束。

2.3 yield from 的高级用法:双向通道

yield from 不仅仅是简单的委托,它还创建了一个双向通道,允许调用者和子生成器之间进行通信。这意味着调用者可以向子生成器发送值(使用 send() 方法),也可以从子生成器接收值。

2.3.1 send() 方法

生成器对象有一个 send() 方法,可以向生成器发送一个值。这个值会成为 yield 表达式的结果。

def echo_generator():
  while True:
    received = yield
    print("Received:", received)

gen = echo_generator()
next(gen)  # 启动生成器 (第一次必须调用 next() 或者 send(None))
gen.send("Hello")  # 输出: Received: Hello
gen.send("World")  # 输出: Received: World

2.3.2 throw() 方法

生成器对象还有一个 throw() 方法,可以向生成器抛出一个异常。

2.3.3 close() 方法

生成器对象还有一个 close() 方法,可以关闭生成器。

2.3.4 双向通道示例

def sub_generator():
  x = yield
  print("Subgenerator received:", x)
  return "Subgenerator done"

def main_generator():
  result = yield from sub_generator()
  print("Main generator received:", result)

gen = main_generator()
next(gen)  # 启动生成器
gen.send(10) #  Subgenerator received: 10
# Main generator received: Subgenerator done

在这个例子中,main_generator 使用 yield from 调用 sub_generatormain_generator 通过 send(10) 将值 10 发送给 sub_generatorsub_generator 接收到值后,打印出来,并返回 "Subgenerator done"。这个返回值会成为 yield from 表达式的结果,并被赋值给 result。最后,main_generator 打印出 "Main generator received: Subgenerator done"。

2.4 yield from 的完整语义

为了更清晰地理解 yield from 的工作方式,我们可以将其展开成更详细的代码:

def yield_from_expanded(gen):
  """
  等价于 yield from gen() 的展开形式
  """
  _i = iter(gen)  # 获取迭代器
  try:
    _y = next(_i)  # 预激子生成器
  except StopIteration as _e:
    _r = _e.value  # 获取子生成器的返回值
  else:
    while 1:
      try:
        _s = yield _y  # 产生值并接收调用者的值
      except GeneratorExit as _e:
        try:
          _m = _i.close
        except AttributeError:
          pass
        else:
          _m()
        raise _e
      except BaseException as _e:
        _x = sys.exc_info()
        try:
          _m = _i.throw
        except AttributeError:
          raise _e
        else:
          try:
            _y = _m(*_x)
          except StopIteration as _e:
            _r = _e.value
            break
      else:
        try:
          if _s is None:
            _y = next(_i)
          else:
            _y = _i.send(_s)
        except StopIteration as _e:
          _r = _e.value
          break
  return _r

这段代码展示了 yield from 如何处理异常、发送值和接收返回值。它完整地模拟了 yield from 的行为。

3. 协程:并发的基石

协程是一种更高级的生成器,它允许在多个函数之间交替执行,从而实现并发。在 Python 中,协程通常使用 async/await 语法来定义。

3.1 async/await 语法

async 关键字用于声明一个协程函数。await 关键字用于暂停协程的执行,等待另一个协程完成。

import asyncio

async def my_coroutine(n):
  print(f"Coroutine {n} started")
  await asyncio.sleep(1)  # 模拟耗时操作
  print(f"Coroutine {n} finished")
  return f"Result from coroutine {n}"

async def main():
  task1 = asyncio.create_task(my_coroutine(1))
  task2 = asyncio.create_task(my_coroutine(2))

  result1 = await task1
  result2 = await task2

  print(result1)
  print(result2)

asyncio.run(main())

# 可能的输出:
# Coroutine 1 started
# Coroutine 2 started
# (等待 1 秒)
# Coroutine 1 finished
# Coroutine 2 finished
# Result from coroutine 1
# Result from coroutine 2

在这个例子中,my_coroutine 是一个协程函数。await asyncio.sleep(1) 会暂停 my_coroutine 的执行,并将控制权交还给事件循环。事件循环会继续执行其他协程,直到 asyncio.sleep(1) 完成。当 asyncio.sleep(1) 完成后,my_coroutine 会恢复执行。

3.2 事件循环

事件循环是协程的核心。它负责调度协程的执行,处理 I/O 事件,以及执行其他任务。asyncio.run() 函数会创建一个事件循环,并运行指定的协程。

3.3 await 的底层机制

await 关键字实际上是一个语法糖。它背后做了以下几件事情:

  1. 检查 await 后面的表达式是否是一个 awaitable 对象。Awaitable 对象是指实现了 __await__() 方法的对象。
  2. 调用 awaitable 对象的 __await__() 方法,返回一个迭代器。
  3. 将当前协程暂停,并将控制权交还给事件循环。
  4. 当 awaitable 对象完成时,事件循环会恢复当前协程的执行,并获取 awaitable 对象的返回值。

3.4 协程与生成器的关系

协程本质上是一种特殊的生成器。async 声明的函数实际上返回一个协程对象,而协程对象实现了 __await__() 方法,使其成为一个 awaitable 对象。await 关键字实际上是 yield from 的一种变体,它用于等待一个 awaitable 对象完成。

3.5 手动实现 await

为了更好地理解 await 的底层机制,我们可以手动实现一个简单的 await 函数:

def manual_await(awaitable):
  """
  手动实现 await 的功能
  """
  iterator = awaitable.__await__()
  try:
    next(iterator)
  except StopIteration as e:
    return e.value

这个函数首先获取 awaitable 对象的迭代器。然后,它调用 next() 函数来启动迭代器。当迭代器抛出 StopIteration 异常时,函数返回迭代器的返回值。

3.6 异步迭代器和异步上下文管理器

Python 3.5 引入了异步迭代器和异步上下文管理器,它们分别使用 async forasync with 语句来处理。

3.6.1 异步迭代器

异步迭代器是指实现了 __aiter__()__anext__() 方法的迭代器。__aiter__() 方法返回异步迭代器本身,__anext__() 方法返回一个 awaitable 对象,该对象产生下一个值。

class AsyncIterator:
  def __init__(self, data):
    self.data = data
    self.index = 0

  async def __aiter__(self):
    return self

  async def __anext__(self):
    if self.index >= len(self.data):
      raise StopAsyncIteration
    value = self.data[self.index]
    self.index += 1
    return value

async def main():
  async for item in AsyncIterator([1, 2, 3]):
    print(item)

asyncio.run(main())

3.6.2 异步上下文管理器

异步上下文管理器是指实现了 __aenter__()__aexit__() 方法的上下文管理器。__aenter__() 方法返回一个 awaitable 对象,该对象在进入上下文时执行。__aexit__() 方法返回一个 awaitable 对象,该对象在退出上下文时执行。

class AsyncContextManager:
  async def __aenter__(self):
    print("Entering context")
    return self

  async def __aexit__(self, exc_type, exc_val, exc_tb):
    print("Exiting context")

async def main():
  async with AsyncContextManager():
    print("Inside context")

asyncio.run(main())

4. 深入理解asyncio库的内部机制

asyncio库是Python中进行异步编程的核心库,它提供了事件循环、协程、任务调度等功能。理解asyncio的内部机制对于编写高效的异步代码至关重要。

4.1 事件循环的本质

事件循环本质上是一个无限循环,它不断地检查是否有就绪的事件需要处理。这些事件可能包括:

  • I/O 事件(例如,socket 连接的建立、数据的接收)
  • 定时器事件(例如,asyncio.sleep()
  • 协程的完成

事件循环使用多路复用技术(例如,selectpollepoll)来监听多个文件描述符(file descriptor),以便在有事件发生时能够及时地通知。

4.2 任务(Task)的调度

asyncio.create_task() 函数用于创建一个任务。任务是对协程的封装,它可以被事件循环调度执行。当一个任务被创建时,它会被放入事件循环的就绪队列中。事件循环会按照一定的策略(例如,先进先出)从就绪队列中取出任务,并执行它。

4.3 Future 对象

Future 对象代表一个异步操作的最终结果。当一个异步操作开始时,会创建一个 Future 对象。当异步操作完成时,Future 对象会被设置为已完成状态,并包含异步操作的结果或异常。

await 关键字实际上是在等待一个 Future 对象完成。当 await 遇到一个未完成的 Future 对象时,它会将当前协程暂停,并将 Future 对象注册到事件循环中。当 Future 对象完成时,事件循环会恢复当前协程的执行。

4.4 实现简单的事件循环

为了更深入地理解事件循环的内部机制,我们可以手动实现一个简单的事件循环:

import time
import queue

class SimpleEventLoop:
  def __init__(self):
    self._tasks = queue.Queue()

  def create_task(self, coro):
    self._tasks.put(coro)

  def run_forever(self):
    while not self._tasks.empty():
      task = self._tasks.get()
      try:
        next(task)
        self._tasks.put(task)  # 重新加入队列,等待下次执行
      except StopIteration:
        pass # 协程执行完毕

async def my_coroutine(n):
  print(f"Coroutine {n} started")
  await sleep(1)  # 使用自定义的 sleep
  print(f"Coroutine {n} finished")

async def sleep(delay):
  """
  模拟 asyncio.sleep
  """
  start_time = time.time()
  while time.time() - start_time < delay:
    yield  # 让出控制权

async def main():
  await my_coroutine(1)
  await my_coroutine(2)

loop = SimpleEventLoop()
loop.create_task(main().__await__())  # 注意: 需要调用 __await__() 方法
loop.run_forever()

这个简单的事件循环使用一个队列来存储任务。create_task() 函数将协程添加到队列中。run_forever() 函数不断地从队列中取出任务,并执行它。sleep() 函数使用 yield 让出控制权,从而模拟异步操作。

注意: 这个简单的事件循环仅仅是为了演示事件循环的内部机制。在实际开发中,应该使用 asyncio 库提供的事件循环。

5. 生成器和协程的不同

虽然协程是在生成器的基础上构建的,但它们之间存在一些关键的区别:

特性 生成器 协程
目的 产生一系列值,实现迭代器协议 实现并发,允许在多个函数之间交替执行
关键字 yield asyncawait
调用方式 next()for 循环 await
返回值 产生的值 Future 对象或其他 awaitable 对象
双向通信 支持,通过 send()throw()close() 支持,通过 awaitsend() (更常见于底层实现)
上下文切换 手动或隐式 由事件循环管理

协程和生成器:异步编程的基石

我们深入研究了 Python 中的协程和生成器,从基本的生成器函数到高级的 async/await 语法,涵盖了 yield from 的详细机制以及 asyncio 库的核心概念。 理解这些概念对于编写高效、可维护的异步代码至关重要。希望本次分享能够帮助大家更好地掌握 Python 的异步编程技术。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注