Python context manager 上下文管理器

TL;DR 上下文管理器通过控制代码块级别的上下文,可以实现的很多诸如自动关闭文件、捕获异常等功能

什么是上下文管理器

上下文管理器 context manager 能够控制程序执行的上下文,比如控制文件的关闭,抑制异常,捕获异常,修改上下文变量等

调用过程

简单例子

class Context(object):
    def __enter__(self):
        print("__enter__ invoked")

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("__exit__ invoked")

with Context():
    print("with block")
`</pre>

运行以上代码,则会有如下输出:

<pre>`__enter__ invoked
with block
__exit__ invoked
`</pre>

### 中级复杂的例子

<pre>`class ContextInstance(object):
    def __init__(self, msg):
        self.msg = msg

    def say(self):
        print(self.msg)

    def when_exit(self):
        print("instance exited!")

class Context(object):
    def __init__(self, msg):
        self.msg = msg
        self.instance = None
        super(Context, self).__init__()

    def __enter__(self):
        print("__enter__ invoked")
        self.instance = ContextInstance(self.msg)
        return self.instance

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("__exit__ invoked")
        self.instance.when_exit()

with Context("Message") as ctx:
    ctx.say()
    print("within block")
`</pre>

运行以上代码,则会有如下输出:

<pre>`__enter__ invoked
Message
within block
__exit__ invoked
instance exited!
`</pre>

### 复杂例子

<pre>`class ContextManager(object):
    def __init__(self, msg):
        self.msg = msg
        self.instance = None
        super(ContextManager, self).__init__()

    def __enter__(self):
        print("__enter__ invoked")
        self.instance = ContextInstance(self.msg)
        return self.instance

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("__exit__ invoked")
        self.instance.when_exit()

class ContextInstance(object):
    def __init__(self, msg):
        self.msg = msg
        super(ContextInstance, self).__init__()

    def say(self):
        print(self.msg)

    def when_exit(self):
        print("instance existed!")

with ContextManager("Message") as ctx:
    ctx.say()
    print("with block")
`</pre>

运行以上代码,则会有如下输出:

<pre>`__enter__ invoked
Message
with block
__exit__ invoked
instance existed!
`</pre>

## 使用场景

### 控制文件关闭

<pre>`with open("/tmp/context_manager.txt", 'wt') as f:
    f.write("contexts go here")
`</pre>

### 抑制异常

<pre>`class Context(object):
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # supress all the exception
        return True

with Context():
    print("start")
    raise ValueError("E!")
    print("end")
print("next")
`</pre>

运行以上代码,则会有如下输出:

<pre>`start
next
`</pre>

### 捕获异常

<pre>`# TODO: see unittest catch exceptioon
`</pre>

### 修改上下文

<pre>`env_context = []

class Context(object):
    def __enter__(self):
        env_context.append(1)

    def __exit__(self, exc_type, exc_val, exc_tb):
        env_context.pop()

print("before with", len(env_context))

with Context():
    print("in with", len(env_context))

print("after with", len(env_context))
`</pre>

运行以上代码,则会有如下输出:

<pre>`('before with', 0)
('in with', 1)
('after with', 0)
`</pre>

## contextlib 库

contextlib是Python官方包,使用contenxtlib可以很方便的构建上下文管理器

### contextlib.contextmanager

#### 简单用法

<pre>`import contextlib

@contextlib.contextmanager
def context():
    print("before yeild")
    yield []
    print("after yeild")

with context() as value:
    print("before value")
    print(value)
    print("after value")
`</pre>

运行以上代码,则会有如下输出:

<pre>`before yeild
before value
[]
after value
after yeild
`</pre>

#### 捕获异常

<pre>`import contextlib

@contextlib.contextmanager
def context():
    print("before yeild")
    try:
        yield
    except ValueError as e:
        print(e)

    print("after yeild")

with context():
    raise ValueError("NO")
    print("after exception")

print("after with")
`</pre>

运行以上代码,则会有如下输出:

<pre>`before yeild
NO
after yeild
after with
`</pre>

### 嵌套的上下文管理器 (Nesting contexts)

<pre>`import contextlib

@contextlib.contextmanager
def context(name):
    print("entring %s" % name)
    yield name
    print("exiting %s" % name)

with contextlib.nested(context('a'), context('b'), context('c')) as (a, b, c):
    print("inside with statement: %s" % ((a, b, c), ))
`</pre>

运行以上代码,则会有如下输出:

<pre>`entring a
entring b
entring c
inside with statement: ('a', 'b', 'c')
exiting c
exiting b
exiting a

/usr/local/lib/python2.7/dist-packages/ipykernel_launcher.py:9: DeprecationWarning: With-statements now directly support multiple context managers
  if __name__ == '__main__':
`</pre>

观察输出,你会发现,嵌套的上下文管理器工作起来像是先入后出的栈一样:最先进入的管理器最后退出,最后进入的管理器最先退出。

同时你应该观察到一个Warning:DeprecationWarning: With-statements now directly support multiple context managers。contextlib.nested函数将于后续Python版本被弃用,Python 2.7引入了嵌套上下文管理器新语法。

#### Python 2.7 版本的新语法 (py2.7 now support nested)

<pre>`import contextlib

@contextlib.contextmanager
def context(name):
    print("entring %s" % name)
    yield name
    print("exiting %s" % name)

with context('a') as a, context('b') as b, context('c') as c:
    print("inside with statement: %s" % ((a, b, c), ))
`</pre>

运行以上代码,则会有如下输出:

<pre>`entring a
entring b
entring c
inside with statement: ('a', 'b', 'c')
exiting c
exiting b
exiting a
`</pre>

### 自动关闭的上下文管理器 (closing context)

`contextlib.closing`等价与如下代码:

<pre>`import contextlib

@contextlib.contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()
`</pre>

### 简单例子

<pre>`import contextlib

class Door():
    def close(self):
        print("Door closing")
    def do_something(self):
        print("doing something")

try:
    with contextlib.closing(Door()) as door:
        door.do_something()
        raise ValueError("Exception raising")
except ValueError as e:
    print(e)
`</pre>

运行以上代码,则会有如下输出:

<pre>`doing something
Door closing
Exception raising
`</pre>

### 替代的例子

<pre>`import contextlib

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

class Door():
    def close(self):
        print("Door closing")
    def do_something(self):
        print("doing something")

try:
    with closing(Door()) as door:
        door.do_something()
        raise ValueError("Exception raising")
except ValueError as e:
    print(e)
`</pre>

运行以上代码,则会有如下输出:

<pre>`doing something
Door closing
Exception raising
`</pre>

### 实际的例子

<pre>`from contextlib import closing
import urllib

fair_license_url = 'http://www.samurajdata.se/opensource/mirror/licenses/fair.txt'

with closing(urllib.urlopen(fair_license_url)) as page:
    for line in page:
        print(line)
`</pre>

运行以上代码,则会有如下输出:

<pre>`Fair License

&lt;Copyright Information&gt;

Usage of the works is permitted provided that this

instrument is retained with the works, so that any entity

that uses the works is notified of this instrument.

DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.

[2004, Fair License: rhid.com/fair]

参考

  1. Python官方contexlib库文档