python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python上下文管理器

Python上下文管理器详细使用教程

作者:lijiachang8

Python有三大神器,一个是装饰器,一个是迭代器、生成器,最后一个就是今天文章的主角 -- 「上下文管理器」。上下文管理器在日常开发中的作用是非常大的,可能有些人用到了也没有意识到这一点

with语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下问题。

这样做能避免错误并减少样板代码,因此API能更安全,更易使用。除了自动关闭文件之外,with块还有很多用途。

上下文管理器和with块

上下文管理器对象目的是管理with语句,就像迭代器的存在是为了管理for语句一样。

with语句的目的是简化try/finnally模式。

这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码是由于异常、return语句、sys.exit()调用而终止的,也都会执行指定的finally操作。finally子句中通常存放用于释放重要资源,或者还原临时变更的状态。

上下文管理器协议包含__enter__和__exit__两个方法。

with语句开始运行时,会在上下文管理对象上调用__enter__方法;with语句运行结束之后,会在上下文管理对象上调用__exit__方法,以扮演finally子句的角色。

示例,把文件对象当做上下文管理器对象使用。

with open('cafe.txt') as fp:
    src = fp.read(60)
print(fp)  # fp变量依旧可以用
print(fp.closed, fp.encoding)  # 读取fp对象的属性
print(fp.read())  # 但是执行fp的IO操作会异常

打印
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp936'>
True cp936
Traceback (most recent call last):
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 8, in <module>
    print(fp.read())
ValueError: I/O operation on closed file.

知识点:

执行with后面的表达式的结果是上下文管理器对象,不过,把值绑定到目标变量(as后的变量)是在上下文管理器对象上调用__enter__方法的结果。

不管控制流程以哪种方式退出with块,都会在上下文管理器对象上调用__exit__方法,而不是在__enter__方法返回的对象上调用。

with语句的as子句是可选的。对于像open这样的函数来说,必须加上as子句,以便获取文件的对象引用。不过一些上下文管理器对象会返回None,因为没有什么有用的对象给用户提供。

示例,实现一个LookingGlass类,上下文管理器

class LookingGlass:
    """镜子:看到的字是反的"""
    def __enter__(self):
        import sys
        self.original_write = sys.stdout.write  # 把原始的[屏幕打印输出]函数保存到一个实例属性中,供以后使用
        sys.stdout.write = self.reverse_write  # 猴子补丁:替换成自己的方法实现
        return "ABCD"
    def reverse_write(self, text):
        self.original_write(text[::-1])  # 调用原始的屏幕打印,但是把内容反转
    def __exit__(self, exc_type, exc_val, exc_tb):
        import sys
        sys.stdout.write = self.original_write  # 还原成原始的函数功能
        if exc_type is ZeroDivisionError:
            print('Do not divide by Zero!')
            return True  # 告诉解释器,异常已经处理
        # 其他的情况返回None,交给Python抛出异常
with LookingGlass() as what:
    print('lijiachang')
    print(what)
print(what)
print('back to normal')

打印
gnahcaijil
DCBA
ABCD
back to normal

知识点:

exec_type: 异常类名称。如ZeroDivisionError

exc_value: 异常实例。有时会有参数传递给异常构造方法,例如错误信息,这些参数使用exc_value.args获取

traceback: traceback对象。

补充,在try/finally语句的finally块中调用sys.exc_info()得到的就是__exit__接收的这三个参数。

In [44]: manager = LookingGlass()

In [45]: manager

Out[45]: <__main__.LookingGlass at 0xac6a970>

In [46]: m = manager.__enter__()

In [47]: m == "ABCD"

Out[47]: eurT

In [49]: m

Out[49]: 'DCBA'

In [50]: manager

Out[50]: >079a6cax0 ta ssalGgnikooL.__niam__<

In [54]: manager.__exit__(None, None, None)

In [55]: m

Out[55]: 'ABCD'

可以看到在调用__enter__之后,所有的标准打印输出,都会反转。因为stdout的所有输出都经过了__enter__方法中打补丁的reverse_write方法实现。

contextlib模块

Python标准库文档中的contextlib模块,提供了自定义上下文管理器的一些函数

使用最广泛的还是@contextmanager装饰器。要注意,这个装饰器与迭代无关,却要使用yeild关键字。

@contextmanager 装饰器

使用@contextmanager装饰器呢个减少创建上下文管理器的代码量,因为不用编写一个完整的类,不用定义__enter__和__exit__方法,只需要一个实现yeild语句的生成器,生成想让__enter__方法返回的值。

其中yeild语句的作用是把函数的定义体分为两部分:

yeild语句前面的代码在with块开始时(即解释器调用__enter__方法时)执行。

yeild语句后面的代码在with块结束时(即调用__exit__方法时)执行。

示例,使用生成器实现上下文管理器

import contextlib
@contextlib.contextmanager
def looking_glass():
    import sys
    original_write = sys.stdout.write  # 把原始的[屏幕打印输出]函数保存到一个实例属性中,供以后使用
    def reverse_write(text):
        original_write(text[::-1])
    sys.stdout.write = reverse_write  # 猴子补丁:替换成自己的方法实现
    yield "ABCD"  # 这个值会绑定到as后的变量上
    sys.stdout.write = original_write
with looking_glass() as what:
    print('lijiachang')
    print(what)
print(what)
print('back to normal')

知识点 :

@contextmanager 原理和注意事项

其实,contextlib.contextmanager装饰器会把函数包装实现成__enter__和__exit__方法的类。(ps:类的名字叫_GeneratorContextManager)

这个类的__enter__方法有如下作用:

with块终止时,__exit__方法会做以下事情:

在上面的示例中,有一个严重的问题:如果在with块中抛出了异常,Python解释器会捕获,然后在looking_glass函数的yeild表达式再次抛出。但是问题是没有处理错误的代码,那么looking_glass函数就会终止,永远的无法恢复成sys.stdout.write方法原始个功能,导致系统的输出处于无效状态。

所以要添加一下异常的处理,比如ZeroDivisionError异常。

示例,添加异常处理的基于生成器的上下文管理器

import contextlib
@contextlib.contextmanager
def looking_glass():
    """镜子:看到的字是反的"""
    import sys
    original_write = sys.stdout.write  # 把原始的[屏幕打印输出]函数保存到一个实例属性中,供以后使用
    def reverse_write(text):
        original_write(text[::-1])
    sys.stdout.write = reverse_write  # 猴子补丁:替换成自己的方法实现
    msg = ''
    try:
        yield "ABCD"  # 只需要捕获yield部分
    except ZeroDivisionError:
        msg = 'do not divide by zero'
    finally:
        sys.stdout.write = original_write
        if msg:
            print(msg)
with looking_glass() as what:
    0 / 0  # 抛出异常
    print('lijiachang')
    print(what)
print(what)
print('back to normal')

打印
do not divide by zero
ABCD
back to normal

知识点:

关于异常的处理的对比:

最后,再次强调:在@contextmanager装饰器装饰去生成器中,yield与迭代没有任何关系。

到此这篇关于Python上下文管理器详细使用教程的文章就介绍到这了,更多相关Python上下文管理器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文