深入解析Python中的上下文管理器


Posted in Python onJune 28, 2016

1. 上下文管理器是什么?

举个例子,你在写Python代码的时候经常将一系列操作放在一个语句块中:

(1)当某条件为真 ? 执行这个语句块

(2)当某条件为真 ? 循环执行这个语句块

有时候我们需要在当程序在语句块中运行时保持某种状态,并且在离开语句块后结束这种状态。

所以,事实上上下文管理器的任务是 ? 代码块执行前准备,代码块执行后收拾。

上下文管理器是在Python2.5加入的功能,它能够让你的代码可读性更强并且错误更少。接下来,让我们来看看该如何使用。

2. 如何使用上下文管理器?

看代码是最好的学习方式,来看看我们通常是如何打开一个文件并写入”Hello World”?

filename = 'my_file.txt'
mode = 'w' # Mode that allows to write to the file
writer = open(filename, mode)
writer.write('Hello ')
writer.write('World')
writer.close()

1-2行,我们指明文件名以及打开方式(写入)。

第3行,打开文件,4-5行写入“Hello world”,第6行关闭文件。

这样不就行了,为什么还需要上下文管理器?但是我们忽略了一个很小但是很重要的细节:如果我们没有机会到达第6行关闭文件,那会怎样?

举个例子,磁盘已满,因此我们在第4行尝试写入文件时就会抛出异常,而第6行则根本没有机会执行。

当然,我们可以使用try-finally语句块来进行包装:

writer = open(filename, mode)
try:
  writer.write('Hello ')
  writer.write('World')
finally:
  writer.close()

finally语句块中的代码无论try语句块中发生了什么都会执行。因此可以保证文件一定会关闭。这么做有什么问题么?当然没有,但当我们进行一些比写入“Hello world”更复杂的事情时,try-finally语句就会变得丑陋无比。例如我们要打开两个文件,一个读一个写,两个文件之间进行拷贝操作,那么通过with语句能够保证两者能够同时被关闭。

OK,让我们把事情分解一下:

(1)首先,创建一个名为“writer”的文件变量。

(2)然后,对writer执行一些操作。

(3)最后,关闭writer。

这样是不是优雅多了?

with open(filename, mode) as writer:
  writer.write('Hello ') 
  writer.write('World')

让我们深入一点,“with”是一个新关键词,并且总是伴随着上下文管理器出现。“open(filename, mode)”曾经在之前的代码中出现。“as”是另一个关键词,它指代了从“open”函数返回的内容,并且把它赋值给了一个新的变量。“writer”是一个新的变量名。

2-3行,缩进开启一个新的代码块。在这个代码块中,我们能够对writer做任意操作。这样我们就使用了“open”上下文管理器,它保证我们的代码既优雅又安全。它出色的完成了try-finally的任务。

open函数既能够当做一个简单的函数使用,又能够作为上下文管理器。这是因为open函数返回了一个文件类型(file type)变量,而这个文件类型实现了我们之前用到的write方法,但是想要作为上下文管理器还必须实现一些特殊的方法,我会在接下来的小节中介绍。

3. 自定义上下文管理器

让我们来写一个“open”上下文管理器。

要实现上下文管理器,必须实现两个方法 ? 一个负责进入语句块的准备操作,另一个负责离开语句块的善后操作。同时,我们需要两个参数:文件名和打开方式。

Python类包含两个特殊的方法,分别名为:__enter__以及__exit__(双下划线作为前缀及后缀)。

当一个对象被用作上下文管理器时:

(1)__enter__ 方法将在进入代码块前被调用。

(2)__exit__ 方法则在离开代码块之后被调用(即使在代码块中遇到了异常)。

下面是上下文管理器的一个例子,它分别进入和离开代码块时进行打印。

class PypixContextManagerDemo:
 
  def __enter__(self):
    print 'Entering the block'
 
  def __exit__(self, *unused):
    print 'Exiting the block'
 
with PypixContextManagerDemo():
  print 'In the block'
 
#Output:
#Entering the block
#In the block
#Exiting the block

注意一些东西:

(1)没有传递任何参数。
(2)在此没有使用“as”关键词。
稍后我们将讨论__exit__方法的参数设置。
我们如何给一个类传递参数?其实在任何类中,都可以使用__init__方法,在此我们将重写它以接收两个必要参数(filename, mode)。

当我们进入语句块时,将会使用open函数,正如第一个例子中那样。而当我们离开语句块时,将关闭一切在__enter__函数中打开的东西。

以下是我们的代码:

class PypixOpen:
 
  def __init__(self, filename, mode):
    self.filename = filename
    self.mode = mode
 
  def __enter__(self):
    self.openedFile = open(self.filename, self.mode)
    return self.openedFile
 
  def __exit__(self, *unused):
    self.openedFile.close()
 
with PypixOpen(filename, mode) as writer:
  writer.write("Hello World from our new Context Manager!")

来看看有哪些变化:

(1)3-5行,通过__init__接收了两个参数。

(2)7-9行,打开文件并返回。

(3)12行,当离开语句块时关闭文件。

(4)14-15行,模仿open使用我们自己的上下文管理器。

除此之外,还有一些需要强调的事情:

4.如何处理异常

我们完全忽视了语句块内部可能出现的问题。

如果语句块内部发生了异常,__exit__方法将被调用,而异常将会被重新抛出(re-raised)。当处理文件写入操作时,大部分时间你肯定不希望隐藏这些异常,所以这是可以的。而对于不希望重新抛出的异常,我们可以让__exit__方法简单的返回True来忽略语句块中发生的所有异常(大部分情况下这都不是明智之举)。

我们可以在异常发生时了解到更多详细的信息,完备的__exit__函数签名应该是这样的:

def __exit__(self, exc_type, exc_val, exc_tb)

这样__exit__函数就能够拿到关于异常的所有信息(异常类型,异常值以及异常追踪信息),这些信息将帮助异常处理操作。在这里我将不会详细讨论异常处理该如何写,以下是一个示例,只负责抛出SyntaxErrors异常。

class RaiseOnlyIfSyntaxError:
 
  def __enter__(self):
    pass
 
  def __exit__(self, exc_type, exc_val, exc_tb):
    return SyntaxError != exc_type

捕获异常:
当一个异常在with块中抛出时,它作为参数传递给__exit__。三个参数被使用,和sys.exc_info()返回的相同:类型、值和回溯(traceback)。当没有异常抛出时,三个参数都是None。上下文管理器可以通过从__exit__返回一个真(True)值来“吞下”异常。例外可以轻易忽略,因为如果__exit__不使用return直接结束,返回None——一个假(False)值,之后在__exit__结束后重新抛出。

捕获异常的能力创造了有意思的可能性。一个来自单元测试的经典例子——我们想确保一些代码抛出正确种类的异常:

class assert_raises(object):
  # based on pytest and unittest.TestCase
  def __init__(self, type):
    self.type = type
  def __enter__(self):
    pass
  def __exit__(self, type, value, traceback):
    if type is None:
      raise AssertionError('exception expected')
    if issubclass(type, self.type):
      return True # swallow the expected exception
    raise AssertionError('wrong exception type')

with assert_raises(KeyError):
  {}['foo']

5. 谈一些关于上下文库(contextlib)的内容

contextlib是一个Python模块,作用是提供更易用的上下文管理器。

(1)contextlib.closing

假设我们有一个创建数据库函数,它将返回一个数据库对象,并且在使用完之后关闭相关资源(数据库连接会话等)

我们可以像以往那样处理或是通过上下文管理器:

with contextlib.closing(CreateDatabase()) as database:
  database.query()

contextlib.closing方法将在语句块结束后调用数据库的关闭方法。

(2)contextlib.nested

另一个很cool的特性能够有效地帮助我们减少嵌套:

假设我们有两个文件,一个读一个写,需要进行拷贝。

以下是不提倡的:

with open('toReadFile', 'r') as reader:
  with open('toWriteFile', 'w') as writer:
    writer.writer(reader.read())

可以通过contextlib.nested进行简化:

with contextlib.nested(open('fileToRead.txt', 'r'),
            open('fileToWrite.txt', 'w')) as (reader, writer):
  writer.write(reader.read())

在Python2.7中这种写法被一种新语法取代:

with open('fileToRead.txt', 'r') as reader, \
    open('fileToWrite.txt', 'w') as writer:
    writer.write(reader.read())
contextlib.contextmanager

对于Python高级玩家来说,任何能够被yield关键词分割成两部分的函数,都能够通过装饰器装饰的上下文管理器来实现。任何在yield之前的内容都可以看做在代码块执行前的操作,而任何yield之后的操作都可以放在exit函数中。

这里我举一个线程锁的例子:

锁机制保证两段代码在同时执行时不会互相干扰。例如我们有两块并行执行的代码同时写一个文件,那我们将得到一个混合两份输入的错误文件。但如果我们能有一个锁,任何想要写文件的代码都必须首先获得这个锁,那么事情就好办了。如果你想了解更多关于并发编程的内容,请参阅相关文献。

下面是线程安全写函数的例子:

import threading
 
lock = threading.Lock()
 
def safeWriteToFile(openedFile, content):
  lock.acquire()
  openedFile.write(content)
  lock.release()

接下来,让我们用上下文管理器来实现,回想之前关于yield和contextlib的分析:

@contextlib.contextmanager
def loudLock():
  print 'Locking'
  lock.acquire()
  yield
  print 'Releasing'
  lock.release()
 
with loudLock():
  print 'Lock is locked: %s' % lock.locked()
  print 'Doing something that needs locking'
 
#Output:
#Locking
#Lock is locked: True
#Doing something that needs locking
#Releasing

特别注意,这不是异常安全(exception safe)的写法。如果你想保证异常安全,请对yield使用try语句。幸运的是threading。lock已经是一个上下文管理器了,所以我们只需要简单地:

@contextlib.contextmanager
def loudLock():
  print 'Locking'
  with lock:
    yield
  print 'Releasing'

因为threading.lock在异常发生时会通过__exit__函数返回False,这将在yield被调用是被重新抛出。这种情况下锁将被释放,但对于“print ‘Releasing'”的调用则不会被执行,除非我们重写try-finally。

如果你希望在上下文管理器中使用“as”关键字,那么就用yield返回你需要的值,它将通过as关键字赋值给新的变量。下面我们就仔细来讲一下。

6.使用生成器定义上下文管理器
当讨论生成器时,据说我们相比实现为类的迭代器更倾向于生成器,因为它们更短小方便,状态被局部保存而非实例和变量中。另一方面,正如双向通信章节描述的那样,生成器和它的调用者之间的数据流可以是双向的。包括异常,可以直接传递给生成器。我们想将上下文管理器实现为特殊的生成器函数。事实上,生成器协议被设计成支持这个用例。

@contextlib.contextmanager
def some_generator(<arguments>):
  <setup>
  try:
    yield <value>
  finally:
    <cleanup>

contextlib.contextmanager装饰一个生成器并转换为上下文管理器。生成器必须遵循一些被包装(wrapper)函数强制执行的法则——最重要的是它至少yield一次。yield之前的部分从__enter__执行,上下文管理器中的代码块当生成器停在yield时执行,剩下的在__exit__中执行。如果异常被抛出,解释器通过__exit__的参数将之传递给包装函数,包装函数于是在yield语句处抛出异常。通过使用生成器,上下文管理器变得更短小精炼。

让我们用生成器重写closing的例子:

@contextlib.contextmanager
def closing(obj):
  try:
    yield obj
  finally:
    obj.close()

再把assert_raises改写成生成器:

@contextlib.contextmanager
def assert_raises(type):
  try:
    yield
  except type:
    return
  except Exception as value:
    raise AssertionError('wrong exception type')
  else:
    raise AssertionError('exception expected')

这里我们用装饰器将生成函数转化为上下文管理器!

Python 相关文章推荐
python实现批量转换文件编码(批转换编码示例)
Jan 23 Python
Python实现矩阵转置的方法分析
Nov 24 Python
django 按时间范围查询数据库实例代码
Feb 11 Python
Python使用Scrapy爬虫框架全站爬取图片并保存本地的实现代码
Mar 04 Python
PyCharm设置SSH远程调试的方法
Jul 17 Python
python判断输入日期为第几天的实例
Nov 13 Python
Python 监测文件是否更新的方法
Jun 10 Python
python暴力解压rar加密文件过程详解
Jul 05 Python
python飞机大战pygame游戏框架搭建操作详解
Dec 17 Python
python线程池 ThreadPoolExecutor 的用法示例
Oct 10 Python
Django执行源生mysql语句实现过程解析
Nov 12 Python
Pytest中conftest.py的用法
Jun 27 Python
详解Python中contextlib上下文管理模块的用法
Jun 28 #Python
实例讲解Python中SocketServer模块处理网络请求的用法
Jun 28 #Python
Python中asyncore异步模块的用法及实现httpclient的实例
Jun 28 #Python
python 字典(dict)按键和值排序
Jun 28 #Python
简单谈谈python的反射机制
Jun 28 #Python
Python实现带百分比的进度条
Jun 28 #Python
Python中的字符串替换操作示例
Jun 27 #Python
You might like
从性能方面考虑PHP下载远程文件的3种方法
2015/12/29 PHP
WordPress后台中实现图片上传功能的实例讲解
2016/01/11 PHP
PHP 与 UTF-8 的最佳实践详细介绍
2017/01/04 PHP
php实现等比例压缩图片
2018/07/26 PHP
PHP实现微信公众号验证Token的示例代码
2019/12/16 PHP
添加到收藏夹代码(兼容几乎所有的浏览器)
2007/01/09 Javascript
Jquery Ajax学习实例6 向WebService发出请求,返回DataSet(XML) 异步调用
2010/03/18 Javascript
读jQuery之十二 删除事件核心方法
2011/07/31 Javascript
JQuery获取或设置ckeditor的数据(示例代码)
2013/11/15 Javascript
wap图片滚动特效无css3元素纯js脚本编写
2014/08/22 Javascript
学习JavaScript设计模式(策略模式)
2015/11/26 Javascript
Node.js模块封装及使用方法
2016/03/06 Javascript
jQuery通过deferred对象管理ajax异步
2016/05/20 Javascript
微信公众平台开发教程(六)获取个性二维码的实例
2016/12/02 Javascript
详解Vue.js 2.0 如何使用axios
2017/04/21 Javascript
通过一个简单的例子学会vuex与模块化
2017/11/22 Javascript
JS使用tween.js动画库实现轮播图并且有切换功能
2018/07/17 Javascript
JavaScript类的继承操作实例总结
2018/12/20 Javascript
element-ui组件table实现自定义筛选功能的示例代码
2019/03/15 Javascript
微信小程序简单的canvas裁剪图片功能详解
2019/07/12 Javascript
javascript实现摄像头拍照预览
2019/09/30 Javascript
Nodejs + Websocket 指定发送及群聊的实现
2020/01/09 NodeJs
Vue2.4+新增属性.sync、$attrs、$listeners的具体使用
2020/03/08 Javascript
python虚拟环境virualenv的安装与使用
2016/12/18 Python
Python3爬取英雄联盟英雄皮肤大图实例代码
2018/11/14 Python
浅谈在django中使用redirect重定向数据传输的问题
2020/03/13 Python
解决TensorFlow训练模型及保存数量限制的问题
2021/03/03 Python
一张图片能隐含千言万语之隐藏你的程序代码
2012/12/13 HTML / CSS
HTML5梦幻之旅——炫丽的流星雨效果实现过程
2013/08/06 HTML / CSS
伦敦的高级牛仔布专家:Trilogy
2018/08/06 全球购物
医德医风个人工作总结2014
2014/11/14 职场文书
学前班语言教学计划
2015/01/20 职场文书
公司门卫岗位职责
2015/04/13 职场文书
承兑汇票延期证明
2015/06/23 职场文书
解决golang结构体tag编译错误的问题
2021/05/02 Golang
铁头也玩根德 YachtBoy YB-230......
2022/04/05 无线电