我想你对 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__ methodcalling __enter__ methodready to write to filecalling __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__ methodcalling __enter__ methodready to write to filecalling __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__ calledexc_type: <class 'Exception'>exc_value: exception raisedexc_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@contextmanagerdef 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,实用于大型的零碎开发;
  • 而基于生成器的上下文管理器更加不便、简洁,实用于中小型程序。