RAII 概念与在 Python 中的应用
RAII(Resource Acquisition Is Initialization),即资源获取即初始化,是一种设计模式,用于解决资源的获取与初始化的问题,最早在 C++中提出与推广。
在这篇文章我来简单地介绍一下 RAII 的概念,以及在 Python 中的应用。
RAII 的概念
在计算机与程序的世界中,有一些资源,比如文件、网络连接、数据库连接、线程、进程等,这些资源在使用的时候需要获取,在使用完成后需要释放。如果不及时释放,会导致资源泄露,造成资源的浪费,程序出错甚至系统崩溃。
一个简单的示例就是文件的读写。
1 | f = open('test.json', 'r') |
这段代码看起来没有什么问题,但是当test.json
文件的内容不是合法的 JSON 格式时,第四行代码反序列化数据就会抛出异常,导致第五行代码无法执行,文件没有被关闭。
这个例子告诉我们在处理一些资源时,需要注意在操作过程中是否会发生一些意外情况,例如抛出异常,并且在意外情况发生后,也需要关闭资源。
在 Python2.5 之前的版本中,我们用try-finally
来保证程序最终会关闭资源。
1 | try: |
在简单的文件读取操作中,使用try
语句多少有点大材小用。为了更好地处理类似的资源管理问题,Python2.5 引入了with
语句,做到无论语句块中的代码执行是否抛出异常,都可以在退出with
语句块时执行清零代码。
事实上在 Python 中进行文件读写的标准方式就是使用with open
语句。
1 | with open('test.json', 'r') as f: |
Python 中的with
语句就是 RAII(Resource Acquisition Is Initialization)的一种具体实现。
RAII 模式的核心就是让资源和资源对应的对象的生命周期保持一致:
- 对象的初始化会导致资源的初始化,
- 对象的释放会导致资源的释放。
实际上最理想的方式是在文件对象被清理的时候自动关闭文件,然而像 Python、Java 这些有自动管理内存的垃圾回收机制的语言中,一般不会手动控制对象的回收,也就无法保证文件关闭的时机符合预期。一般带 GC 的语言会有自己的 RAII 模式的实现机制,例如 Python 中的with
语句和 Java 中的try with
语句。
RAII 在无 GC 的语言(C++,Rust)中其实表现的更自然。
1 | std::mutex m, |
在上述的 C++代码中,lockGuard
对象在初始化时就会获取m
锁,并且在lockGuard
对象被释放时,会自动释放m
锁,保证了sharedVariable
的值不会被其他线程访问。同时也规避了传统的m.lock()
和m.unlock()
的写法。
当然本文的主题是 Python, 接下来我们将了解一下with
语句的更多细节。
with
语句
Python 中with
语句的语法如下:
1 | with expression [as variable]: |
其中experssion
表达式执行后得到的是一个上下文管理器对象(Context Manager)。
一个上下文管理器可以是任何对象,只要它实现了__enter__
和__exit__
方法。
__enter__
方法的返回值会赋值给variable
变量(需要使用as
语句为其绑定一个名字)。with-block
语句块会在expression
执行完后执行。__exit__
方法会在with-block
语句块执行完后执行(即使 with-block 抛出了异常)。
一个简单的上下文管理器对象的实现如下:
1 | class ContextManager: |
值得注意的是,__exit__
方法的三个参数分别是异常类型、异常值和异常的追踪信息。当然如果没有抛出异常,那么这三个参数都是None
。
我们可以通过with
语句来使用ContextManager
对象:
在with-block
抛出异常时,__exit__
方法也会被调用。
在这个例子中,with-block
抛出的异常会被__exit__
方法捕获,并且被__exit__
方法抛出。
如果不重新抛出异常的话,就会丢失异常信息,类似于在try/except
语句中捕获Exception
却不做任何处理,是不负责任的行为。
应该区分哪些异常是可以处理的,无法处理的异常应该再抛出,由调用者来处理。
使用contextlib
定义上下文管理器
除了给类定义__enter__
方法和__exit__
方法,Python 官方还提供了contextlib
标准库用于简化上下文管理器的定义。
使用contextlib.contextmanager
装饰器装饰生成器函数,yield
语句之前的代码相当于传统上下文管理器的__enter__
方法,yield
的值会被赋值给as
后的变量,yield
之后的代码相当于__exit__
方法,会在退出with-block
后执行。
1 | from contextlib import contextmanager |
上述代码中我们使用contextlib
, 定义了一个myopen
函数来模拟 Python 内置的open
函数,在退出with-block
后执行f.close()
方法,保证了文件被正确释放。
常见的上下文管理器
Python 除了内置的with open
处理文件之外,还有很多的流行的第三方库也广泛使用了with
语句和上下文管理器进行资源管理。
例如requests
库中可以使用with
语句来管理Session
对象,退出with
语句后 session 会自动关闭。
1 | import requests |
redis
库提供的lock
方法也是使用with
语句来管理锁,退出with
语句后锁会自动释放。
1 | import redis |
总结
RAII
是一个比较先进的理念, with
语句是其在 Python 中的实现。在面向资源管理相关的业务场景时,可以更多地使用with
语句来保证代码执行的安全的同时维持代码的简洁与优雅。