详细理解并举例分析Python中的上下文管理器

理解Python的上下文管理器的原理和使用

一 背景

很久前看了《流畅的Python》一书,未做笔记,最近学FastAPI框架时,学到了在FastAPI中使用到了上下文管理器,特此来把笔记补上,加深学习印象!

二 简介

with作为python上下文管理的关键字,它会设置一个临时的上下文,交给上下文的管理器对象控制,并且负责清理上下文,避免错误以及减少重用代码。

用一句话简要概括: 上下文管理器的存在母的是管理with语句,就像迭代器的存在是为了管理for语句


三 探索with

1.with的语句是简化重复的try/finally在语句中多次出现,并完成try/finally该做的事情。说到这,我觉得装饰器和上下文管理器有着异曲同工之妙,面向切面编程。

2.上下文管理器的协议包含__enter____exit__方法。

3.以with open() as f:为例子大致执行流程:

(1)首先with开始运行时,会在上下文管理器中调用__enter__方法,将__enter__返回的值绑定到f对象上,这样就得到了上下文管理对象,接下来在with代码体内都可以通过f来调用相关属性(例如这里的f就是文件对象,那么我就可以通过f来调用文件对象中的属性和方法)。退出with代码块时调用__exit__

(2)__enter__返回的也不一定是上下文对象(自身),也可以是其他对象

(3)注意要绑定对象,必须使用as


举一个最简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TestContextManager:
def __enter__(self):
print('__enter__ is running')
return self

def compute(self):
x = 1/0
print('computing is running')

def __exit__(self, exc_type, exc_value,traceback):
if exc_type is ZeroDivisionError:
print('error')
else:
print('no error')
return True


with TestContextManager() as t:
t.compute()

# 执行结果:
__enter__ is running
error

执行流程:先进入with前,执行__enter__,然后调用compute,最后退出with时调用__exit__

注意点:

1.从上述__exit__代码的参数中可以看出需要传入三个参数,分别为异常类型,异常实例(通过exc_value.args来获取异常值),回朔点对象。

2.注意看__exit__中最后一句是return True,这里会告诉解释器异常已经处理,解释器会压制异常,从而不会报错。而如果不写return True时,则解释器仍会抛出异常。这一点会在@contextmanager装饰器中再次提到


四 常用的contextlib.@contextmanager装饰器

1.在抽象AbstractContextManager(出现在3.6中)定义了该两个函数的默认抽象方法,同时定义了一个钩子函数,用于检查自定义的上下文管理器类是否具备__enter__,和__exit__方法。(也就是继承了抽象类,必须要存在这两个方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

class AbstractContextManager(abc.ABC):

"""An abstract base class for context managers."""

def __enter__(self):
"""Return `self` upon entering the runtime context."""
return self

@abc.abstractmethod
def __exit__(self, exc_type, exc_value, traceback):
"""Raise any exception triggered within the runtime context."""
return None

@classmethod
def __subclasshook__(cls, C):
if cls is AbstractContextManager:
return _collections_abc._check_methods(C, "__enter__", "__exit__")
return NotImplemented

(1)默认的__enter__返回的是上下文管理器对象自身。

(2)默认的__exit__返回的是None


2.AbstractAsyncContextManager和AbstractContextManager的区别主要在前者之处异步,后者支持同步。

3.@contextmanager(出现在3.7版本)是一个装饰器,装饰器在这里的作用是简化代码,通过装饰器来装饰函数,而不需要定义一个类,包含两个必要的函数(__enter__,__exit__)。这样在函数中需用yield来作为__enter____exit__的临界点。yield的前半段的代码作用等同于调用了__enter__,后半段等同与调用了__exit__

4.@contextmanager会将函数包装成实现了__enter____exit__

5.会在退出with的作用域后调用__exit__或执行yield之后的代码段


仍用上面的1/0举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

@contextlib.contextmanager
def test_contextmanager(number):

print('enter %d' % number)

try:
yield 1
except ZeroDivisionError as e:
print(e)

with test_contextmanager(0) as t:
print(t)
print('running') # 模拟业务逻辑
print(1/0) # 模拟业务逻辑出错,会被yield后的异常处理代码段捕捉

# 执行结果:
enter 0 # yield之前执行
1 # yield之后执行
running # yield之后执行
division by zero # 异常处理

注意:

使用调试模式运行,在with下面的所有语句(包括自身)打上断点,就可以直观的感受到是如何运行的了,@contextlib.contextmanager是语法糖,能够在无需__enter____exit__方法,通过单yield实现
上下文管理器功能。


4.1 @contextmanager中等价于__enter__的功能作用:

(1)调用生成器函数(被装饰的函数),保存生成器对象obj。

(2)调用next(obj),执行到yield关键字所在的位置处。

(3)将yield value产出的值,绑定到as 后指明的目标变量上。


4.2 @contextmanager中等价于__exit__的功能作用:

(1)检查有没有把异常传给exc_type,如果有则调用obj.throw,抛出异常。

(2)在生成器函数yield关键字那一行抛出抛出异常(可以回滚到yield),在yield后半段处理异常

1
2
3
4
5
6
7
# 例如在yield处抛出异常,yield后半段进行处理
try:
yield ValueError('value error')
except ValueError as e:
print(e)
finally:
print('Exception successfully handled')

(3)否则没有异常,则继续调用next(obj),执行yield语句以后的代码。

注意点:

刚才上面提到了自定义的上下文类中的__exit__方法中使用了return True来压制异常。但是在@contextmanager中由于没有显示的__exit__方法存在,所以其装饰器会默认异常已经被处理,也就是强制压制了异常,因此在使用@contextmanager的时候必须要在yield处用try和catch捕获异常。然后捕捉到在yield处抛出。


五 FastApi中所提到的Dependencies with yield

1.如果对FastApi不感兴趣的可以不用往下看了。

2.FastApi中使用到了一种解耦+复用的机制—-依赖注入,依赖注入在Spring中也被广泛使用到。而FastApi将依赖注入和上下文管理器结合到了一起。

举个官网的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from fastapi import Depends


async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()


async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)


async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)

@app.get('/items-syz')
def dependency(dependency: str = DeDepends(dependency_c)):
return dependency

分析:

1.FastApi中对上下文管理器的用法和普通的上下文管理器有一些差别,但基本原理是一致的。

2.FastApi中设定了这样一种机制,它允许在__exit__中依赖项处理异常程序之外自定义异常处理程序。其中依赖项退出代码相当于在__exit__中定义的函数。而自定义异常需要自定义异常处理程序处理,不能由依赖项退出代码处理(__exit__\yield之后)

3.FastApi在捕获处理了HttpExcpetion后,其他自定义的异常处理程序将不能再捕获该异常。

4.如果在后台运行期间(路径函数中)发生了异常,可以会滚到yield处进行捕获抛出。

5.进入到yield后的代码段,将执行异常处理程序,同时将不能再对response做出修改(此时已经退出了路径函数作用域)。

6.总的来说,其实并不需要在fastapi中使用@contextmanager装饰器或者自定义上下文管理器类实现,因为它已经将其和依赖注入结合封装好了(Depends),要学习它的原理。


结合官方的一张图能更好的理解:

6666{width=100%}