关于python:Python中的上下文管理器

8次阅读

共计 4458 个字符,预计需要花费 12 分钟才能阅读完成。

我想你对 Python 中的 with 语句肯定不生疏,尤其是在文件的读写操作中,不过我想,大部分人可能习惯了它的应用,却并不知道暗藏在其背地的“机密”。

那么,到底 with 语句要怎么用,与之相干的上下文管理器(context manager)是什么,它们之间又有着怎么的分割呢?这篇文章就为大家带来解密~

什么是上下文处理器?

在任何一门编程语言中,文件的输入输出、数据库的连贯断开等,都是很常见的资源管理操作。但资源都是无限的,在写程序时,咱们必须保障这些资源在应用过后失去开释,不然就容易造成资源泄露,轻者使得零碎解决迟缓,重则会使零碎解体。

光说这些概念,你可能领会不到这一点,咱们能够看看上面的例子:

for x in range(10000000):
    f = open('test.txt', 'w')
    f.write('hello')

这里咱们一共关上了 10000000 个文件,然而用完当前都没有敞开它们,如果你运行该段代码,便会报错:

OSError: [Errno 23] Too many open files in system: 'test.txt'

这就是一个典型的资源泄露的例子。因为程序中同时关上了太多的文件,占据了太多的资源,造成零碎解体。

为了解决这个问题,不同的编程语言都引入了不同的机制。而在 Python 中,对应的解决形式便是上下文管理器(context manager)。上下文管理器,可能帮忙你主动调配并且开释资源,其中最典型的利用便是 with 语句。所以,下面代码的正确写法应该如下所示:

for x in range(10000000):
    with open('test.txt', 'w') as f:
        f.write('hello')

这样,咱们每次关上文件“test.txt”,并写入‘hello’之后,这个文件便会主动敞开,相应的资源也能够失去开释,避免资源泄露。当然,with 语句的代码,也能够用上面的模式示意:

f = open('test.txt', 'w')
try:
    f.write('hello')
finally:
    f.close()

要留神的是,最初的 finally block 尤其重要,哪怕在写入文件时产生谬误异样,它也能够保障该文件最终被敞开。不过与 with 语句相比,这样的代码就显得冗余了,并且还容易漏写,因而咱们个别更偏向于应用 with 语句。

另外一个典型的例子,是 Python 中的 threading.lock 类。举个例子,比方我想要获取一个锁,执行相应的操作,实现后再开释,那么代码就能够写成上面这样:

some_lock = threading.Lock()
some_lock.acquire()
try:
    ...
finally:
    some_lock.release()

而对应的 with 语句,同样十分简洁:

some_lock = threading.Lock()
with somelock:
    ...

咱们能够从这两个例子中看到,with 语句的应用,能够简化了代码,无效防止资源泄露的产生。

上下文管理器的实现

基于类的上下文管理器

理解了上下文治理的概念和长处后,上面咱们就通过具体的例子,一起来看看上下文管理器的原理,搞清楚它的外部实现。这里,我自定义了一个上下文治理类 FileManager,模仿 Python 的关上、敞开文件操作:

class FileManager:
    def __init__(self, name, mode):
        print('calling __init__ method')
        self.name = name
        self.mode = mode
        self.file = None
        
    def __enter__(self):
        print('calling __enter__ method')
        self.file = open(self.name, self.mode)
        return self.file
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('calling __exit__ method')
        if self.file:
            self.file.close()
            
with FileManager('test.txt', 'w') as f:
    print('ready to write to file')
    f.write('hello world')
    
## 输入
calling __init__ method
calling __enter__ method
ready to write to file
calling __exit__ method

须要留神的是,当咱们用类来创立上下文管理器时,必须保障这个类包含办法”__enter__()”和办法“__exit__()”。其中,办法“__enter__()”返回须要被治理的资源,办法“__exit__()”里通常会存在一些开释、清理资源的操作,比方这个例子中的敞开文件等等。

而当咱们用 with 语句,执行这个上下文管理器时:

with FileManager('test.txt', 'w') as f:
    f.write('hello world')

上面这四步操作会顺次产生:

1. 办法“__init__()”被调用,程序初始化对象 FileManager,使得文件名(name)是 ”test.txt”,文件模式 (mode) 是 ’w’;

2. 办法“__enter__()”被调用,文件“test.txt”以写入的模式被关上,并且返回 FileManager 对象赋予变量 f;

3. 字符串“hello world”被写入文件“test.txt”;

4. 办法“__exit__()”被调用,负责敞开之前关上的文件流。

因而,这个程序的输入是:

calling __init__ method
calling __enter__ method
ready to write to file
calling __exit__ meth

另外,值得一提的是,办法“__exit__()”中的参数“exc_type, exc_val, exc_tb”,别离示意 exception_type、exception_value 和 traceback。当咱们执行含有上下文管理器的 with 语句时,如果有异样抛出,异样的信息就会蕴含在这三个变量中,传入办法“__exit__()”。

因而,如果你须要解决可能产生的异样,能够在“__exit__()”增加相应的代码,比方上面这样来写:

class Foo:
    def __init__(self):
        print('__init__ called')
    def __enter__(self):
        print('__enter__ called')
        return self
    def __exit__(self, exc_type, exc_value, exc_tb):
        print('__exit__ called')
        if exc_type:
            print(f'exc_type: {exc_type}')
            print(f'exc_value: {exc_value}')
            print(f'exc_traceback: {exc_tb}')
            print('exception handled')
        return True
    
with Foo() as obj:
    raise Exception('exception raised').with_traceback(None)
# 输入
__init__ called
__enter__ called
__exit__ called
exc_type: <class 'Exception'>
exc_value: exception raised
exc_traceback: <traceback object at 0x1046036c8>
exception handled

这里,咱们在 with 语句中手动抛出了异样“exception raised”,你能够看到,“__exit__()”办法中异样,被顺利捕获并进行了解决。不过须要留神的是,如果办法“__exit__()”没有返回 True,异样依然会被抛出。因而,如果你确定异样曾经被解决了,请在“__exit__()”的最初,加上“return True”这条语句。

基于生成器的上下文管理器

诚然,基于类的上下文管理器,在 Python 中利用宽泛,也是咱们常常看到的模式,不过 Python 中的上下文管理器并不局限于此。除了基于类,它还能够基于生成器实现。接下来咱们来看一个例子。

比方,你能够应用装璜器 contextlib.contextmanager,来定义本人所需的基于生成器的上下文管理器,用以反对 with 语句。还是拿后面的类上下文管理器 FileManager 来说,咱们也能够用上面模式来示意:

from contextlib import contextmanager
@contextmanager
def file_manager(name, mode):
    try:
        f = open(name, mode)
        yield f
    finally:
        f.close()
        
with file_manager('test.txt', 'w') as f:
    f.write('hello world')

这段代码中,函数 file_manager() 是一个生成器,当咱们执行 with 语句时,便会关上文件,并返回文件对象 f;当 with 语句执行完后,finally block 中的敞开文件操作便会执行。

你能够看到,应用基于生成器的上下文管理器时,咱们不再用定义“__enter__()”和“__exit__()”办法,但请务必加上装璜器 @contextmanager,这一点老手很容易忽略。

讲完这两种不同原理的上下文管理器后,还须要强调的是,基于类的上下文管理器和基于生成器的上下文管理器,这两者在性能上是统一的。只不过

  • 基于类的上下文管理器更加 flexible,实用于大型的零碎开发;
  • 而基于生成器的上下文管理器更加不便、简洁,实用于中小型程序。

正文完
 0