深入学习Python中的上下文管理器与else块


Posted in Python onAugust 27, 2017

前言

本文主要个大家介绍了关于Python上下文管理器与else块的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。

在开始之前,我们先来看看下面这段话:

最终,上下文管理器可能几乎与子程序(subroutine)本身一样重要。目前,我们只了解了上下文管理器的皮毛……Basic 语言有with 语句,而且很多语言都有。但是,在各种语言中 with 语句的作用不同,而且做的都是简单的事,虽然可以避免不断使用点号查找属性,但是不会做事前准备和事后清理。不要觉得名字一样,就意味着作用也一样。with 语句是非常了不起的特性。

——Raymond Hettinger

雄辩的 Python 布道者

先做这个,再做那个:if语句之外的else块

这个语言特性不是什么秘密,但却没有得到重视:else 子句不仅能在if 语句中使用,还能在 for、while 和 try 语句中使用。for/else、while/else 和 try/else 的语义关系紧密,不过与if/else 差别很大。起初,else 这个单词的意思阻碍了我对这些特性的理解,但是最终我习惯了。

else 子句的行为如下:

for

仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止)才运行 else 块。

while

仅当 while 循环因为条件为假值而退出时(即 while 循环没有被break 语句中止)才运行 else 块。

try

仅当 try 块中没有异常抛出时才运行 else 块。官方文档(https://docs.python.org/3/reference/compound_stmts.html)还指出:“else 子句抛出的异常不会由前面的 except 子句处理。”

注意:

在所有情况下,如果异常或者 return、break 或 continue 语句导致控制权跳到了复合语句的主块之外,else 子句也会被跳过。

在这些语句中使用 else 子句通常能让代码更易于阅读,而且能省去一些麻烦,不用设置控制标志或者添加额外的 if 语句。

在循环中使用 else 子句的方式如下述代码片段所示:

for item in my_list:
  if item.flavor == 'banana':
   break
  else:
   raise ValueError('No banana flavor found!')

一开始,你可能觉得没必要在 try/except 块中使用 else 子句。毕竟,在下述代码片段中,只有 dangerous_call() 不抛出异常,after_call() 才会执行,对吧?

try:
  dangerous_call()
  after_call()
 except OSError:
  log('OSError...')

然而,after_call() 不应该放在 try 块中。为了清晰和准确,try 块中应该只抛出预期异常的语句。因此,像下面这样写更好:

try:
  dangerous_call()
 except OSError:
  log('OSError...')
 else:
  after_call()

现在很明确,try 块防守的是 dangerous_call() 可能出现的错误,而不是 after_call() 。而且很明显,只有 try 块不抛出异常,才会执行after_call()

上下文管理器和with块

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

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

上下文管理器协议包含 __enter__ 和 __exit__ 两个方法。with 语句开始运行时,会在上下文管理器对象上调用 __enter__ 方法。with 语句运行结束后,会在上下文管理器对象上调用 __exit__ 方法,以此扮演 finally 子句的角色。

? 演示把文件对象当成上下文管理器使用

>>> with open('mirror.py') as fp: # fp绑定到打开的文件上,因为文件的__enter__方法返回self
...   src = fp.read(60) # 从fp中读取一些数据
...
>>> len(src)
>>> fp # fp变量依然可以使用
<_io.TextIOWrapper name='mirror.py' mode='r' encoding='UTF-8'>
>>> fp.closed, fp.encoding # 可以读取fp对象的属性
(True, 'UTF-8')
>>> fp.read(60) # 但是不能在fp上执行I/O操作,因为在with块的结尾,调用了TextIOWrappper.__exit__方法把文件关闭了
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.

测试 LookingGlass 上下文管理器类

>>> from mirror import LookingGlass
>>> with LookingGlass() as what: # 上下文管理器是LookingGlass类的实例;Python在上下文管理器上调用__enter__方法,把返回结果绑定在what上
...  print('Alice, Kitty and Snowdrop') # 打印一个字符串,然后打印what变量的值
...  print(what)
...
pordwonS dna yttiK ,ecilA # 打印出的内容是反向的
YKCOWREBBAJ
>>> what # 现在,with块已经执行完毕,可以看出,__enter__方法返回的值,即存储在what变量中的值是字符串'JABBERWOCKY'
'JABBERWOCKY'
>>> print('Back to normal.') # 输出不在是反向的了
Back to normal.

mirror.py:LookingGlass 上下文管理器类的代码

class LookingGlass:

 def __enter__(self):      # 除了 self 之外,Python调用__enter__方法时不窜入其他参数
  import sys
  self.original_write = sys.stdout.write # 把原来的 sys.stdout.write 方法保存在一个实例属性中,供后面使用
  sys.stdout.write = self.reverse_write # 为 sys.stdout.write 打猴子补丁,替换成自己编写的方法
  return 'JABBERWOCKY'     # 返回 'JABBERWOCKY' 字符串,这样才有内容存入目标变量 what

 def reverse_write(self, text):    # 这是用于取代 sys.stdout.write 的方法,把 text 参数的内容反转,然后调用原来的方法实现
  return self.original_write(text[::-1])


 def __exit__(self, exc_type, exc_val, traceback): # 如果一切正常,Python会调用__exit__方法传入的参数是三个None,如果抛出异常,则三个参数是异常的数据
  import sys
  sys.stdout.write = self.original_write # 还原成原来的sys.studout.write方法
  if exc_type is ZeroDivisionError:  # 如果有异常,而且是 ZeroDivisionError 类型,打印一个消息
   print('Please DO NOT divide by zero!') 
   return True       # 然后返回 True,告诉解释器,异常已经处理

解释器调用 __enter__ 方法时,除了隐式的 self 之外,不会传入任何参数。传给 __exit__ 方法的三个参数列举如下。

exc_type

异常类(例如 ZeroDivisionError)

exc_value

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

traceback

traceback 对象

在 with 块之外使用 LookingGlass 类

>>> from mirror import LookingGlass
>>> manager = LookingGlass() # 实例化并审查manager实例,等同于 with LookingGlass() as manager
>>> manager
<mirror.LookingGlass object at 0x2a578ac>
>>> monster = manager.__enter__() # 在上下文管理器中调用__enter__()方法,把结果存储在monster中
>>> monster == 'JABBERWOCKY' # monster的值是字符串'JABBERWOCKY',打印出来的True标识符是反向,因为用了猴子补丁
eurT
>>> monster
'YKCOWREBBAJ'
>>> manager
>ca875a2x0 ta tcejbo ssalGgnikooL.rorrim<
>>> manager.__exit__(None, None, None) # 调用manager.__exit__,还原成之前的stdout.write
>>> monster
'JABBERWOCKY'

contextlib模块中的实用工具

closing

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

suppress

构建临时忽略指定异常的上下文管理器。

@contextmanager

这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了。

ContextDecorator

这是个基类,用于定义基于类的上下文管理器。这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数。

ExitStack

这个上下文管理器能进入多个上下文管理器。with 块结束时,ExitStack 按照后进先出的顺序调用栈中各个上下文管理器的__exit__ 方法。如果事先不知道 with 块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意一个文件列表中的所有文件。

使用@contextmanager

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

在使用 @contextmanager 装饰的生成器中,yield 语句的作用是把函数的定义体分成两部分:yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__ 方法时)执行, yield 语句后面的代码在with 块结束时(即调用 __exit__ 方法时)执行。

mirror_gen.py:使用生成器实现的上下文管理器

import contextlib


@contextlib.contextmanager    # 应用 contextmanager 装饰器
def looking_glass():
 import sys
 original_write = sys.stdout.write # 贮存原来的 sys.stdout.write 方法

 def reverse_write(text):   # 定义自定义的 reverse_write 函数;在闭包中可以访问 original_write
  original_write(text[::-1])

 sys.stdout.write = reverse_write # 把 sys.stdout.write 替换成 reverse_write
 yield 'JABBERWOCKY'     # 产出一个值,这个值会绑定到 with 语句中 as 子句的目标变量上
 sys.stdout.write = original_write # 控制权一旦跳出 with 块,继续执行 yield 语句之后的代码;这里是恢复成原来的 sys. stdout.write 方法

with looking_glass() as what:   # 直接通过上下文管理器实现with的功能
 print('Alice, Kitty and Snowdrop')
 print(what)

print(what)

以上代码执行的结果为:

pordwonS dna yttiK ,ecilA
YKCOWREBBAJ
JABBERWOCKY

其实,contextlib.contextmanager 装饰器会把函数包装成实现__enter__ 和 __exit__ 方法的类

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

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

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

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

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

(1) 检查有没有把异常传给 exc_type;如果有,调用gen.throw(exception) , 在生成器函数定义体中包含 yield 关键字的那一行抛出异常。

(2) 否则,调用 next(gen) ,继续执行生成器函数定义体中 yield 语句之后的代码。

注意:  

上面的 ? 有一个严重的错误:如果在 with 块中抛出了异常,Python 解释器会将其捕获,然后在 looking_glass 函数的 yield 表达式里再次抛出。但是,那里没有处理错误的代码,因此 looking_glass 函数会中止,永远无法恢复成原来的 sys.stdout.write 方法,导致系统处于无效状态。

mirror_gen_exc.py:基于生成器的上下文管理器,而且实现了异常处理

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 'JABBERWOCKY'
 except ZeroDivisionError:    #处理 ZeroDivisionError 异常,设置一个错误消息
  msg = 'Please DO NOT divide by zero!'
 finally:
  sys.stdout.write = original_write # 撤销对 sys.stdout.write 方法所做的猴子补丁
  if msg:
   print(msg)      # 如果设置了错误消息,把它打印出来

注意:

使用 @contextmanager 装饰器时,要把 yield 语句放在try/finally 语句中(或者放在 with 语句中),这是无法避免的,因为我们永远不知道上下文管理器的用户会在 with 块中做什么。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
Python算法应用实战之队列详解
Feb 04 Python
Python实现JSON反序列化类对象的示例
Jan 31 Python
Python实现Event回调机制的方法
Feb 13 Python
基于PyQt4和PySide实现输入对话框效果
Feb 27 Python
谈谈Python中的while循环语句
Mar 10 Python
使用Pandas对数据进行筛选和排序的实现
Jul 29 Python
详解Python并发编程之创建多线程的几种方法
Aug 23 Python
keras 特征图可视化实例(中间层)
Jan 24 Python
使用python实现CGI环境搭建过程解析
Apr 28 Python
Django contrib auth authenticate函数源码解析
Nov 12 Python
Python高并发和多线程有什么关系
Nov 14 Python
python实现简单区块链结构
Apr 25 Python
python利用MethodType绑定方法到类示例代码
Aug 27 #Python
Python中使用haystack实现django全文检索搜索引擎功能
Aug 26 #Python
python读取excel表格生成erlang数据
Aug 26 #Python
使用Python实现简单的服务器功能
Aug 25 #Python
详解Python实现多进程异步事件驱动引擎
Aug 25 #Python
python基础while循环及if判断的实例讲解
Aug 25 #Python
itchat和matplotlib的结合使用爬取微信信息的实例
Aug 25 #Python
You might like
一个php导出oracle库的php代码
2009/04/20 PHP
php使用多个进程同时控制文件读写示例
2014/02/28 PHP
PhpDocumentor 2安装以及生成API文档的方法
2014/05/21 PHP
JavaScript中令你抓狂的魔术变量
2006/11/30 Javascript
不错的JS中变量相关的细节分析
2007/08/13 Javascript
JavaScript Event学习第四章 传统的事件注册模型
2010/02/07 Javascript
jQuery的写法不同导致的兼容性问题的解决方法
2010/07/29 Javascript
详谈 Jquery Ajax异步处理Json数据.
2011/09/09 Javascript
web css实现整站样式互相切换
2013/10/29 Javascript
js获取form的方法
2015/05/06 Javascript
数据结构中的各种排序方法小结(JS实现)
2016/07/23 Javascript
canvas学习之API整理笔记(二)
2016/12/29 Javascript
Vue监听数据对象变化源码
2017/03/09 Javascript
解决webpack+Vue引入iView找不到字体文件的问题
2018/09/28 Javascript
微信小程序点击图片实现长按预览、保存、识别带参数二维码、转发等功能
2019/07/20 Javascript
vue自定义switch开关组件,实现样式可自行更改
2019/11/01 Javascript
AutoJs实现刷宝短视频的思路详解
2020/05/22 Javascript
[03:12]完美世界DOTA2联赛PWL DAY6集锦
2020/11/05 DOTA
Python 2.7.x 和 3.x 版本的重要区别小结
2014/11/28 Python
VSCode Python开发环境配置的详细步骤
2019/02/22 Python
Python及Pycharm安装方法图文教程
2019/08/05 Python
python读取图像矩阵文件并转换为向量实例
2020/06/18 Python
css3的过滤效果简单实例
2016/08/03 HTML / CSS
借助HTML5 Canvas来绘制三角形和矩形等多边形的方法
2016/03/14 HTML / CSS
html5通过canvas实现刮刮卡效果示例分享
2014/01/27 HTML / CSS
西班牙多品牌鞋店连锁店:Krack
2018/11/30 全球购物
中国跨镜手机配件批发在线商店:TVC-Mall
2019/08/20 全球购物
俄罗斯最大的在线手表商店:Bestwatch.ru
2020/01/11 全球购物
家长寄语大全
2014/04/02 职场文书
《冬阳童年骆驼队》教学反思
2014/04/15 职场文书
超市开业庆典策划方案
2014/05/14 职场文书
课外科技活动总结
2014/08/27 职场文书
就业意向协议书
2015/01/29 职场文书
水知道答案观后感
2015/06/08 职场文书
python爬虫selenium模块详解
2021/03/30 Python
Python字符串对齐方法使用(ljust()、rjust()和center())
2021/04/26 Python