skip to content
Liu Yang's Blog

[Coroutine]Python从协程到上下文管理器

/ 5 min read

Updated:
Table of Contents

一、什么是协程(Coroutine)

协程可以用于很多场景,例如生成器(Generator)上下文管理器(Context Manager)*等。 从本质上看,协程允许*代码在执行过程中被挂起,并在之后从挂起点继续执行,这是一种比函数调用更加灵活的控制流机制。

下面通过一个生产者–消费者的例子来说明协程的工作方式。


二、生产者与消费者示例

在下面的代码中,consumer 函数中包含 yield,因此它是一个生成器,也可以被视为一个简单的协程。

def consumer():
r = ""
while True:
n = yield r
if not n:
return
print("[CONSUMER] Consuming %s..." % n)
r = '200 OK'

1. 执行流程说明

produce 的视角来看,协程的执行流程如下:

  1. 启动协程 通过 c.send(None) 启动生成器,使其运行到第一个 yield 位置。

  2. 生产与消费交替执行 之后每次调用 c.send(n)

    • consumeryield 处恢复执行
    • 接收生产者传入的数据 n
    • 执行消费逻辑
    • 再次在 yield 处挂起,并将结果返回给生产者

    从而实现“一次生产、一次消费”的效果。

  3. 关闭协程 最终需要调用 c.close() 关闭协程。

def produce(c):
c.send(None) # 启动生成器
n = 0
while n < 5:
n += 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return %s' % r)
c.close()
c = consumer() # generator
produce(c)

2. 为什么要 close?

如果将 consumer 类比为一个 HTTP 连接池中的连接,那么 close() 的含义就是归还资源。 如果忘记关闭协程,可能会导致资源无法释放,从而造成连接泄露或内存泄露


三、Python 中的上下文管理器(Context Manager)

1. 传统的资源管理方式

以打开文件为例,最原始的写法通常是:

try:
f = open('/path/to/file', 'r')
f.read()
finally:
if f:
f.close()

这种方式的问题在于:

  • 样板代码较多
  • 容易遗漏 close

2. 使用 with 语句简化资源管理

Python 提供了上下文管理器协议,通过 __enter____exit__ 方法,将资源的获取与释放自动化:

with open('/path/to/file', 'r') as f:
f.read()

3. 自定义上下文管理器

下面是一个简单的 Query 类示例:

class Query(object):
def __init__(self, name):
self.name = name
def __enter__(self):
print('Begin')
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
print('Error')
else:
print('End')
def query(self):
print('Query info about %s...' % self.name)

使用方式如下:

with Query('Bob') as q:
q.query()

四、使用协程简化上下文管理器的实现

手动实现 __enter____exit__ 有时会显得比较繁琐。 Python 标准库 contextlib 提供了 @contextmanager 装饰器,可以基于生成器(协程)来实现上下文管理器

1. 基于生成器的上下文管理器

核心思想是:

  • yield 之前的代码 → 相当于 __enter__
  • yield 之后的代码 → 相当于 __exit__
from contextlib import contextmanager
class Query(object):
def __init__(self, name):
self.name = name
def query(self):
print('Query info about %s...' % self.name)
@contextmanager
def create_query(name):
print('Begin')
q = Query(name)
yield q
print('End')

使用方式完全一致:

with create_query('Bob') as q:
q.query()

这里的 create_query 本质上返回的是一个生成器对象,上下文管理器正是通过协程的“挂起与恢复”机制来完成资源的管理。


五、对任意代码块进行包装

基于同样的思路,我们也可以对任意代码块进行包裹,例如用于打标签、统计耗时、日志埋点等。

@contextmanager
def tag(name):
print("<%s>" % name)
yield
print("</%s>" % name)

使用示例:

with tag("run"):
print("running...")

输出:

<run>
running...
</run>

六、小结

  • 协程的核心能力:代码可以被挂起,并从挂起点继续执行
  • 生成器是协程的一种实现形式
  • send / yield / close 构成了协程之间的通信机制
  • 上下文管理器本质上是对资源生命周期的管理
  • @contextmanager 利用协程机制,大幅简化了上下文管理器的实现

如果你愿意,我也可以帮你把这篇文章改成面试版总结博客发布版(含标题与结构目录)