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获取豆瓣电影简介代码分享
Jan 16 Python
采用python实现简单QQ单用户机器人的方法
Jul 03 Python
python实现将文本转换成语音的方法
May 28 Python
python实现微信定时每天和女友发送消息
Apr 29 Python
python语言基本语句用法总结
Jun 11 Python
python3.6 如何将list存入txt后再读出list的方法
Jul 02 Python
pandas的to_datetime时间转换使用及学习心得
Aug 11 Python
在Python3 numpy中mean和average的区别详解
Aug 24 Python
python创建子类的方法分析
Nov 28 Python
理解Django 中Call Stack机制的小Demo
Sep 01 Python
详解python中的异常和文件读写
Jan 03 Python
使用pytorch实现线性回归
Apr 11 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
PHP4中实现动态代理
2006/10/09 PHP
php之字符串变相相减的代码
2007/03/19 PHP
phpMyAdmin 安装及问题总结
2009/05/28 PHP
用php随机生成福彩双色球号码的2种方法
2013/02/04 PHP
PHP中使用正则表达式提取中文实现笔记
2015/01/20 PHP
配置eAccelerator和XCache扩展来加速PHP程序的执行
2015/12/22 PHP
PHP实现求连续子数组最大和问题2种解决方法
2017/12/26 PHP
Laravel使用模型实现like模糊查询的例子
2019/10/24 PHP
javascript中巧用“闭包”实现程序的暂停执行功能
2007/04/04 Javascript
jQuery对象和DOM对象的相互转化实现代码
2010/03/02 Javascript
jquery keypress,keyup,onpropertychange键盘事件
2010/06/25 Javascript
javascript 冒泡排序 正序和倒序实现代码
2010/12/14 Javascript
Array.prototype.concat不是通用方法反驳[译]
2012/09/20 Javascript
浅谈JavaScript 标准对象
2016/06/02 Javascript
浅析Javascript的自动分号插入(ASI)机制
2016/09/29 Javascript
jquery实现input框获取焦点的方法
2017/02/06 Javascript
原生js实现可拖拽效果
2017/02/28 Javascript
vue.js源代码core scedule.js学习笔记
2017/07/03 Javascript
基于easyui checkbox 的一些操作处理方法
2017/07/10 Javascript
使用vue-resource进行数据交互的实例
2017/09/02 Javascript
VUE组件中的 Drawer 抽屉实现代码
2019/08/06 Javascript
微信JSSDK实现打开摄像头拍照再将相片保存到服务器
2019/11/15 Javascript
从零学Python之hello world
2014/05/21 Python
Python IDE PyCharm的基本快捷键和配置简介
2015/11/04 Python
tensorflow入门之训练简单的神经网络方法
2018/02/26 Python
python3实现zabbix告警推送钉钉的示例
2019/02/20 Python
pyqt5 使用label控件实时显示时间的实例
2019/06/14 Python
HTML5 3D衣服摇摆动画特效
2016/03/17 HTML / CSS
Timberland澳大利亚官网:全球领先的户外品牌
2019/12/10 全球购物
什么是静态路由,其特点是什么?什么是动态路由,其特点是什么?
2013/07/26 面试题
初任培训自我鉴定
2013/10/07 职场文书
物业电工岗位职责
2013/11/20 职场文书
党员组织生活会发言材料
2014/10/17 职场文书
2015年街道办事处工作总结
2015/05/22 职场文书
合理缓解职场压力,让你随时保持最佳状态!
2019/06/21 职场文书
Python实现Matplotlib,Seaborn动态数据图
2022/05/06 Python