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

作者:lijiachang8 时间:2021-06-24 05:47:35 

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.

知识点:

  • fp变量在上下文管理器之外,依旧存在,可以读取fp对象属性。因为with块于函数和模块不同,没有定义新的作用域。

  • 但是 不能在fp上再执行IO操作,因为在with块的末尾,已经调用了TextIOWrapper__exit__方法把文件关闭了。

执行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

知识点:

  • sys.stdout.write 是标准屏幕打印输出。要注意如果暂时缓存其他对象改变功能,记得最后还原成原来的版本。

  • Python调用__enter__方法时,除了self之外不会传入其他参数

  • 如果一切正常,Python调用__exit__方法时,传入的是None,None,None;如果抛出了异常,这三个参数是异常数据。如下:

exec_type: 异常类名称。如ZeroDivisionError

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

traceback: traceback对象。

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

  • 在__exit__中返回True是告诉解释器,异常已经处理了。如果返回True之外的值,比如默认的None,with块中的异常会向上冒泡,让Python来抛出。

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模块,提供了自定义上下文管理器的一些函数

  • closing :如果对象提供了close()方法,但没有实现__enter__/__exit__方法,可以使用这个函数来构建上下文管理器。

  • suppress : 构建临时忽略指定异常的上下文。

  • @contextmanager :这个装饰器可以把简单的生成器变为上下文管理器,这样就不需要创建类来实现管理器协议了。

  • ContextDecorate :这是个基类,用于编写类可以继承他,用于定义基于类的上下文管理器。也可以用于装饰器函数,在受管理的上下文中运行整个函数。

  • ExitStack : 这个上下文管理器能进入多个上下文管理器。with块结束时,按照后进先出的顺序调用栈中各个上下文管理器的__exit__方法。如果事先不知道 with块要进入多少个上下文管理器,可以使用这个类。

使用最广泛的还是@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')

知识点 :

  • yield 后面的值会绑定到with语句中as子句的目标变量上,执行with块中的代码,这个函数会在这里暂停。

  • 控制权一旦调成with块,就会继续执行yeild语句后的代码

@contextmanager 原理和注意事项

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

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

  • 调用生成器函数,保存生成器对象(这里把他称为gen)。

  • 调用next(gen),执行到yeild关键字所在的位置。

  • 返回next(gen)产出的值,把产出的值绑定到with/as语句的目标变量上。

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

  • 检查有没有异常传给exc_type:

  • 如果有,就调用gen.throw(exception), 在生成器函数定义体中包含yeild关键字的那一行抛出异常。

  • 如果没有异常,再次调用next(gen),继续执行定义体中yeild语句之后的代码。

在上面的示例中,有一个严重的问题:如果在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

知识点:

  • 在生成器函数中只需要捕获yeild关键字这行的异常,Python解释器会在with块中的异常,转移到yield这行抛出。

  • 所以在使用@contextmanager装饰器时,要把yeild语句放到try/finally语句中,因为我们永远不知道上下文管理器的用户会在with块中做什么。

关于异常的处理的对比:

  • 在用类实现上下文管理器时,前面说过,为了告诉解释器异常已经处理过了,需要在__exit__方法中返回True,此时解释器会压制异常。如果__exit__没有显示的返回一个值,那么解释器得到的就是None,此时会向上冒泡异常。

  • 在用@contextmanager装饰器时,默认的行为是相反的:装饰器提供的__exit__方法假定发给生成器的所有异常都已经处理了,因此默认压制异常。如果不想让contextmanager压制异常,必须在装饰的函数中显式的重新抛出异常。

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

来源:https://blog.csdn.net/lijiachang8/article/details/128825353

标签:Python,上下文,管理
0
投稿

猜你喜欢

  • IE的button元素bug

    2009-12-08 12:39:00
  • python IP地址转整数

    2023-06-08 22:38:25
  • FSO组件之文件操作(上)

    2010-05-03 11:01:00
  • 深入研究PHP中的preg_replace和代码执行

    2023-11-22 07:36:55
  • 如何往SQL Server数据库传递日期数据?

    2010-06-08 09:29:00
  • 详解php实现页面静态化原理

    2023-10-25 10:58:10
  • golang定时器和超时的使用详解

    2023-07-23 21:35:12
  • 将内容自动添加到指定文本域的JS

    2007-10-08 12:50:00
  • [关注细节的最佳方案]有效期时间格式的展现

    2009-10-30 18:51:00
  • XHTML 1.0:标记新的开端

    2023-08-04 21:59:30
  • Javascript语法检查插件 jsLint for Vim

    2009-03-11 16:37:00
  • 美图秀秀web开放平台--PHP流式上传和表单上传示例分享

    2023-11-15 08:34:42
  • python opencv 检测移动物体并截图保存实例

    2023-11-05 16:10:35
  • JS打开新窗口的2种方式

    2023-07-07 02:44:49
  • Javascript世界的最大整数值

    2008-06-23 13:23:00
  • 用画为5.12地震受灾同胞们祈福

    2008-05-20 12:08:00
  • PHP中SimpleXML函数用法分析

    2023-06-23 11:52:09
  • Golang利用casbin实现权限验证详解

    2023-08-06 23:18:45
  • sql server中通过查询分析器实现数据库的备份与恢复方法分享

    2012-05-22 18:41:58
  • 将mysql转换到oracle必须了解的50件事

    2010-07-05 12:15:00
  • asp之家 网络编程 m.aspxhome.com