0%

RAII概念与在Python中的应用

RAII 概念与在 Python 中的应用

RAII(Resource Acquisition Is Initialization),即资源获取即初始化,是一种设计模式,用于解决资源的获取与初始化的问题,最早在 C++中提出与推广。
在这篇文章我来简单地介绍一下 RAII 的概念,以及在 Python 中的应用。

RAII 的概念

在计算机与程序的世界中,有一些资源,比如文件、网络连接、数据库连接、线程、进程等,这些资源在使用的时候需要获取,在使用完成后需要释放。如果不及时释放,会导致资源泄露,造成资源的浪费,程序出错甚至系统崩溃。

一个简单的示例就是文件的读写。

1
2
3
4
f = open('test.json', 'r')
raw = f.read()
data = json.loads(raw)
f.close()

这段代码看起来没有什么问题,但是当test.json文件的内容不是合法的 JSON 格式时,第四行代码反序列化数据就会抛出异常,导致第五行代码无法执行,文件没有被关闭。

这个例子告诉我们在处理一些资源时,需要注意在操作过程中是否会发生一些意外情况,例如抛出异常,并且在意外情况发生后,也需要关闭资源。

在 Python2.5 之前的版本中,我们用try-finally来保证程序最终会关闭资源。

1
2
3
4
5
6
7
8
try:
f = open('test.json', 'r')
raw = f.read()
data = json.loads(raw)
except JSONDecodeError:
...
finally:
f.close()

在简单的文件读取操作中,使用try语句多少有点大材小用。为了更好地处理类似的资源管理问题,Python2.5 引入了with语句,做到无论语句块中的代码执行是否抛出异常,都可以在退出with语句块时执行清零代码。

事实上在 Python 中进行文件读写的标准方式就是使用with open语句。

1
2
3
with open('test.json', 'r') as f:
raw = f.read()
data = json.loads(raw)

Python 中的with语句就是 RAII(Resource Acquisition Is Initialization)的一种具体实现。
RAII 模式的核心就是让资源和资源对应的对象的生命周期保持一致:

  • 对象的初始化会导致资源的初始化,
  • 对象的释放会导致资源的释放。

实际上最理想的方式是在文件对象被清理的时候自动关闭文件,然而像 Python、Java 这些有自动管理内存的垃圾回收机制的语言中,一般不会手动控制对象的回收,也就无法保证文件关闭的时机符合预期。一般带 GC 的语言会有自己的 RAII 模式的实现机制,例如 Python 中的with语句和 Java 中的try with语句。

RAII 在无 GC 的语言(C++,Rust)中其实表现的更自然。

1
2
3
4
5
std::mutex m,
{
std::lock_guard<std::mutex> lockGuard(m);
sharedVariable= getVar();
}

在上述的 C++代码中,lockGuard对象在初始化时就会获取m锁,并且在lockGuard对象被释放时,会自动释放m锁,保证了sharedVariable的值不会被其他线程访问。同时也规避了传统的m.lock()m.unlock()的写法。

当然本文的主题是 Python, 接下来我们将了解一下with语句的更多细节。

with语句

Python 中with语句的语法如下:

1
2
with expression [as variable]:
with-block

其中experssion表达式执行后得到的是一个上下文管理器对象(Context Manager)。
一个上下文管理器可以是任何对象,只要它实现了__enter____exit__方法。

  • __enter__方法的返回值会赋值给variable变量(需要使用as语句为其绑定一个名字)。
  • with-block语句块会在expression执行完后执行。
  • __exit__方法会在with-block语句块执行完后执行(即使 with-block 抛出了异常)。

一个简单的上下文管理器对象的实现如下:

1
2
3
4
5
6
7
8
9
class ContextManager:
def __enter__(self):
print('enter')
return self

def __exit__(self, ex_type, ex_value, ex_traceback):
print('exit')
if ex_value:
raise ex_value

值得注意的是,__exit__方法的三个参数分别是异常类型、异常值和异常的追踪信息。当然如果没有抛出异常,那么这三个参数都是None

我们可以通过with语句来使用ContextManager对象:

normal

with-block抛出异常时,__exit__方法也会被调用。

exception

在这个例子中,with-block抛出的异常会被__exit__方法捕获,并且被__exit__方法抛出。

如果不重新抛出异常的话,就会丢失异常信息,类似于在try/except语句中捕获Exception却不做任何处理,是不负责任的行为。

应该区分哪些异常是可以处理的,无法处理的异常应该再抛出,由调用者来处理。

使用contextlib定义上下文管理器

除了给类定义__enter__方法和__exit__方法,Python 官方还提供了contextlib标准库用于简化上下文管理器的定义。

使用contextlib.contextmanager装饰器装饰生成器函数,yield语句之前的代码相当于传统上下文管理器的__enter__方法,yield的值会被赋值给as后的变量,yield之后的代码相当于__exit__方法,会在退出with-block后执行。

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

@contextmanager
def myopen(path:str,mode:str):
f = open(path,mode)
try:
yield f
finally:
f.close()

with myopen('test.json','r') as f:
raw = f.read()
data = json.loads(raw)

上述代码中我们使用contextlib, 定义了一个myopen函数来模拟 Python 内置的open函数,在退出with-block后执行f.close()方法,保证了文件被正确释放。

常见的上下文管理器

Python 除了内置的with open处理文件之外,还有很多的流行的第三方库也广泛使用了with语句和上下文管理器进行资源管理。

例如requests库中可以使用with语句来管理Session对象,退出with语句后 session 会自动关闭。

1
2
3
4
5
6
import requests

with requests.Session() as s:
s.get('https://httpbin.org/cookies/set/key/value')
resp = s.get('https://httpbin.org/cookies')
print(resp.json()) # {'cookies': {'key': 'value'}}

redis库提供的lock方法也是使用with语句来管理锁,退出with语句后锁会自动释放。

1
2
3
4
5
6
import redis

client = redis.Redis()

with client.lock('LOCK_KEY'):
print('do_something')

总结

RAII是一个比较先进的理念, with语句是其在 Python 中的实现。在面向资源管理相关的业务场景时,可以更多地使用with语句来保证代码执行的安全的同时维持代码的简洁与优雅。

扫码加入技术交流群🖱️
QR code