浅谈上下文

本文最后更新于:2023年4月24日 凌晨

浅谈上下文

Python 也好久了,在编写代码时,通常会用到一个表达式,with .... as .....,其中用的最多的可能是打开文件的时候使用这个表达式,但是却没想过为什么要这样写,或者这样写有什么方便之处,今天就来复习一下。

一、上下文管理器

何为上下文,在自然语言中,给你一段话,如果没有在上文和下文的情况下,你无法判断这段话在讲什么,也看不懂;这在计算机里也是如此,当一个程序独立存在的时候,它不依赖任何外部的数据或变量,此时它就不存在上下文,但是当多个程序关联起来,互相引用各自的数据或变量时,那么每个程序都不能独自运行了,这个时候就需要一个上下文,来管理这些各自的外部数据和变量。

而在 Python 中,则有了一个上下文管理器的概念,是指实现了 __enter__() 方法和 __exit__()方法的对象;同时上下文管理器的存在也是为了管理 with 语句。

二、with 表达式的使用

with 表达式出现之前,对于资源的管理通常使用三段式的方式来实现,如下所示:

1
2
3
4
5
6
try:
f = open('f')
except Exception as e:
print(e)
finally:
f.close()

当运行发生异常的时候,finally 块里的代码会确保资源被正常的关闭,以此来引发内存泄漏,或者是下面这种实现方式:

1
2
3
4
5
6
try:
f = open('f')
except Exception as e:
print(e)
else:
f.close()

当资源被正确打开时,没发生异常的情况下,代码最终回到 else块,而资源会被正确的关闭。由此可见这样的代码是写的非常长的,同时对资源的管理,异常的捕捉又不是十分方便,这个时候 with 语句就派上用场了。

使用with 语句,可以优雅的实现资源的关闭,如下代码所示。

1
2
3
with open('some.txt') as f:
print(f.read())
print(f.closed)

with 语句中使用 open函数时,完全不必担心打开的文件会没关闭,因为离开with语句块之后,此时打印f.closed已经可以看到为True,为何 open 函数能在 with语句里面做到自动关闭文件的操作,因为它内部实现了 __enter__()__exit__()方法。

三、实现自定义上下文管理器

实现自定义上下文的前提是,要在自定义类里面实现 __enter__() 方法和 __exit__() 方法。

  • __enter__():当with语句块运行后, 会在上下文管理器对象里执行__enter__() 方法,通常情况下,这个方法应该返回一个赋值给 as 后变量的对象,默认情况下为 None,同时这个是可选的,如果不需要返回,那么同时也不需要使用 as 语句。标准上来说应该返回self
  • __exit__():当with 语句结束后,上下文管理器会调用__exit__()方法,效果等同于finally关键字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class File:
def __init__(self, mode, filename):
self.filename = filename
self.mode = mode
self.file = None

def __enter__(self):
print("__enter__")
self.file = open(self.filename, self.mode)
return self.file

def __exit__(self, exec_type, exec_val, exec_tb):
print("__exit__")
if self.file is not None:
self.file.close()

with File('some.txt', 'r') as f:
data = f.read()
print(data)

print(f.closed)

当程序进入 with 语句块之后,__exit__() 方法会帮我们处理好异常,但是 __exit__()语句需要接受 4 个参数,第一个是 self,其他三个参数分别是异常类型,异常属性,异常跟踪信息。

四、使用装饰器实现上下文管理器

如果只是为了创建一个上下文管理器而创建一个类的话,未免也太麻烦了,这个时候 contextlib contextmanager 装饰器就起到了作用,使用这个装饰器可以轻松的在函数上实现上下文管理器,同时它采用的是生成器的实现方式; 以下是代码示范。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from contextlib import contextmanager

@contextmanager
def file(filename, mode):
f = open(filename, mode)
try:
yield f
finally:
file.close()

with file('some.text', 'r') as f:
data = f.read()
print(data)

print(f.closed)

五、异步上下文装饰器

前面的都是基于同步的上下文管理器,contextlib 里还提供了异步的上下文管理器,使用 asynccontextmanager 实现,同样的,这个方法需要放在异步的方法上,才会起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
from contextlib import asynccontextmanager
import asyncio

@asynccontextmanager
async def web_crawler(url):
data = await get_page(url)
yield data
await save_content(url)

async with web_crawler('https://www.baidu.com') as data:
print(data)

asyncio.run(web_crawler())

浅谈上下文
http://aim467.github.io/2021/08/20/浅谈上下文/
作者
Dedsec2z
发布于
2021年8月20日
更新于
2023年4月24日
许可协议