with 语句 - 上下文管理器

with 语句 - 上下文管理器

1. The with statement - with 语句

English
https://docs.python.org/3.7/reference/compound_stmts.html

Simplified Chinese
https://docs.python.org/zh-cn/3.7/reference/compound_stmts.html

The with statement is used to wrap the execution of a block with methods defined by a context manager (see section With Statement Context Managers). This allows common try…except…finally usage patterns to be encapsulated for convenient reuse.
with 语句用于包装带有使用上下文管理器 (参见 with 语句上下文管理器一节) 定义的方法的代码块的执行。这允许对普通的 try...except...finally 使用模式进行封装以方便地重用。

with_stmt ::=  "with" with_item ("," with_item)* ":" suite
with_item ::=  expression ["as" target]

The execution of the with statement with one “item” proceeds as follows:
带有一个 item 的 with 语句的执行过程如下:

  1. The context expression (the expression given in the with_item) is evaluated to obtain a context manager.
    对上下文表达式 (在 with_item 中给出的表达式) 求值以获得一个上下文管理器。

  2. The context manager’s __exit__() is loaded for later use.
    载入上下文管理器的 __exit__() 以便后续使用。

  3. The context manager’s __enter__() method is invoked.
    发起调用上下文管理器的 __enter__() 方法。

  4. If a target was included in the with statement, the return value from __enter__() is assigned to it.
    如果 with 语句中包含一个目标,来自 __enter__() 的返回值将被赋值给它。

The with statement guarantees that if the __enter__() method returns without an error, then __exit__() will always be called. Thus, if an error occurs during the assignment to the target list, it will be treated the same as an error occurring within the suite would be. See step 6 below.
with 语句会保证如果 __enter__() 方法返回时未发生错误,则 __exit__() 将总是被调用。因此,如果在对目标列表赋值期间发生错误,则会将其视为在语句体内部发生的错误。参见下面的第 6 步。

  1. The suite is executed.
    执行语句体。

  2. The context manager’s __exit__() method is invoked. If an exception caused the suite to be exited, its type, value, and traceback are passed as arguments to __exit__(). Otherwise, three None arguments are supplied.
    发起调用上下文管理器的 __exit__() 方法。如果语句体的退出是由异常导致的,则其类型、值和回溯信息将被作为参数传递给 __exit__()。否则的话,将提供三个 None 参数。

If the suite was exited due to an exception, and the return value from the __exit__() method was false, the exception is reraised. If the return value was true, the exception is suppressed, and execution continues with the statement following the with statement.
如果语句体的退出是由异常导致的,并且来自 __exit__() 方法的返回值为假,则该异常会被重新引发。如果返回值为真,则该异常会被抑制,并会继续执行 with 语句之后的语句。

If the suite was exited for any reason other than an exception, the return value from __exit__() is ignored, and execution proceeds at the normal location for the kind of exit that was taken.
如果语句体由于异常以外的任何原因退出,则来自 __exit__() 的返回值会被忽略,并会在该类退出正常的发生位置继续执行。

With more than one item, the context managers are processed as if multiple with statements were nested:
如果有多个项目,则会视作存在多个 with 语句嵌套来处理多个上下文管理器:

with A() as a, B() as b:
    suite

is equivalent to

with A() as a:
    with B() as b:
        suite

2. With Statement Context Managers - with 语句上下文管理器

English
https://docs.python.org/3.7/reference/datamodel.html

Simplified Chinese
https://docs.python.org/zh-cn/3.7/reference/datamodel.html

A context manager is an object that defines the runtime context to be established when executing a with statement. The context manager handles the entry into, and the exit from, the desired runtime context for the execution of the block of code. Context managers are normally invoked using the with statement (described in section The with statement), but can also be used by directly invoking their methods.
上下文管理器是一个对象,它定义了在执行 with 语句时要建立的运行时上下文。上下文管理器处理进入和退出所需运行时上下文以执行代码块。通常使用 with 语句 (在 with 语句中描述),但是也可以通过直接调用它们的方法来使用。

Typical uses of context managers include saving and restoring various kinds of global state, locking and unlocking resources, closing opened files, etc.
上下文管理器的典型用法包括保存和恢复各种全局状态,锁定和解锁资源,关闭打开的文件等等。

For more information on context managers, see Context Manager Types.
要了解上下文管理器的更多信息,请参阅 上下文管理器类型。

object.__enter__(self)
Enter the runtime context related to this object. The with statement will bind this method’s return value to the target(s) specified in the as clause of the statement, if any.
进入与此对象相关的运行时上下文。with 语句将会绑定这个方法的返回值到 as 子句中指定的目标,如果有的话。

object.__exit__(self, exc_type, exc_value, traceback)
Exit the runtime context related to this object. The parameters describe the exception that caused the context to be exited. If the context was exited without an exception, all three arguments will be None.
退出关联到此对象的运行时上下文。各个参数描述了导致上下文退出的异常。如果上下文是无异常地退出的,三个参数都将为 None。

If an exception is supplied, and the method wishes to suppress the exception (i.e., prevent it from being propagated), it should return a true value. Otherwise, the exception will be processed normally upon exit from this method.
如果提供了异常,并且希望方法屏蔽此异常 (即避免其被传播),则应当返回真值。否则的话,异常将在退出此方法时按正常流程处理。

Note that __exit__() methods should not reraise the passed-in exception; this is the caller’s responsibility.
请注意 __exit__() 方法不应该重新引发被传入的异常,这是调用者的责任。

3. Context Manager Types - 上下文管理器类型

English
https://docs.python.org/3.7/library/stdtypes.html

Simplified Chinese
https://docs.python.org/zh-cn/3.7/library/stdtypes.html

Python’s with statement supports the concept of a runtime context defined by a context manager. This is implemented using a pair of methods that allow user-defined classes to define a runtime context that is entered before the statement body is executed and exited when the statement ends:
Python 的 with 语句支持通过上下文管理器所定义的运行时上下文这一概念。此对象的实现使用了一对专门方法,允许用户自定义类来定义运行时上下文,在语句体被执行前进入该上下文,并在语句执行完毕时退出该上下文:

contextmanager.__enter__()
Enter the runtime context and return either this object or another object related to the runtime context. The value returned by this method is bound to the identifier in the as clause of with statements using this context manager.
进入运行时上下文并返回此对象或关联到该运行时上下文的其他对象。此方法的返回值会绑定到使用此上下文管理器的 with 语句的 as 子句中的标识符。

An example of a context manager that returns itself is a file object. File objects return themselves from __enter__() to allow open() to be used as the context expression in a with statement.
一个返回其自身的上下文管理器的例子是 file object。文件对象会从 __enter__() 返回其自身,以允许 open() 被用作 with 语句中的上下文表达式。

An example of a context manager that returns a related object is the one returned by decimal.localcontext(). These managers set the active decimal context to a copy of the original decimal context and then return the copy. This allows changes to be made to the current decimal context in the body of the with statement without affecting code outside the with statement.
一个返回关联对象的上下文管理器的例子是 decimal.localcontext() 所返回的对象。此种管理器会将活动的 decimal 上下文设为原始 decimal 上下文的一个副本并返回该副本。这允许对 with 语句的语句体中的当前 decimal 上下文进行更改,而不会影响 with 语句以外的代码。

contextmanager.__exit__(exc_type, exc_val, exc_tb)
Exit the runtime context and return a Boolean flag indicating if any exception that occurred should be suppressed. If an exception occurred while executing the body of the with statement, the arguments contain the exception type, value and traceback information. Otherwise, all three arguments are None.
退出运行时上下文并返回一个布尔值旗标来表明所发生的任何异常是否应当被屏蔽。如果在执行 with 语句的语句体期间发生了异常,则参数会包含异常的类型、值以及回溯信息。在其他情况下三个参数均为 None

Returning a true value from this method will cause the with statement to suppress the exception and continue execution with the statement immediately following the with statement. Otherwise the exception continues propagating after this method has finished executing. Exceptions that occur during execution of this method will replace any exception that occurred in the body of the with statement.
自此方法返回一个真值将导致 with 语句屏蔽异常并继续执行紧随在 with 语句之后的语句。否则异常将在此方法结束执行后继续传播。在此方法执行期间发生的异常将会取代 with 语句的语句体中发生的任何异常。

The exception passed in should never be reraised explicitly - instead, this method should return a false value to indicate that the method completed successfully and does not want to suppress the raised exception. This allows context management code to easily detect whether or not an __exit__() method has actually failed.
传入的异常绝对不应当被显式地重新引发 - 相反地,此方法应当返回一个假值以表明方法已成功完成并且不希望屏蔽被引发的异常。这允许上下文管理代码方便地检测 __exit__() 方法是否确实已失败。

Python defines several context managers to support easy thread synchronisation, prompt closure of files or other objects, and simpler manipulation of the active decimal arithmetic context. The specific types are not treated specially beyond their implementation of the context management protocol. See the contextlib module for some examples.
Python 定义了一些上下文管理器来支持简易的线程同步、文件或其他对象的快速关闭,以及更方便地操作活动的十进制算术上下文。除了实现上下文管理协议以外,不同类型不会被特殊处理。请参阅 contextlib 模块查看相关的示例。

Python’s generators and the contextlib.contextmanager decorator provide a convenient way to implement these protocols. If a generator function is decorated with the contextlib.contextmanager decorator, it will return a context manager implementing the necessary __enter__() and __exit__() methods, rather than the iterator produced by an undecorated generator function.
Python 的 generator 和 contextlib.contextmanager 装饰器提供了实现这些协议的便捷方式。如果使用 contextlib.contextmanager 装饰器来装饰一个生成器函数,它将返回一个实现了必要的 __enter__() and __exit__() 方法的上下文管理器,而不再是由未经装饰的生成器函数所产生的迭代器。

Note that there is no specific slot for any of these methods in the type structure for Python objects in the Python/C API. Extension types wanting to define these methods must provide them as a normal Python accessible method. Compared to the overhead of setting up the runtime context, the overhead of a single class dictionary lookup is negligible.
请注意,Python/C API 中 Python 对象的类型结构中并没有针对这些方法的专门槽位。想要定义这些方法的扩展类型必须将它们作为普通的 Python 可访问方法来提供。与设置运行时上下文的开销相比,单个类字典查找的开销可以忽略不计。

4. 上下文管理器

上下文管理器在对象内实现了两个方法:__enter__() and __exit__()__enter__() 方法会在 with 的代码块执行之前执行,__exit__() 会在代码块执行结束后执行。__exit__() 方法内会自带当前对象的清理方法。

上下文可以简单地把它理解成环境。进程上下文指的是一个进程在执行的时候,CPU 的所有寄存器中的值、进程的状态以及堆栈上的内容等,当系统需要切换到其他进程时,系统会保留当前进程的上下文,也就是运行时的环境,以便再次执行该进程。

迭代器协议 (Iterator Protocol)
上下文管理器 (Context manager)
上下文管理协议 (Context Management Protocol)

上下文管理器协议是指要实现对象的 __enter__()__exit__() 方法。
上下文管理器是支持上下文管理器协议的对象。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Yongqiang Cheng

from __future__ import absolute_import
from __future__ import print_function
from __future__ import division

import os
import sys

sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/..')
current_directory = os.path.dirname(os.path.abspath(__file__))

print(16 * "++--")
print("current_directory:", current_directory)

from math import sqrt, pow


class Point(object):
    def __init__(self, x, y):
        print('initialize x and y')
        self.x, self.y = x, y

    def __enter__(self):
        print("Entering context")
        return self

    def __exit__(self, type, value, traceback):
        print("Exiting context")

    def get_distance(self):
        distance = sqrt(pow(self.x, 2) + pow(self.y, 2))
        return distance


if __name__ == '__main__':
    with Point(3, 4) as pt:
        print('distance: ', pt.get_distance())
/usr/bin/python2.7 /home/strong/tensorflow_work/R2CNN_Faster-RCNN_Tensorflow/yongqiang.py --gpu=0
++--++--++--++--++--++--++--++--++--++--++--++--++--++--++--++--
current_directory: /home/strong/tensorflow_work/R2CNN_Faster-RCNN_Tensorflow
initialize x and y
Entering context
distance:  5.0
Exiting context

Process finished with exit code 0

上面的代码定义了一个 Point 类,并实现了 __enter__()__exit__() 方法,还定义了 get_distance 方法,用于返回点到原点的距离。

我们使用 with 语句调用上下文管理器:

initialize x and y   # 调用了 __init__ 方法
Entering context     # 调用了 __enter__ 方法
distance:  5.0       # 调用了 get_distance 方法
Exiting context      # 调用了 __exit__ 方法
  • Point(3, 4) 生成了一个上下文管理器。
  • 调用上下文管理器的 __enter__() 方法,并将 __enter__() 方法的返回值赋给 as 字句中的变量 pt。
  • 执行语句体 (指 with 语句包裹起来的代码块) 内容,输出 distance。
  • 不管执行过程中是否发生异常,都执行上下文管理器的 __exit__() 方法。__exit__() 方法负责执行清理工作,如释放资源,关闭文件等。如果执行过程没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 __exit__(None, None, None)。如果执行过程中出现异常,则使用 sys.exc_info得到的异常信息为参数调用 __exit__(exc_type, exc_value, exc_traceback)
  • 出现异常时,如果 __exit__(type, value, traceback) 返回 False 或 None,则会重新抛出异常,让 with 之外的语句逻辑来处理异常。如果返回 True,则忽略异常,不再对异常进行处理。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Yongqiang Cheng

from __future__ import absolute_import
from __future__ import print_function
from __future__ import division

import os
import sys

sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/..')
current_directory = os.path.dirname(os.path.abspath(__file__))

print(16 * "++--")
print("current_directory:", current_directory)

from math import sqrt, pow


class Point(object):
    def __init__(self, x, y):
        print('initialize x and y')
        self.x, self.y = x, y

    def __enter__(self):
        print("Entering context")
        return self

    def __exit__(self, type, value, traceback):
        print("Exiting context")

    def get_distance(self):
        distance = sqrt(pow(self.x, 2) + pow(self.y, 2))
        return distance


if __name__ == '__main__':
    with Point(3, 4) as pt:
        print('length: ', pt.get_length())

/usr/bin/python2.7 /home/strong/tensorflow_work/R2CNN_Faster-RCNN_Tensorflow/yongqiang.py --gpu=0
++--++--++--++--++--++--++--++--++--++--++--++--++--++--++--++--
current_directory: /home/strong/tensorflow_work/R2CNN_Faster-RCNN_Tensorflow
initialize x and y
Entering context
Exiting context
Traceback (most recent call last):
  File "/home/strong/tensorflow_work/R2CNN_Faster-RCNN_Tensorflow/yongqiang.py", line 40, in 
    print('length: ', pt.get_length())
AttributeError: 'Point' object has no attribute 'get_length'

Process finished with exit code 1

__exit__ 方法返回的是 None (如果没有 return 语句那么方法会返回 None)。with 语句抛出了那个异常。我们对 __exit__ 方法做一些改动,让它返回 True。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Yongqiang Cheng

from __future__ import absolute_import
from __future__ import print_function
from __future__ import division

import os
import sys

sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/..')
current_directory = os.path.dirname(os.path.abspath(__file__))

print(16 * "++--")
print("current_directory:", current_directory)

from math import sqrt, pow


class Point(object):
    def __init__(self, x, y):
        print('initialize x and y')
        self.x, self.y = x, y

    def __enter__(self):
        print("Entering context")
        return self

    def __exit__(self, type, value, traceback):
        print("Exception has been handled")
        print("Exiting context")
        return True

    def get_distance(self):
        distance = sqrt(pow(self.x, 2) + pow(self.y, 2))
        return distance


if __name__ == '__main__':
    with Point(3, 4) as pt:
        print('length: ', pt.get_length())

/usr/bin/python2.7 /home/strong/tensorflow_work/R2CNN_Faster-RCNN_Tensorflow/yongqiang.py --gpu=0
++--++--++--++--++--++--++--++--++--++--++--++--++--++--++--++--
current_directory: /home/strong/tensorflow_work/R2CNN_Faster-RCNN_Tensorflow
initialize x and y
Entering context
Exception has been handled
Exiting context

Process finished with exit code 0

由于 __exit__ 方法返回了 True,因此没有异常会被 with 语句抛出。

内建对象使用 with 语句,除了自定义上下文管理器,Python 中也提供了一些内置对象,可直接用于 with 语句中,比如最常见的文件操作。

传统的文件操作经常使用 try/finally 的方式,

file = open('somefile', 'r')
try:
    for line in file:
        print(line)
finally:
    file.close()     # 确保关闭文件

将上面的代码改用 with 语句:

with open('somefile', 'r') as file:
    for line in file:
        print(line)

可以看到,通过使用 with,代码变得很简洁,而且即使处理过程发生异常,with 语句也会确保我们的文件被关闭。

  • 上下文管理器是支持上下文管理协议的对象,也就是实现了 __enter____exit__ 方法。
  • 通常,我们使用 with 语句调用上下文管理器。with 语句尤其适用于对资源进行访问的场景,确保执行过程中出现异常情况时也可以对资源进行回收,比如自动关闭文件等。
  • __enter__ 方法在 with 语句体执行前调用,with 语句将该方法的返回值赋给 as 字句中的变量,如果有 as 字句的话。
  • __exit__ 方法在退出运行时上下文时被调用,它负责执行清理工作,比如关闭文件,释放资源等。如果退出时没有发生异常,则 __exit__ 的三个参数,即 type, value 和 traceback 都为 None。如果发生异常,返回 True 表示不处理异常,否则会在退出该方法后重新抛出异常以由 with 语句之外的代码逻辑进行处理。

5. contextlib 模块

除了在类中定义 __enter____exit__ 方法来实现上下文管理器,我们还可以通过生成器函数 (也就是带有 yield 的函数) 结合装饰器来实现上下文管理器,Python 中自带的 contextlib 模块就是做这个的。

contextlib 模块提供了三个对象:装饰器 contextmanager、函数 nested 和上下文管理器 closing。其中,contextmanager 是一个装饰器,用于装饰生成器函数,并返回一个上下文管理器。需要注意的是,被装饰的生成器函数只能产生一个值,否则会产生 RuntimeError 异常。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Yongqiang Cheng

from __future__ import absolute_import
from __future__ import print_function
from __future__ import division

import os
import sys

sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/..')
current_directory = os.path.dirname(os.path.abspath(__file__))

print(16 * "++--")
print("current_directory:", current_directory)

from contextlib import contextmanager


@contextmanager
def point(x, y):
    print('before yield')
    yield x * x + y * y
    print('after yield')


if __name__ == '__main__':
    with point(3, 4) as value:
        print('value is: %s' % value)
/usr/bin/python2.7 /home/strong/tensorflow_work/R2CNN_Faster-RCNN_Tensorflow/yongqiang.py --gpu=0
++--++--++--++--++--++--++--++--++--++--++--++--++--++--++--++--
current_directory: /home/strong/tensorflow_work/R2CNN_Faster-RCNN_Tensorflow
before yield
value is: 25
after yield

Process finished with exit code 0

可以看到,yield 产生的值赋给了 as 子句中的 value 变量。

需要强调的是,虽然通过使用 contextmanager 装饰器,我们可以不必再编写 __enter____exit__ 方法,但是获取和清理资源的操作仍需要我们自己编写。获取资源的操作定义在 yield 语句之前,释放资源的操作定义在 yield 语句之后。

上下文是与当前操作相关的前一步状态和下一步状态。context 是 environment 的 snapshot。上下文是把一堆有用的状态信息放一起。context是跟随信息。操作系统调度的时候,进程切换,需要保存各相关寄存器的值。

  • 上下文管理协议 (Context Management Protocol):包含方法 enter() 和exit(),支持该协议的对象要实现这两个方法。
  • 上下文管理器 (Context Manager):支持上下文管理协议的对象,这种对象实现了enter() 和 exit() 方法。上下文管理器定义执行 with 语句时要建立的运行时上下文,负责执行 with 语句块上下文中的进入与退出操作。通常使用 with语句调用上下文管理器,也可以通过直接调用其方法来使用。
  • 运行时上下文 (runtimecontext):由上下文管理器创建,通过上下文管理器的 enter() 和exit() 方法实现,enter() 方法在语句体执行之前进入运行时上下文,exit() 在语句体执行完后从运行时上下文退出。with 语句支持运行时上下文这一概念。
  • 上下文表达式 (Context Expression):with 语句中跟在关键字 with 之后的表达式,该表达式要返回一个上下文管理器对象。
    语句体 (with-body):with 语句包裹起来的代码块,在执行语句体之前会调用上下文管理器的 enter() 方法,执行完语句体之后会执行exit() 方法。

适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。

References

https://www.python.org/dev/peps/pep-0343/
https://docs.python.org/3.7/reference/compound_stmts.html
https://docs.python.org/zh-cn/3.7/reference/compound_stmts.html
https://docs.python.org/3.7/reference/datamodel.html
https://docs.python.org/zh-cn/3.7/reference/datamodel.html
https://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/

你可能感兴趣的:(Python,3.x,-,Python,2.x)