python standard library context-manager-types

python 标准库 上下文管理类型

每日一词:

intresting :

US [‘ɪntrəstɪŋ] UK [‘ɪntrəstɪŋ]

  • adj.有趣的;有吸引力的
  • v.“interest”的现在分词
  • Web有意思的;令人感兴趣的;引人入胜的

比较级:more interesting
最高级:most interesting

大部分时候,你都是一个人在默默努力,这样,距离成功才会更进一步。

​ – 凭海临风语录

python 标准库学习 上下文管理

什么是上下文管理器?上下文管理器就是一个用装饰器实现上下文协议管理的对象。主要用于保存和恢复各种全局状态,例如关闭文件等。下面我们来了解具体的内容。

函数

  • contextmanager.__enter__()

    进入运行时上下文并返回此对象或关联到该运行时上下文的其他对象。 此方法的返回值会绑定到使用此上下文管理器的 with 语句的 as 子句中的标识符。一个返回其自身的上下文管理器的例子是 file object。 文件对象会从 enter() 返回其自身,以允许 open() 被用作 with 语句中的上下文表达式。一个返回关联对象的上下文管理器的例子是 decimal.localcontext() 所返回的对象。 此种管理器会将活动的 decimal 上下文设为原始 decimal 上下文的一个副本并返回该副本。 这允许对 with 语句的语句体中的当前 decimal 上下文进行更改,而不会影响 with 语句以外的代码。

  • contextmanager.__exit__(*exc_type*, *exc_val*, *exc_tb*)

    退出运行时上下文并返回一个布尔值旗标来表明所发生的任何异常是否应当被屏蔽。 如果在执行 with 语句的语句体期间发生了异常,则参数会包含异常的类型、值以及回溯信息。 在其他情况下三个参数均为 None。自此方法返回一个真值将导致 with 语句屏蔽异常并继续执行紧随在 with 语句之后的语句。 否则异常将在此方法结束执行后继续传播。 在此方法执行期间发生的异常将会取代 with 语句的语句体中发生的任何异常。传入的异常绝对不应当被显式地重新引发 —— 相反地,此方法应当返回一个假值以表明方法已成功完成并且不希望屏蔽被引发的异常。 这允许上下文管理代码方便地检测 __exit__() 方法是否确实已失败。

一个文件操作实例

1
2
3
4
>>> with open("/etc/hosts", "r") as file:
... dir(file)
...
['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']

此时的open返回的对象file,就实现了管理打开文件、关闭文件的上下文管理协议。

with 语句上下文管理器

上下文管理器 是一个对象,它定义了在执行 with 语句时要建立的运行时上下文。 上下文管理器处理进入和退出所需运行时上下文以执行代码块。 通常使用 with 语句(在 with 语句 中描述),但是也可以通过直接调用它们的方法来使用。

上下文管理器的典型用法包括保存和恢复各种全局状态,锁定和解锁资源,关闭打开的文件等等。

要了解上下文管理器的更多信息,请参阅 上下文管理器类型

  • object.__enter__(self)

    进入与此对象相关的运行时上下文。 with 语句将会绑定这个方法的返回值到 as 子句中指定的目标,如果有的话。

  • object.__exit__(self, exc_type, exc_value, traceback)

    退出关联到此对象的运行时上下文。 各个参数描述了导致上下文退出的异常。 如果上下文是无异常地退出的,三个参数都将为 None。如果提供了异常,并且希望方法屏蔽此异常(即避免其被传播),则应当返回真值。 否则的话,异常将在退出此方法时按正常流程处理。请注意 __exit__() 方法不应该重新引发被传入的异常,这是调用者的责任。

可以参考

  • PEP 343 - “with” 语句

    Python with 语句的规范描述、背景和示例。

自定义上下文管理

实现__enter____exit__ 方法就是一个实现了上下文管理的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ContextManager(object):
def __init__(self):
print '__init__()'

def __enter__(self):
print '__enter__()'
return self

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

with ContextManager():
print "OK, we can do something here~~"

#输出
__init__()
__enter__()
OK, we can do something here~~
__exit__()

另一个不返回当前类的上下文管理器的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class InnerContext(object):
def __init__(self, obj):
print 'InnerContext.__init__(%s)' % obj

def do_something(self):
print 'InnerContext.do_something()'

def __del__(self):
print 'InnerContext.__del__()'

class ContextManager(object):
def __init__(self):
print 'ContextManager.__init__()'

def __enter__(self):
print 'ContextManager.__enter__()'
return InnerContext(self)

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

with ContextManager() as obj:
obj.do_something()
print "OK, we can do something here~~"

#输出
ContextManager.__init__()
ContextManager.__enter__()
InnerContext.__init__(<__main__.ContextManager object at 0x1012f95d0>)
InnerContext.do_something()
OK, we can do something here~~
ContextManager.__exit__()
InnerContext.__del__()

异常处理的例子 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ContextManager(object):
def __init__(self, flag):
print 'ContextManager.__init__(%s)' % flag
self.flag = flag

def __enter__(self):
print 'ContextManager.__enter__()'
return self

def __exit__(self, exc_type, exc_val, exc_tb):
print 'ContextManager.__exit__(%s, %s, %s)' % (exc_type, exc_val, exc_tb)
return self.flag

with ContextManager(True):
raise RuntimeError('error message handled')

print
with ContextManager(False):
raise RuntimeError('error message propagated')

#输出
ContextManager.__init__(True)
ContextManager.__enter__()
ContextManager.__exit__(<type 'exceptions.RuntimeError'>, error message handled, <traceback object at 0x10d69dbd8>)

ContextManager.__init__(False)
ContextManager.__enter__()
ContextManager.__exit__(<type 'exceptions.RuntimeError'>, error message propagated, <traceback object at 0x109e0fbd8>)
Traceback (most recent call last):
File "ContextManager.py", line 19, in <module>
raise RuntimeError('error message propagated')
RuntimeError: error message propagated

contextlib 模块

参考文档

源代码 Lib/contextlib.py

这个内置模块实现了上下文管理,使用with关键字。

主要方法如下(节选自源码):

1
2
3
4
__all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext",
"AbstractContextManager", "AbstractAsyncContextManager",
"AsyncExitStack", "ContextDecorator", "ExitStack",
"redirect_stdout", "redirect_stderr", "suppress"]

核心类

class contextlib.AbstractContextManager

同步的上下文管理类

class contextlib.AbstractAsyncContextManager

异步的上下文管理类

装饰器

  • `@contextlib.contextmanager`

    一个实现了上下文资源管理的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from contextlib import contextmanager

    @contextmanager
    def managed_resource(*args, **kwds):
    # Code to acquire resource, e.g.:
    resource = acquire_resource(*args, **kwds)
    try:
    yield resource
    finally:
    # Code to release resource, e.g.:
    release_resource(resource)

    >>> with managed_resource(timeout=3600) as resource:
    ... # Resource is released at the end of this block,
    ... # even if code in the block raises an exception

    ==tips== : 注意这里 返回的是generator对象,每次迭代器只会yield一个对象出来,这个值会用在with语句中,绑定到as 后的对象上。

  • @contextlib.asynccontextmanager`

    下面是一个实现了异步上下文管理器的实例,关于操作数据库对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from contextlib import asynccontextmanager

    @asynccontextmanager
    async def get_connection():
    conn = await acquire_db_connection()
    try:
    yield conn
    finally:
    await release_db_connection(conn)

    async def get_all_users():
    async with get_connection() as conn:
    return conn.query('SELECT ...')

其他方法

  • contextlib.closing(thing)

    返回一个上下文管理对象,在语句结束之前被调用

    相当于下面的实现

    1
    2
    3
    4
    5
    6
    7
    8
    from contextlib import contextmanager

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

    也可以这样实现

    1
    2
    3
    4
    5
    6
    from contextlib import closing
    from urllib.request import urlopen

    with closing(urlopen('http://www.python.org')) as page:
    for line in page:
    print(line)
  • contextlib.nullcontext

    返回一个上下文管理对象( 实现了__enter__方法)

    一个例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def myfunction(arg, ignore_exceptions=False):
    if ignore_exceptions:
    # Use suppress to ignore all exceptions.
    cm = contextlib.suppress(Exception)
    else:
    # Do not ignore any exceptions, cm has no effect.
    cm = contextlib.nullcontext()
    with cm:
    # Do something

另一个例子

1
2
3
4
5
6
7
8
9
10
def process_file(file_or_path):
if isinstance(file_or_path, str):
# If string, open file
cm = open(file_or_path)
else:
# Caller is responsible for closing file
cm = nullcontext(file_or_path)

with cm as file:
# Perform processing on the file
  • contextlib.suppress

    返回一个声明的异常对象的上下文管理

    一个例子:

    1
    2
    3
    4
    5
    6
    7
    from contextlib import suppress

    with suppress(FileNotFoundError):
    os.remove('somefile.tmp')

    with suppress(FileNotFoundError):
    os.remove('someotherfile.tmp')

    和下面的代码等价

    1
    2
    3
    4
    5
    6
    7
    8
    9
    try:
    os.remove('somefile.tmp')
    except FileNotFoundError:
    pass

    try:
    os.remove('someotherfile.tmp')
    except FileNotFoundError:
    pass
  • contextlib.redirect_stdout

    临时输出标准输出的上下文管理器

  • contextlib.redirect_stderr

    临时输出标准错误的上下文管理器

  • class contextlib.ContextDecorator

允许一个类像装饰器那样使用,ContextDecorator 正好实现了__enter__ and __exit__ 方法。

使用contextlib就自动调用这个装饰器。

一个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from contextlib import ContextDecorator

class mycontext(ContextDecorator):
def __enter__(self):
print('Starting')
return self

def __exit__(self, *exc):
print('Finishing')
return False

>>> @mycontext()
... def function():
... print('The bit in the middle')
...
>>> function()
Starting
The bit in the middle
Finishing

>>> with mycontext():
... print('The bit in the middle')
...
Starting
The bit in the middle
Finishing

其实本质上就是实现了语法糖

例如:

1
2
3
def f():
with cm():
# Do stuff

ContextDecorator 允许你这样使用:

1
2
3
@cm()
def f():
# Do stuff

允许你通过继承ContextBaseClass和ContextDecorator,实现Mixin class(我也不知道该如何翻译,姑且翻译成混合继承吧)

1
2
3
4
5
6
7
8
from contextlib import ContextDecorator

class mycontext(ContextBaseClass, ContextDecorator):
def __enter__(self):
return self

def __exit__(self, *exc):
return False
  • class contextlib.ExitStack

一个上下文管理器可以被设计成自动合并其他上下文管理器,清除方法(栈),尤其是那些需要输入数据的功能实现。

这里我看了源码,通过一个栈结构管理上下文管理, 其实就是实现了一个上下文管理器栈

下面是一个例子:

1
2
3
4
5
with ExitStack() as stack:
files = [stack.enter_context(open(fname)) for fname in filenames]
# All opened files will automatically be closed at the end of
# the with statement, even if attempts to open files later
# in the list raise an exception

个人理解这是一个低级的api,内部实现了,你无需关心何时该调用该方法,由python内部去处理。

小结

最近恰好看flask的源码,flask的生命周期管理也是使用上下文管理装饰器实现。

等有空再更新一篇吧。

今天就到这里,祝大家周末愉快!

坚持原创技术分享,您的支持将鼓励我继续创作!