zl程序教程

您现在的位置是:首页 >  后端

当前栏目

RAII概念与在Python中的应用

2023-06-13 09:18:30 时间

RAII 概念与在 Python 中的应用

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

RAII 的概念

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

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

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

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

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

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

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语句。

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)中其实表现的更自然。

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语句的语法如下:

with expression [as variable]:
    with-block

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

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

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

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对象:

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后执行。

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 会自动关闭。

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语句后锁会自动释放。

import redis

client = redis.Redis()

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

总结

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