吃透Python上下文管理器

什么是上下文管理器?

咱们常见的with open操做文件,就是一个上下文管理器。如:html

with open(file, 'rb') as f:
    text = f.read()

那上下文管理器具体的定义是什么呢?python

上下文管理器:是指在一段代码执行以前执行一段代码,用于一些预处理工做;执行以后再执行一段代码,用于一些清理工做git

好比刚提到的文件操做,打开文件进行读写,读写完以后须要将文件关闭。很明显用到了上下文管理器。主要依靠__enter____exit__这两个”魔术方法”实现。github

__enter__(self)数据库

Defines what the context manager should do at the beginning of the block created by the with statement. Note that the return value of enter is bound to the target of the with statement, or the name after the as.app

__exit__(self, exception_type, exception_value, traceback)函数

Defines what the context manager should do after its block has been executed (or terminates). It can be used to handle exceptions, perform cleanup, or do something always done immediately after the action in the block. If the block executes successfully, exception_type, exceptionvalue, and traceback will be None. Otherwise, you can choose to handle the exception or let the user handle it; if you want to handle it, make sure _exit__ returns True after all is said and done. If you don’t want the exception to be handled by the context manager, just let it happen.post

当咱们须要建立一个上下文管理器类型的时候,就须要实现__enter____exit__方法,这对方法称为上下文管理协议(Context Manager Protocol),定义了一种运行时上下文环境。code

基本语法orm

with EXPR as VAR:
    BLOCK

这里就是一个标准的上下文管理器的使用逻辑,其中的运行逻辑:

(1)执行EXPR语句,获取上下文管理器(Context Manager)

(2)调用上下文管理器中的__enter__方法,该方法执行一些预处理工做。

(3)这里的as VAR能够省略,若是不省略,则将__enter__方法的返回值赋值给VAR。

(4)执行代码块BLOCK,这里的VAR能够当作普通变量使用。

(5)最后调用上下文管理器中的的__exit__方法。

(6)__exit__方法有三个参数:exc_type, exc_val, exc_tb。若是代码块BLOCK发生异常并退出,那么分别对应异常的type、value 和 traceback。不然三个参数全为None。

(7)__exit__方法的返回值能够为True或者False。若是为True,那么表示异常被忽视,至关于进行了try-except操做;若是为False,则该异常会被从新raise。

如何本身实现上下文管理器?

简单来讲,若是一个类中,实现了__enter____exit__方法,那么这个类就是上下文管理器。

class Contextor():
    def __enter__(self):    
        print('程序的预处理开始啦!')
        return self     # 做为as说明符指定的变量的值

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('正在进行收尾!')

    def func(self):
        print('程序进行中....')


with Contextor() as var:
    var.func()
   
# 输出
程序的预处理开始啦!
程序进行中....
正在进行收尾!

从这个示例能够很明显的看出,在编写代码时,能够将资源的链接或者获取放在__enter__中,而将资源的关闭写在__exit__ 中。

为何要使用上下文管理器?

在我看来,这和 Python 崇尚的优雅风格有关。

  1. 能够以一种更加优雅的方式,操做(建立/获取/释放)资源,如文件操做、数据库链接;
  2. 能够以一种更加优雅的方式,处理异常;

第二种,会被大多数人所忽略。这里着重介绍下。

在处理异常时,一般都是使用try...except...来进行异常处理的。这就可能会出如今程序的主逻辑中有大量的异常处理代码,这会大大影响程序的可读性。

好的作法能够经过with将异常处理隐藏起来。

咱们以1/0举例(1/0必然会抛出错误)

class Resource():
    def __enter__(self):
        print('===connect to resource===')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('===close resource connection===')
        return True

    def operate(self):
        1/0

with Resource() as res:
    res.operate()
    
# 输出结果
===connect to resource===
===close resource connection===

运行发现,并无出异常。

这就是上下文管理器的强大之处,异常能够在__exit__进行捕获,并本身决定如何处理。在__exit__ 里返回 True(没有return 就默认为 return False),就至关于告诉 Python解释器,这个异常咱们已经捕获了,不须要再往外抛了。

在 写__exit__ 函数时,须要注意的事,它必需要有这三个参数:

  • exc_type:异常类型
  • exc_val:异常值
  • exc_tb:异常的错误栈信息

当主逻辑代码没有报异常时,这三个参数将都为None。

如何处理自行处理异常

咱们以上面的代码为例,在__exit__加入判断异常的逻辑,若是发生异常,则打印异常信息。

class Resource():
    def __enter__(self):
        print('===connect to resource===')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            print(f'出现异常:{exc_type}:{exc_val}')
        print('===close resource connection===')
        return True

    def operate(self):
        1/0

with Resource() as res:
    res.operate()
    
# 输出
===connect to resource===
出现异常:<class 'ZeroDivisionError'>:division by zero
===close resource connection===

如何更好的使用上下文管理器?

在Python中有一个专门用于实现上下文管理的标准库contextlib

有了 contextlib 建立上下文管理的最好方式就是使用 contextmanager 装饰器,经过 contextmanager 装饰一个生成器函数,yield 语句前面的部分被认为是__enter__() 方法的代码,后面的部分被认为是 __exit__()方法的代码。

咱们以打开文件为例:

import contextlib

@contextlib.contextmanager
def open_func(file_name):
    # __enter__方法
    print('open file:', file_name, 'in __enter__')
    file_handler = open(file_name, 'r')
    
    # 【重点】:yield
    yield file_handler

    # __exit__方法
    print('close file:', file_name, 'in __exit__')
    file_handler.close()
    return

with open_func('/Users/MING/mytest.txt') as file_in:
    for line in file_in:
        print(line)

若是要处理异常,将上面代码改写成下面的样子。

import contextlib

@contextlib.contextmanager
def open_func(file_name):
    # __enter__方法
    print('open file:', file_name, 'in __enter__')
    file_handler = open(file_name, 'r')

    try:
        yield file_handler
    except Exception as exc:
        # deal with exception
        print('the exception was thrown')
    finally:
        print('close file:', file_name, 'in __exit__')
        file_handler.close()

        return

with open_func('test.txt') as file_in:
    for line in file_in:
        1/0
        print(line)

参考

https://juejin.im/post/5c87b165f265da2dac4589cc
https://www.cnblogs.com/linxiyue/p/3855751.html
https://runnerliu.github.io/2018/01/02/pythoncontextmanager/

扫描下面二维码,关注公众号, 每周按期与您分享原创的、有深度的Python知识点
在这里插入图片描述

相关文章
相关标签/搜索