每日一记:(2016-12-02)Python:ContextManager(上下文管理器)

今天的日志就来说说python的一个优雅的语法糖——ContextManager(上下文管理器)

1. 那么什么叫ContextManager

上下文管理器的任务是 – 代码块执行前准备,代码块执行后收拾。其中代码块执行后包括了正常退出和产生异常。
当然这些功能可以用try-except语句实现,但是如果使用try-except代码就显得不那么优雅。而且经常会忘记会产生异常这种情况。

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

先给个例子吧!
>>> with open('test.txt', 'w') as writer:
...     writer.write("aaaa")
...     writer.write('bbbb')
其实就是with语句啦,使用的时候也是很简单的说。当然也可以没有as之后的内容单独一个with open()也是可以的。

3. 自定义上下文管理器

要实现上下文管理器,必须实现两个方法 – 一个负责进入语句块的准备操作,另一个负责离开语句块的善后操作。
要实现上下文管理器,需要实现两个魔法方法__enter__以及__exit__(双下划线作为前缀及后缀)。

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

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

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

下面是上下文管理器的一个例子,我们通过例子看一下__enter__、__exit__都是什么时候被调用的吧

>>> class PypixContextManagerDemo:
...     def __enter__(self):
...             print 'Entering the block'
...     def __exit__(self, *unseted):
...             print 'Exiting the block'
...
>>> with PypixContextManagerDemo():
...     print 'In the block'
...
  Entering the block
  In the block
  Exiting the block
这里有一个疑问:这上面都是对于类来说实现了上下文管理器,但是对于open(处理文件的函数)他就是一个函数,为啥也能用上下文管理器呢?
上面的例子还有几点不完善的地方:
  • 没有传递任何参数。
  • 在此没有使用“as”关键词。
  • 稍后我们将讨论__exit__方法的参数设置。
...     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, *unseted):
...             self.openedFile.close()
...
>>> with PypixOpen(filename, mode) as writer:
...     writer.write("Hello world from our new Context Manager")
那么,上面解决了输入参数和as之后赋值问题,还有一个异常处理的问题,还没有解决呢?

如何处理异常

如果语句块内部发生了异常,__exit__方法将被调用,而异常将会被重新抛出(re-raised)。当处理文件写入操作时,大部分时间你肯定不希望隐藏这些异常,所以这是可以的。而对于不希望重新抛出的异常,我们可以让__exit__方法简单的返回True来忽略语句块中发生的所有异常(大部分情况下这都不是明智之举)。
完备的__exit__函数签名应该是这样的:
def __exit__(self,exc_type,exc_val,exc_tb)
__exit__方法中给的参数有exc_type, exc_val, exc_tb,那么就可以进行异常处理了。(exc_type是异常类型,exc_val是异常的值和exc_tb是异常的回溯信息)
调用上下文管理器的 __exit__() 方法,如果 with结构中产生异常,那么该异常的 type、value 和 traceback 会作为参数传给 __exit__(),否则传三个 None

  • 如果 with_suite 产生异常,并且__exit__() 的返回值等于 False,那么这个异常将被重新抛出到上层
  • 如果 with_suite 产生异常,并且__exit__() 的返回值等于 True,那么这个异常就被忽略,继续执行后面的代码
通过上面的内容我们可以自己进行构造__exit__()函数了。
例:
>>> def __exit__(self,exc_type,exc_val,exc_tb):
...     self.file.close()
...     if exc_type:
...             print exc_type, exc_val, exc_tb
...             return False
...     return True

4. contextmanager装饰器

@contextmanager

contextlib模块的contextmanager装饰器可以更方便的实现上下文管理器。

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

from contextlib import contextmanager
@contextmanager
def listTrans(alist):
    thecopy=list(alist)
    yield thecopy
    alist[:]=thecopy
alist=[]
with listTrans(alist) as working:
    working.append(1)
    working.append(2)
print alist
 

yield返回的值相当于__enter__的返回值。

要注意的是,这不是异常安全的写法,也就是说,当出现异常时,yield后的语句是不会执行的,想要异常安全,可用try捕捉异常:

from contextlib import contextmanager
@contextmanager
def listTrans(alist):
    thecopy=list(alist)
    try:
        yield thecopy
    except RuntimeError:
        pass
    alist[:]=thecopy
alist=[]
with listTrans(alist) as working:
    working.append(1)
    working.append(2)
    raise RuntimeError
nested与closing

contextlib模块还有两个好玩的方法:nested,closing。

nested:用来更方便的减少嵌套写法:

当要嵌套的写上下文管理器时:

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

python2.7后nested就过时了:

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


closing(object):创建上下文管理器,在执行过程离开with语句时自动执行object.close():

class Door(object) :
    def open(self) :
        print 'Door is opened'
    def close(self) :
        print 'Door is closed'
with contextlib.closing(Door()) as door :
    door.open()

5. 其他

Python定义了几个上下文管理器来支持简单的线程同步,提示文件或其他对象的关闭,以及更简单地操作主动小数算术上下文。

发表评论