深入学习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读取Android permission文件
Nov 01 Python
Python新手入门最容易犯的错误总结
Apr 24 Python
python实现日常记账本小程序
Mar 10 Python
Python 利用内置set函数对字符串和列表进行去重的方法
Jun 29 Python
Python项目 基于Scapy实现SYN泛洪攻击的方法
Jul 23 Python
python gdal安装与简单使用
Aug 01 Python
关于PyTorch 自动求导机制详解
Aug 18 Python
python微信公众号开发简单流程实现
Mar 09 Python
浅谈python 调用open()打开文件时路径出错的原因
Jun 05 Python
完美解决keras保存好的model不能成功加载问题
Jun 11 Python
mac系统下安装pycharm、永久激活、中文汉化详细教程
Nov 24 Python
Matplotlib绘制条形图的方法你知道吗
Mar 21 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
JAVA/JSP学习系列之七
2006/10/09 PHP
php 图片上传类代码
2009/07/17 PHP
Linux环境下php实现给网站截图的方法
2016/05/03 PHP
PHP中文字符串截断无乱码解决方法
2016/10/10 PHP
基于php中echo用逗号和用点号的区别详解
2018/01/23 PHP
In Javascript Class, how to call the prototype method.(three method)
2007/01/09 Javascript
再次分享18个非常棒的jQuery表格插件
2011/04/10 Javascript
json格式数据的添加,删除及排序方法
2016/01/21 Javascript
jquery实现文本框textarea自适应高度
2016/03/09 Javascript
js实现当鼠标移到表格上时显示这一格全部内容的代码
2016/06/12 Javascript
Javascript将双字节字符转换成单字节字符并计算长度
2016/06/22 Javascript
微信小程序 保留小数(toFixed)详细介绍
2016/11/16 Javascript
Linux系统中利用node.js提取Word(doc/docx)及PDF文本的内容
2017/06/17 Javascript
vue实现通讯录功能
2018/07/14 Javascript
vue 自定义右键样式的实例代码
2019/11/06 Javascript
vue3.0中使用postcss-pxtorem的具体方法
2019/11/20 Javascript
vue微信分享插件使用方法详解
2020/02/18 Javascript
详解JavaScript 异步编程
2020/07/13 Javascript
[00:11]战神迅矛
2019/03/06 DOTA
python时间日期函数与利用pandas进行时间序列处理详解
2018/03/13 Python
Python3实现的Mysql数据库操作封装类
2018/06/06 Python
Python文本处理简单易懂方法解析
2019/12/19 Python
基于HTML5 Canvas 实现弹出框效果
2017/06/05 HTML / CSS
美国修容界大佬创建的个人美妆品牌:Kevyn Aucoin Beauty
2018/12/12 全球购物
利用指针变量实现队列的入队操作
2012/04/07 面试题
自荐书4要点
2014/01/25 职场文书
六一儿童节活动策划方案
2014/01/27 职场文书
怎样写好创业计划书的内容
2014/02/06 职场文书
国庆节促销广告语2014
2014/09/19 职场文书
导游欢送词
2015/01/31 职场文书
健康教育主题班会
2015/08/14 职场文书
Python数据类型最全知识总结
2021/05/31 Python
Vue3.0写自定义指令的简单步骤记录
2021/06/27 Vue.js
Python OpenCV形态学运算示例详解
2022/04/07 Python
如何Python使用re模块实现okenizer
2022/04/30 Python
解决Oracle数据库用户密码过期
2022/05/11 Oracle