深入学习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设计模式之访问者模式
Nov 18 Python
Python多线程扫描端口代码示例
Feb 09 Python
pandas数值计算与排序方法
Apr 12 Python
pandas 使用apply同时处理两列数据的方法
Apr 20 Python
Python3用tkinter和PIL实现看图工具
Jun 21 Python
Win10下python3.5和python2.7环境变量配置教程
Sep 18 Python
学Python 3的理由和必要性
Nov 19 Python
解决python web项目意外关闭,但占用端口的问题
Dec 17 Python
python opencv实现信用卡的数字识别
Jan 12 Python
python实现的分层随机抽样案例
Feb 25 Python
基于Keras 循环训练模型跑数据时内存泄漏的解决方式
Jun 11 Python
python 实现图片特效处理
Apr 03 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完整的日历类(CLASS)
2006/11/27 PHP
php多层数组与对象的转换实例代码
2013/08/05 PHP
php上传文件,创建递归目录的实例代码
2013/10/18 PHP
PHP字符串和十六进制如何实现互相转换
2020/07/16 PHP
JQuery.ajax传递中文参数的解决方法 推荐
2011/03/28 Javascript
根据json字符串生成Html的一种方式
2013/01/09 Javascript
js控制的回到页面顶端goTop的代码实现
2013/03/20 Javascript
第一次接触神奇的Bootstrap导航条
2016/08/09 Javascript
微信小程序 跳转方式总结
2017/04/20 Javascript
vue+elementUI实现表单和图片上传及验证功能示例
2019/05/14 Javascript
layui点击弹框页面 表单请求的方法
2019/09/21 Javascript
layUI使用layer.open,在content打开数据表格,获取值并返回的方法
2019/09/26 Javascript
如何正确解决VuePress本地访问出现资源报错404的问题
2020/12/03 Vue.js
[01:12:35]Spirit vs Navi Supermajor小组赛 A组败者组第一轮 BO3 第二场 6.2
2018/06/03 DOTA
python常用函数与用法示例
2019/07/02 Python
深入浅析python3中的unicode和bytes问题
2019/07/03 Python
Python 中的pygame安装与配置教程详解
2020/02/10 Python
在keras里实现自定义上采样层
2020/06/28 Python
jupyter notebook指定启动目录的方法
2021/03/02 Python
使用CSS3实现字体颜色渐变的实现
2020/08/10 HTML / CSS
美国玩具公司:U.S.Toy
2018/05/19 全球购物
澳大利亚体育和露营装备在线/实体零售商:Find Sports
2020/06/03 全球购物
Lentiamo丹麦:购买便宜的隐形眼镜
2021/01/13 全球购物
Strathberry苏贝瑞中国官网:西班牙高级工匠手工打造
2020/10/19 全球购物
介绍一下sql server的安全性
2014/08/10 面试题
linux面试题参考答案(1)
2016/01/22 面试题
夜大毕业自我鉴定
2013/10/11 职场文书
大学生村官心得体会范文
2014/01/04 职场文书
优秀大学生职业生涯规划书
2014/02/27 职场文书
降消项目实施方案
2014/03/30 职场文书
部门群众路线教育实践活动对照检查材料思想汇报
2014/10/07 职场文书
审计局2014法制宣传日活动总结
2014/11/01 职场文书
2014年乡镇民政工作总结
2014/12/02 职场文书
运动会通讯稿50字
2015/07/20 职场文书
甜美蛋糕店的创业计划书模板,拿来即用!
2019/08/21 职场文书
go语言中切片与内存复制 memcpy 的实现操作
2021/04/27 Golang