python高阶教程-上下文管理器

本篇内容来自原创小册子《python高阶教程》,点击查看目录

从资源的释放说起

我们知道在打开一个文件后必须关闭、打开一个socket之后也必须关闭,但是总会由于代码的比较复杂或其他原因忘记释放这些资源,导致未定义的后果。

关闭这些资源其实就是为了给后续代码一个“未曾破坏”的运行环境,即在使用这些资源的前后,应保证上下文环境是相同的。与嵌入式编程中的中断需要保存现场、恢复现场有些相似。

在python中是用with语句来实现上下文管理的。

with语句的执行流程

在python中使用with进行上下文的管理,with语句的执行过程如下:

  1. 计算表达式的值,返回一个上下文管理器对象
  2. 加载上下文管理器对象的exit()方法,但不执行
  3. 调用上下文管理器对象的enter()方法
  4. 如果with语句设置了目标对象,则将enter()方法的返回值赋给目标对象
  5. 执行with中的代码块
  6. 如果5中的代码正常结束,调用上下文管理器对象的exit()方法,其返回值直接忽略。
  7. 如果5中的代码发生异常,调用上下文管理器对象的exit()方法,并将异常类型、异常值和traceback传递给exit()方法。如果exit()方法返回值为false,则异常会被重新抛出;如果其返回值为true,则视为异常已经被处理,程序继续执行。

使用类实现上下文

在类中是通过__enter__()__exit__()方法实现的。下面是一个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class my_context():
def __init__(self, num):
self.num = num
def __enter__(self):
print("entering")
return self
def __exit__(self, exception_type,
exception_value,
traceback):
print("exiting")
# test it
with my_context(1) as ins:
print("ins' num is", ins.num)

运行上述代码后,输出为

1
2
3
entering
ins' num is 1
exiting

可以看出,只要在类中实现了__enter____exit__这两个方法,就可以实现一个简单的上下文管理。

在类实现的上下文管理器中进行异常处理

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
30
31
32
33
34
35
36
class my_context():
def __init__(self, num):
self.num = num
def __enter__(self):
print("entering")
return self
def __exit__(self, exception_type,
exception_value,
traceback):
print("exiting")
if exception_type is None:
ret = True
elif exception_type is ValueError:
print("Value error handled")
ret = True
else:
print("Unknown exception type,"
"throw it")
ret = False
return ret
# test it
print("Test 1, no exception")
with my_context(1) as ins:
print("ins' num is", ins.num)
print()
print("Test 2, value error")
with my_context(2) as ins:
print("ins' num is", ins.num)
raise(ValueError)
print()
print("Test 3, key error")
with my_context(3) as ins:
print("ins' num is", ins.num)
raise(KeyError)

上述代码执行后输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Test 1, no exception
entering
ins' num is 1
exiting
Test 2, value error
entering
ins' num is 2
exiting
Value error handled
Test 3, key error
entering
ins' num is 3
exiting
Unknown exception type,throw it
Traceback (most recent call last):
File "error_class.py", line 36, in <module>
raise(KeyError)
KeyError

可以看到,如果在执行with代码块的时候发生了异常,可以在__exit__()方法中进行处理。如果处理结束,返回True,代码继续执行;如果无法处理,就返回False,python会把这个异常继续抛出,直至被正常处理。

使用生成器实现上下文管理器

如果我们只是为一个简单的函数进行上下文管理,那么定义一个类略有些麻烦。好在我们还有标准库可以使用,这个标准库是contextlib。下面是一个简单的应用例子。

1
2
3
4
5
6
7
8
9
10
11
from contextlib import contextmanager
@contextmanager
def process(num):
print("entering")
yield num
print("exiting")
# test it
with process(1) as test_num:
print("test num is", test_num)

执行上述代码后,运行结果如下:

1
2
3
entering
test num is 1
exiting

可以看出,yield关键词用于产生一个生成器,这个生成器又被上下文管理器封装,最后由为with语句返回,即test_num.

在生成器实现的上下文管理器中进行异常处理
使用类的方法进行上下文管理时,异常是作为参数传递的,那使用生成器进行上下文管理时应该怎样做呢?首先想到用try...except语句,如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from contextlib import contextmanager
@contextmanager
def process(num):
print("entering")
try:
yield num
except RuntimeError as err:
print("handled error:", err)
finally:
print("exiting")
# test it
print("Test with runtime error")
with process(1) as test_num:
print("test num is", test_num)
raise(RuntimeError("It's runtime error"))
print()
print("Test with value error")
with process(2) as test_num:
print("test num is", test_num)
raise(ValueError("It's value error"))

上述代码运行后,输出为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Test with runtime error
entering
test num is 1
handled error: It's runtime error
exiting
Test with value error
entering
test num is 2
exiting
Traceback (most recent call last):
File "./generator_error_func.py", line 24, in <module>
raise(ValueError("It's value error"))
ValueError: It's value error

我们在try..except语句中对RuntimeError进行了处理,所以代码可以继续执行;没有对ValueError处理,所以异常继续向上抛,直到控制台输出错误信息。

这里也可以看出,实际上with代码块的内容是在紧接着yield关键词后执行的。了解这个执行顺序后,就可以对上下文管理中出现的错误进行处理。

0%