Python标准模块--ContextManager上下文管理器的具体用法


Posted in Python onNovember 27, 2017

写代码时,我们希望把一些操作放到一个代码块中,这样在代码块中执行时就可以保持在某种运行状态,而当离开该代码块时就执行另一个操作,结束当前状态;所以,简单来说,上下文管理器的目的就是规定对象的使用范围,如果超出范围就采取“处理”。

这一功能是在Python2.5之后引进的,它的优势在于可以使得你的代码更具可读性,且不容易出错。

1 模块简介

在数年前,Python 2.5 加入了一个非常特殊的关键字,就是with。with语句允许开发者创建上下文管理器。什么是上下文管理器?上下文管理器就是允许你可以自动地开始和结束一些事情。例如,你可能想要打开一个文件,然后写入一些内容,最后再关闭文件。这或许就是上下文管理器中一个最经典的示例。事实上,当你利用with语句打开一个文件时,Python替你自动创建了一个上下文管理器。

with open("test/test.txt","w") as f_obj:
  f_obj.write("hello")

如果你使用的是Python 2.4,你不得不以一种老的方式来完成这个任务。

f_obj = open("test/test.txt","w")
f_obj.write("hello")
f_obj.close()

上下文管理器背后工作的机制是使用Python的方法:__enter__和__exit__。让我们尝试着去创建我们的上下文管理器,以此来了解上下文管理器是如何工作的。

2 模块使用

2.1 创建一个上下文管理器类

与其继续使用Python打开文件这个例子,不如我们创建一个上下文管理器,这个上下文管理器将会创建一个SQLite数据库连接,当任务处理完毕,将会将其关闭。下面就是一个简单的示例。

import sqlite3

class DataConn:
  def __init__(self,db_name):
    self.db_name = db_name

  def __enter__(self):
    self.conn = sqlite3.connect(self.db_name)
    return self.conn

  def __exit__(self,exc_type,exc_val,exc_tb):
    self.conn.close()
    if exc_val:
      raise

if __name__ == "__main__":
  db = "test/test.db"
  with DataConn(db) as conn:
    cursor = conn.cursor()

在上述代码中,我们创建了一个类,获取到SQLite数据库文件的路径。__enter__方法将会自动执行,并返回数据库连接对象。现在我们已经获取到数据库连接对象,然后我们创建光标,向数据库写入数据或者对数据库进行查询。当我们退出with语句的时候,它将会调用__exit__方法用于执行和关闭这个连接。

让我们使用其它的方法来创建上下文管理器。

2.2 利用contextlib创建一个上下文管理器

Python 2.5 不仅仅添加了with语句,它也添加了contextlib模块。这就允许我们使用contextlib的contextmanager函数作为装饰器,来创建一个上下文管理器。让我们尝试着用它来创建一个上下文管理器,用于打开和关闭文件。

from contextlib import contextmanager

@contextmanager
def file_open(path):
  try:
    f_obj = open(path,"w")
    yield f_obj
  except OSError:
    print("We had an error!")
  finally:
    print("Closing file")
    f_obj.close()

if __name__ == "__main__":
  with file_open("test/test.txt") as fobj:
    fobj.write("Testing context managers")

在这里,我们从contextlib模块中引入contextmanager,然后装饰我们所定义的file_open函数。这就允许我们使用Python的with语句来调用file_open函数。在函数中,我们打开文件,然后通过yield,将其传递出去,最终主调函数可以使用它。

一旦with语句结束,控制就会返回给file_open函数,它继续执行yield语句后面的代码。这个最终会执行finally语句--关闭文件。如果我们在打开文件时遇到了OSError错误,它就会被捕获,最终finally语句依然会关闭文件句柄。

contextlib.closing(thing)

contextlib模块提供了一些很方便的工具。第一个工具就是closing类,一旦代码块运行完毕,它就会将事件关闭。Python官方文档给出了类似于以下的一个示例,

>>> from contextlib import contextmanager
>>> @contextmanager
... def closing(db):
...   try:
...     yield db.conn()
...   finally:
...     db.close()

在这段代码中,我们创建了一个关闭函数,它被包裹在contextmanager中。这个与closing类相同。区别就是,我们可以在with语句中使用closing类本身,而非装饰器。让我们看如下的示例,

>>> from contextlib import closing
>>> from urllib.request import urlopen
>>> with closing(urlopen("http://www.google.com")) as webpage:
...   for line in webpage:
...     pass

在这个示例中,我们在closing类中打开一个url网页。一旦我们运行完毕with语句,指向网页的句柄就会关闭。

contextlib.suppress(*exceptions)

另一个工具就是在Python 3.4中加入的suppress类。这个上下文管理工具背后的理念就是它可以禁止任意数目的异常。假如我们想忽略FileNotFoundError异常。如果你书写了如下的上下文管理器,那么它不会正常运行。

>>> with open("1.txt") as fobj:
...   for line in fobj:
...     print(line)
...
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '1.txt'

正如你所看到的,这个上下文管理器没有处理这个异常,如果你想忽略这个错误,你可以按照如下方式来做,

>>> from contextlib import suppress
>>> with suppress(FileNotFoundError):
...   with open("1.txt") as fobj:
...     for line in fobj:
...       print(line)

在这段代码中,我们引入suppress,然后将我们要忽略的异常传递给它,在这个例子中,就是FileNotFoundError。如果你想运行这段代码,你将会注意到,文件不存在时,什么事情都没有发生,也没有错误被抛出。请注意,这个上下文管理器是可重用的,2.4章节将会具体解释。

contextlib.redirect_stdout/redirect_stderr

contextlib模块还有一对用于重定向标准输出和标准错误输出的工具,分别在Python 3.4 和3.5 中加入。在这些工具被加入之前,如果你想对标准输出重定向,你需要按照如下方式操作,

import sys
path = "test/test.txt"

with open(path,"w") as fobj:
  sys.stdout = fobj
  help(sum)

利用contextlib模块,你可以按照如下方式操作,

from contextlib import redirect_stdout

path = "test/test.txt"

with open(path,"w") as fobj:
  with redirect_stdout(fobj):
    help(redirect_stdout)

在上面两个例子中,我们均是将标准输出重定向到一个文件。当我们调用Python的help函数,不是将信息输出到标准输出上,而是将信息保存到重定向的文件中。你也可以将标准输出重定向到缓存或者从用接口如Tkinter或wxPython中获取的文件控制类型上。

2.3 ExitStack

ExitStack是一个上下文管理器,允许你很容易地与其它上下文管理结合或者清除。这个咋听起来让人有些迷糊,我们来看一个Python官方文档的例子,或许会让我们更容易理解它。

>>> from contextlib import ExitStack
>>> filenames = ["1.txt","2.txt"]
>>> with ExitStack as stack:
...   file_objects = [stack.enter_context(open(filename)) for filename in filenames]

这段代码就是在列表中创建一系列的上下文管理器。ExitStack维护一个寄存器的栈。当我们退出with语句时,文件就会关闭,栈就会按照相反的顺序调用这些上下文管理器。

Python官方文档中关于contextlib有很多示例,你可以学习到如下的技术点:

  1. 从__enter__方法中捕获异常
  2. 支持不定数目的上下文管理器
  3. 替换掉try-finally
  4. 其它

2.4 可重用的上下文管理器

大部分你所创建的上下文管理器仅仅只能在with语句中使用一次,示例如下:

>>> from contextlib import contextmanager
>>> @contextmanager
... def single():
...   print("Yielding")
...   yield
...   print("Exiting context manager")
...
>>> context = single()
>>> with context:
...   pass
...
Yielding
Exiting context manager
>>> with context:
...   pass
...
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "/usr/lib/python3.4/contextlib.py", line 61, in __enter__
  raise RuntimeError("generator didn't yield") from None
RuntimeError: generator didn't yield

在这段代码中,我们创建了一个上下文管理器实例,并尝试着在Python的with语句中运行两次。当第二次运行时,它抛出了RuntimeError。

但是如果我们想运行上下文管理器两次呢?我们需要使用可重用的上下文管理器。让我们使用之前所用过的redirect_stdout这个上下文管理器作为示例,

>>> from contextlib import redirect_stdout
>>> from io import StringIO
>>> stream = StringIO()
>>> write_to_stream = redirect_stdout(stream)
>>> with write_to_stream:
...   print("Write something to the stream")
...   with write_to_stream:
...     print("Write something else to stream")
...
>>> print(stream.getvalue())
Write something to the stream
Write something else to stream

在这段代码中,我们创建了一个上下文管理器,它们均向StringIO(一种内存中的文件流)写入数据。这段代码正常运行,而没有像之前那样抛出RuntimeError错误,原因就是redirect_stdout是可重用的,允许我们可以调用两次。当然,实际的例子将会有更多的函数调用,会更加的复杂。一定要注意,可重用的上下文管理器不一定是线程安全的。如果你需要在线程中使用它,请先仔细阅读Python的文档。

2.5 总结

上下文管理器很有趣,也很方便。我经常在自动测试中使用它们,例如,打开和关闭对话。现在,你应该可以使用Python内置的工具去创建你的上下文管理器。你还可以继续阅读Python关于contextlib的文档,那里有很多本文没有覆盖到的知识。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
深入源码解析Python中的对象与类型
Dec 11 Python
详解Python中open()函数指定文件打开方式的用法
Jun 04 Python
python使用 HTMLTestRunner.py生成测试报告
Oct 20 Python
tensorflow识别自己手写数字
Mar 14 Python
[原创]windows下Anaconda的安装与配置正解(Anaconda入门教程)
Apr 05 Python
详解Django-restframework 之频率源码分析
Feb 27 Python
详解python websocket获取实时数据的几种常见链接方式
Jul 01 Python
Python Django 页面上展示固定的页码数实现代码
Aug 21 Python
将tensorflow.Variable中的某些元素取出组成一个新的矩阵示例
Jan 04 Python
python统计字符串中字母出现次数代码实例
Mar 02 Python
如何基于Django实现上下文章跳转
Sep 16 Python
Django集成富文本编辑器summernote的实现步骤
May 31 Python
利用信号如何监控Django模型对象字段值的变化详解
Nov 27 #Python
深入理解Python中range和xrange的区别
Nov 26 #Python
PyCharm在win10的64位系统安装实例
Nov 26 #Python
python shell根据ip获取主机名代码示例
Nov 25 #Python
python自动裁剪图像代码分享
Nov 25 #Python
分享一个简单的python读写文件脚本
Nov 25 #Python
python之virtualenv的简单使用方法(必看篇)
Nov 25 #Python
You might like
各种咖啡的英文名子是什么
2021/03/03 新手入门
要会喝咖啡也要会知道咖啡豆
2021/03/03 咖啡文化
PHP常量使用的几个需要注意的地方(谨慎使用PHP中的常量)
2014/09/12 PHP
php使用explode()函数将字符串拆分成数组的方法
2015/02/17 PHP
理解Javascript_03_javascript全局观
2010/10/11 Javascript
jQuery下扩展插件和拓展函数的写法(匿名函数使用的典型例子)
2010/10/20 Javascript
详解Javascript 装载和执行
2014/11/17 Javascript
一不小心就做错的JS闭包面试题
2015/11/25 Javascript
JavaScript利用HTML DOM进行文档操作的方法
2016/03/28 Javascript
Javascript小技能总结(推荐)
2016/06/02 Javascript
BootStrap下拉框在firefox浏览器界面不友好的解决方案
2016/08/18 Javascript
webix+springmvc session超时跳转登录页面
2016/10/30 Javascript
vue-router+vuex addRoutes实现路由动态加载及菜单动态加载
2017/09/28 Javascript
深入浅析JSONAPI在PHP中的应用
2017/12/24 Javascript
bootstrap table合并行数据并居中对齐效果
2018/10/17 Javascript
使用vue自定义指令开发表单验证插件validate.js
2019/05/23 Javascript
[01:58]最残酷竞争 2016国际邀请赛中国区预选赛积分循环赛回顾
2016/06/28 DOTA
菜鸟使用python实现正则检测密码合法性
2016/01/05 Python
Python+Socket实现基于UDP协议的局域网广播功能示例
2017/08/31 Python
pandas.loc 选取指定列进行操作的实例
2018/05/18 Python
Python实现多级目录压缩与解压文件的方法
2018/09/01 Python
Python使用post及get方式提交数据的实例
2019/01/24 Python
解决Python3下map函数的显示问题
2019/12/04 Python
Python+OpenCV+图片旋转并用原底色填充新四角的例子
2019/12/12 Python
opencv python Canny边缘提取实现过程解析
2020/02/03 Python
Tensorflow实现将标签变为one-hot形式
2020/05/22 Python
Django中Aggregation聚合的基本使用方法
2020/07/09 Python
伦敦一家非常流行的时尚精品店:Oxygen Boutique
2017/01/15 全球购物
CHARLES & KEITH台湾官网:新加坡时尚品牌
2019/07/30 全球购物
请写出 float x 与"零值"比较的 if 语句
2016/01/04 面试题
自荐信的两点禁忌
2013/10/30 职场文书
财务会计应届生求职信
2013/11/24 职场文书
银行财务部实习生的自我鉴定
2013/11/27 职场文书
欠条样本
2015/07/03 职场文书
辩论赛新闻稿
2015/07/17 职场文书
2016党员三严三实心得体会
2016/01/15 职场文书