亲宝软件园·资讯

展开

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

lijiachang8 人气:0

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与迭代没有任何关系。

加载全部内容

相关教程
猜你喜欢
用户评论