2.1. 高级 Python 结构

作者 Zbigniew Jędrzejewski-Szmek

本节介绍 Python 语言中的一些功能,可以认为是高级的——从某种意义上说,并非每种语言都具备这些功能,而且它们在更复杂的程序或库中更有用,但并不意味着这些功能特别专业或特别复杂。

重要的是要强调,本章纯粹是关于语言本身——关于通过特殊语法支持的功能以及 Python 标准库的功能,这些功能无法通过巧妙的外部模块来实现。

Python 编程语言及其语法的开发过程非常透明;提出的更改会从各个角度进行评估,并通过Python 增强提案进行讨论——PEPs。因此,本章中描述的功能是在证明它们确实解决了实际问题并且它们的使用尽可能简单之后才添加的。

2.1.1. 迭代器、生成器表达式和生成器

2.1.1.1. 迭代器

迭代器是一个遵循迭代器协议的对象——基本上意味着它有一个next 方法,该方法在调用时返回序列中的下一个项目,如果没有要返回的项目,则会引发 StopIteration 异常。

迭代器对象只允许循环一次。它保存单个迭代的状态(位置),或者从另一方面来说,每个遍历序列都需要一个单独的迭代器对象。这意味着我们可以多次同时遍历同一个序列。将迭代逻辑与序列分离,使我们能够拥有多种迭代方式。

在容器上调用 __iter__ 方法以创建迭代器对象是获得迭代器的最直接方法。iter 函数会为我们做到这一点,节省几个按键操作。

>>> nums = [1, 2, 3]      # note that ... varies: these are different objects
>>> iter(nums)
<...iterator object at ...>
>>> nums.__iter__()
<...iterator object at ...>
>>> nums.__reversed__()
<...reverseiterator object at ...>
>>> it = iter(nums)
>>> next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

在循环中使用时,StopIteration 会被吞并,并导致循环结束。但如果显式调用,我们可以看到,一旦迭代器耗尽,访问它就会引发异常。

使用 for..in 循环也使用 __iter__ 方法。这使我们能够透明地开始遍历序列。但是,如果我们已经拥有迭代器,我们希望能够以相同的方式在 for 循环中使用它。为了实现这一点,除了 next 之外,迭代器还需要具有名为 __iter__ 的方法,该方法返回迭代器 (self)。

对迭代的支持在 Python 中无处不在:标准库中的所有序列和无序容器都允许这样做。该概念还扩展到其他事物:例如,file 对象支持对行的迭代。

>>> with open("/etc/fstab") as f: 
... f is f.__iter__()
...
True

file 本身就是一个迭代器,它的 __iter__ 方法不会创建单独的对象:只允许单个顺序访问线程。

2.1.1.2. 生成器表达式

创建迭代器对象的第二种方式是通过**生成器表达式**,它是**列表推导**的基础。为了提高清晰度,生成器表达式必须始终包含在括号或表达式中。如果使用圆括号,则会创建生成器迭代器。如果使用方括号,则过程会被短路,我们会得到一个 list

>>> (i for i in nums)
<generator object <genexpr> at 0x...>
>>> [i for i in nums]
[1, 2, 3]
>>> list(i for i in nums)
[1, 2, 3]

列表推导语法也扩展到**字典和集合推导**。当生成器表达式包含在花括号中时,会创建一个 set。当生成器表达式包含 key:value 形式的“对”时,会创建一个 dict

>>> {i for i in range(3)}
{0, 1, 2}
>>> {i:i**2 for i in range(3)}
{0: 0, 1: 1, 2: 4}

应该提到一个陷阱:在旧版本的 Python 中,索引变量 (i) 会泄漏,在版本 >= 3 中,这个问题已修复。

2.1.1.3. 生成器

创建迭代器对象的第三种方式是调用生成器函数。**生成器**是一个包含关键字 yield 的函数。需要注意的是,仅仅存在这个关键字就完全改变了函数的本质:这个 yield 语句不必被调用,甚至不必被访问,但会导致该函数被标记为生成器。当调用普通函数时,函数体中包含的指令开始执行。当调用生成器时,执行会在函数体中的第一个指令之前停止。调用生成器函数会创建一个生成器对象,该对象遵循迭代器协议。与普通函数调用一样,允许并发和递归调用。

当调用 next 时,函数会一直执行到第一个 yield。每个遇到的 yield 语句都会给出一个值,该值成为 next 的返回值。在执行 yield 语句后,该函数的执行会被挂起。

>>> def f():
... yield 1
... yield 2
>>> f()
<generator object f at 0x...>
>>> gen = f()
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

让我们回顾一下生成器函数的单个调用的生命周期。

>>> def f():
... print("-- start --")
... yield 3
... print("-- finish --")
... yield 4
>>> gen = f()
>>> next(gen)
-- start --
3
>>> next(gen)
-- finish --
4
>>> next(gen)
Traceback (most recent call last):
...
StopIteration

与普通函数相反,在普通函数中,执行 f() 会立即导致第一个 print 被执行,而 gen 会在不执行函数体中的任何语句的情况下被赋值。只有当 gen.__next__()next 调用时,才会执行到第一个 yield 之前的语句。第二个 next 会打印 -- finish --,并在第二个 yield 上停止执行。第三个 next 会从函数末尾掉下来。由于没有到达 yield,因此会引发异常。

当控制权传递给调用者时,函数在 yield 之后会发生什么?每个生成器的状态都存储在生成器对象中。从生成器函数的角度来看,它看起来几乎像是运行在单独的线程中,但这只是一个幻觉:执行是严格的单线程,但解释器会在请求下一个值的间隔中保存和恢复状态。

为什么生成器有用?正如在迭代器部分中提到的,生成器函数只是创建迭代器对象的一种不同方式。所有可以用 yield 语句完成的事情,也可以用 next 方法完成。然而,使用函数并让解释器执行它的魔法来创建迭代器有其优点。函数可以比包含所需 next__iter__ 方法的类的定义短得多。更重要的是,对于生成器的作者来说,理解存储在局部变量中的状态比使用实例属性要容易得多,而实例属性必须用于在对迭代器对象上的 next 的连续调用之间传递数据。

一个更广泛的问题是为什么迭代器有用?当迭代器用于为循环提供动力时,循环变得非常简单。初始化状态、确定循环是否结束以及查找下一个值的代码被提取到一个单独的位置。这突出了循环的主体——有趣的部分。此外,可以将迭代器代码重用在其他地方。

2.1.1.4. 双向通信

每个 yield 语句都会将一个值传递给调用者。 这是 PEP 255 引入生成器的主要原因。 但是,反方向的通信也很有用。 一个明显的方法是使用一些外部状态,例如全局变量或共享可变对象。 由于 PEP 342,直接通信是可能的。 它是通过将以前普通的 yield 语句变成表达式来实现的。 当生成器在 yield 语句之后恢复执行时,调用者可以调用生成器对象上的方法来向生成器中传递一个值,该值将由 yield 语句返回,或者使用另一种方法将异常注入生成器。

第一个新方法是 send(value),它类似于 next(),但会将 value 传递给生成器,用于 yield 表达式的值。 事实上,g.next()g.send(None) 是等效的。

第二个新方法是 throw(type, value=None, traceback=None),它等效于

raise type, value, traceback

yield 语句的点。

raise(它立即从当前执行点引发异常)不同,throw() 首先恢复生成器,然后才引发异常。 选择词语“throw”是因为它暗示着将异常放在另一个位置,并且与其他语言中的异常有关。

生成器内部发生异常会怎样? 异常可以是显式引发的,也可以是在执行某些语句时引发的,或者可以通过 throw() 方法在 yield 语句的点注入。 在任何情况下,此类异常都以标准方式传播:它可以被 exceptfinally 子句拦截,否则会导致生成器函数的执行中止并在调用者中传播。

为了完整起见,值得一提的是,生成器迭代器也有一个 close() 方法,它可以用来强制生成器立即完成,否则生成器将能够提供更多值。 它允许生成器 __del__ 方法销毁保存生成器状态的对象。 让我们定义一个生成器,它只打印通过 send 和 throw 传入的内容。

>>> import itertools
>>> def g():
... print('--start--')
... for i in itertools.count():
... print('--yielding %i--' % i)
... try:
... ans = yield i
... except GeneratorExit:
... print('--closing--')
... raise
... except Exception as e:
... print('--yield raised %r--' % e)
... else:
... print('--yield returned %s--' % ans)
>>> it = g()
>>> next(it)
--start--
--yielding 0--
0
>>> it.send(11)
--yield returned 11--
--yielding 1--
1
>>> it.throw(IndexError)
--yield raised IndexError()--
--yielding 2--
2
>>> it.close()
--closing--

2.1.1.5. 链接生成器

注意

这是对 PEP 380 的预览(尚未实施,但已接受用于 Python 3.3)。

假设我们正在编写一个生成器,并且我们希望生成由第二个生成器(**子生成器**)生成的多个值。 如果只关心值的生成,那么可以使用循环来轻松实现,例如

subgen = some_other_generator()
for v in subgen:
yield v

但是,如果子生成器要与调用者在调用 send()throw()close() 时正确交互,事情会变得相当困难。 yield 语句必须由类似于上一节中定义的用于“调试”生成器函数的 try..except..finally 结构保护。 此类代码在 PEP 380#id13 中提供,这里只需说明在 Python 3.3 中引入了用于从子生成器正确生成的新语法

yield from some_other_generator()

它的行为类似于上面的显式循环,反复从 some_other_generator 生成值,直到它被耗尽,但也将 sendthrowclose 转发到子生成器。

2.1.2. 装饰器

由于函数和类是对象,因此可以将它们传递。 由于它们是可变对象,因此可以修改它们。 在构造函数或类对象之后但在将其绑定到其名称之前修改它们的行为称为装饰。

名称“装饰器”后面隐藏着两件事:一个是执行装饰工作的函数,即执行实际工作的函数,另一个是符合装饰器语法的表达式,即符号“@”和装饰函数的名称。

可以使用函数的装饰器语法来装饰函数

@decorator             # ②
def function(): # ①
pass
  • 函数以标准方式定义。 ①

  • 放置在函数定义之前的以 @ 开头的表达式是装饰器 ②。 @ 之后的必须是简单的表达式,通常只是一个函数或类的名称。 这部分首先被评估,并在下面定义的函数准备好之后,使用新定义的函数对象作为单个参数调用装饰器。 装饰器返回的值将附加到函数的原始名称。

装饰器可以应用于函数和类。 对于类,语义相同:原始类定义用作调用装饰器的参数,并且任何返回的值都将分配到原始名称下。

在实施装饰器语法之前 (PEP 318),可以通过将函数或类对象分配给一个临时变量,然后显式调用装饰器,然后将返回值分配给函数的名称来实现相同的效果。 这听起来像是打字更多,实际上也是,而且被装饰函数的名称作为临时变量必须至少使用三次,这很容易出错。 尽管如此,上面的示例等同于

def function():                  # ①
pass
function = decorator(function) # ②

装饰器可以堆叠:应用顺序是从下到上,或者从里到外。 语义是这样的:最初定义的函数用作第一个装饰器的参数,第一个装饰器返回的任何内容都用作第二个装饰器的参数,……,最后一个装饰器返回的任何内容都将附加到原始函数的名称下。

选择装饰器语法是为了它的可读性。 由于装饰器在函数头之前指定,因此很明显它不是函数体的一部分,并且很清楚它只能对整个函数进行操作。 由于表达式以 @ 为前缀,因此它很突出,并且很难错过(根据 PEP,它是“显眼”的)。 当应用多个装饰器时,每个装饰器都放置在单独的行上,以便于阅读。

2.1.2.1. 替换或调整原始对象

装饰器可以返回相同的函数或类对象,也可以返回完全不同的对象。 在第一种情况下,装饰器可以利用函数和类对象是可变的事实,并添加属性,例如向类添加文档字符串。 即使不修改对象,装饰器也可能做一些有用的事情,例如在全局注册表中注册被装饰的类。 在第二种情况下,实际上任何事情都可能发生:当用不同的东西替换原始函数或类时,新对象可以是完全不同的。 尽管如此,这种行为不是装饰器的目的:它们旨在调整被装饰的对象,而不是做一些不可预测的事情。 因此,当一个函数被“装饰”为用另一个函数替换它时,新函数通常在执行一些准备工作后调用原始函数。 同样,当一个类被“装饰”为用一个新类替换它时,新类通常从原始类派生。 当装饰器的目的是做一些“每次”都做的事情时,例如记录对被装饰函数的每次调用,只能使用第二种类型的装饰器。 另一方面,如果第一种类型足够,最好使用它,因为它更简单。

2.1.2.2. 用类和函数实现装饰器

装饰器的唯一要求是它们可以使用单个参数调用。 这意味着装饰器可以作为普通函数实现,或者作为具有 __call__ 方法的类实现,或者理论上,甚至作为 lambda 函数实现。

让我们比较函数和类方法。 装饰器表达式(@ 之后的部分)可以是简单的名称,也可以是调用。 纯名称方法很好(打字更少,看起来更干净,等等),但只有在不需要参数来定制装饰器时才有可能。 作为函数编写的装饰器可以在这两种情况下使用

>>> def simple_decorator(function):
... print("doing decoration")
... return function
>>> @simple_decorator
... def function():
... print("inside function")
doing decoration
>>> function()
inside function
>>> def decorator_with_arguments(arg):
... print("defining the decorator")
... def _decorator(function):
... # in this inner function, arg is available too
... print("doing decoration, %r" % arg)
... return function
... return _decorator
>>> @decorator_with_arguments("abc")
... def function():
... print("inside function")
defining the decorator
doing decoration, 'abc'
>>> function()
inside function

上面的两个微不足道的装饰器属于返回原始函数的装饰器类别。 如果它们要返回一个新函数,则需要额外的嵌套级别。 在最坏的情况下,需要三个嵌套函数级别。

>>> def replacing_decorator_with_args(arg):
... print("defining the decorator")
... def _decorator(function):
... # in this inner function, arg is available too
... print("doing decoration, %r" % arg)
... def _wrapper(*args, **kwargs):
... print("inside wrapper, %r %r" % (args, kwargs))
... return function(*args, **kwargs)
... return _wrapper
... return _decorator
>>> @replacing_decorator_with_args("abc")
... def function(*args, **kwargs):
... print("inside function, %r %r" % (args, kwargs))
... return 14
defining the decorator
doing decoration, 'abc'
>>> function(11, 12)
inside wrapper, (11, 12) {}
inside function, (11, 12) {}
14

定义 _wrapper 函数是为了接受所有位置参数和关键字参数。 通常我们不知道被装饰函数应该接受什么参数,因此包装器函数只将所有内容传递给被包装函数。 一个不幸的结果是,明显的参数列表具有误导性。

与定义为函数的装饰器相比,定义为类的复杂装饰器更简单。当创建对象时,__init__ 方法只能返回 None,并且创建的对象类型不能更改。这意味着当装饰器定义为类时,使用无参数形式没有太大意义:最终装饰后的对象将仅仅是装饰类的实例,由构造函数调用返回,这并不是很有用。因此,足以讨论基于类的装饰器,其中参数在装饰器表达式中给出,装饰器 __init__ 方法用于装饰器构建。

>>> class decorator_class(object):
... def __init__(self, arg):
... # this method is called in the decorator expression
... print("in decorator init, %s" % arg)
... self.arg = arg
... def __call__(self, function):
... # this method is called to do the job
... print("in decorator call, %s" % self.arg)
... return function
>>> deco_instance = decorator_class('foo')
in decorator init, foo
>>> @deco_instance
... def function(*args, **kwargs):
... print("in function, %s %s" % (args, kwargs))
in decorator call, foo
>>> function()
in function, () {}

与正常规则相反 (PEP 8),作为类编写的装饰器更像函数,因此它们的名称通常以小写字母开头。

实际上,仅仅为了创建一个返回原始函数的装饰器而创建一个新类没有太大意义。对象应该保存状态,而此类装饰器在装饰器返回新对象时更有用。

>>> class replacing_decorator_class(object):
... def __init__(self, arg):
... # this method is called in the decorator expression
... print("in decorator init, %s" % arg)
... self.arg = arg
... def __call__(self, function):
... # this method is called to do the job
... print("in decorator call, %s" % self.arg)
... self.function = function
... return self._wrapper
... def _wrapper(self, *args, **kwargs):
... print("in the wrapper, %s %s" % (args, kwargs))
... return self.function(*args, **kwargs)
>>> deco_instance = replacing_decorator_class('foo')
in decorator init, foo
>>> @deco_instance
... def function(*args, **kwargs):
... print("in function, %s %s" % (args, kwargs))
in decorator call, foo
>>> function(11, 12)
in the wrapper, (11, 12) {}
in function, (11, 12) {}

这样的装饰器几乎可以做任何事情,因为它可以修改原始函数对象和修改参数,调用或不调用原始函数,并在之后修改返回值。

2.1.2.3. 复制原始函数的文档字符串和其他属性

当装饰器返回一个新函数来替换原始函数时,一个不幸的后果是原始函数名称、原始文档字符串、原始参数列表都会丢失。这些原始函数的属性可以通过设置 __doc__(文档字符串)、__module____name__(函数的完整名称)以及 __annotations__(Python 3 中有关参数和函数返回值的额外信息)来部分“移植”到新函数。这可以通过使用 functools.update_wrapper 自动完成。

在可以复制到替换函数的属性列表中,缺少一个重要内容:参数列表。可以通过 __defaults____kwdefaults__ 属性修改参数的默认值,但不幸的是,参数列表本身无法设置为属性。这意味着 help(function) 将显示一个无用的参数列表,这将让函数用户感到困惑。一个有效但难看的解决方法是使用 eval 动态创建包装器。这可以通过使用外部 decorator 模块来实现自动化。它为 decorator 装饰器提供支持,该装饰器接受一个包装器并将其转换为一个保留函数签名的装饰器。

总而言之,装饰器应该始终使用 functools.update_wrapper 或其他一些复制函数属性的方法。

2.1.2.4. 标准库中的示例

首先,应该提到在标准库中提供了一些有用的装饰器。有三个装饰器实际上构成了语言的一部分。

  • classmethod 使方法成为“类方法”,这意味着它可以在不创建类实例的情况下调用。当调用普通方法时,解释器将实例对象插入为第一个位置参数 self。当调用类方法时,类本身作为第一个参数给出,通常称为 cls

    类方法仍然可以通过类的命名空间访问,因此它们不会污染模块的命名空间。类方法可以用于提供替代构造函数。

    class Array(object):
    
    def __init__(self, data):
    self.data = data
    @classmethod
    def fromfile(cls, file):
    data = numpy.load(file)
    return cls(data)

    这比对 __init__ 使用大量标志更简洁。

  • staticmethod 应用于方法,使其成为“静态方法”,即基本上是一个普通函数,但可以通过类的命名空间访问。当函数仅在该类内部需要时(其名称将以 _ 为前缀),或者当我们希望用户认为该方法与该类相关联,即使其实现不需要该类时,这都很有用。

  • property 是 Python 对 getter 和 setter 问题的答案。使用 property 装饰的方法将成为一个 getter,它会在属性访问时自动调用。

    >>> class A(object):
    
    ... @property
    ... def a(self):
    ... "an important attribute"
    ... return "a value"
    >>> A.a
    <property object at 0x...>
    >>> A().a
    'a value'

    在此示例中,A.a 是一个只读属性。它也被记录在案:help(A) 包括从 getter 方法获取的属性 a 的文档字符串。将 a 定义为属性允许它在运行时计算,并且具有使其成为只读的副作用,因为没有定义 setter。

    要拥有 setter 和 getter,显然需要两种方法。

    class Rectangle(object):
    
    def __init__(self, edge):
    self.edge = edge
    @property
    def area(self):
    """Computed area.
    Setting this updates the edge length to the proper value.
    """
    return self.edge**2
    @area.setter
    def area(self, area):
    self.edge = area ** 0.5

    它的工作原理是,property 装饰器用属性对象替换 getter 方法。该对象反过来有三个方法,gettersetterdeleter,它们可以用作装饰器。它们的工作是设置属性对象的 getter、setter 和 deleter(存储为属性 fgetfsetfdel)。getter 可以像上面示例中创建对象时那样设置。在定义 setter 时,我们已经在 area 下拥有了属性对象,我们通过使用 setter 方法将 setter 添加到其中。所有这一切都发生在我们创建类的时候。

    之后,当类的实例创建后,属性对象很特殊。当解释器执行属性访问、赋值或删除时,该工作将委托给属性对象的方法。

    为了使一切都清清楚楚,让我们定义一个“调试”示例。

    >>> class D(object):
    
    ... @property
    ... def a(self):
    ... print("getting 1")
    ... return 1
    ... @a.setter
    ... def a(self, value):
    ... print("setting %r" % value)
    ... @a.deleter
    ... def a(self):
    ... print("deleting")
    >>> D.a
    <property object at 0x...>
    >>> D.a.fget
    <function ...>
    >>> D.a.fset
    <function ...>
    >>> D.a.fdel
    <function ...>
    >>> d = D() # ... varies, this is not the same `a` function
    >>> d.a
    getting 1
    1
    >>> d.a = 2
    setting 2
    >>> del d.a
    deleting
    >>> d.a
    getting 1
    1

    属性有点偏离了装饰器语法。装饰器语法的一个前提——名称不重复——被违反了,但到目前为止还没有更好的发明。对 getter、setter 和 deleter 方法使用相同的名称是良好的风格。

一些更新的示例包括

2.1.2.5. 函数的弃用

假设我们要在第一次调用我们不再喜欢的函数时在 stderr 上打印一个弃用警告。如果我们不想修改函数,我们可以使用装饰器。

class deprecated(object):
"""Print a deprecation warning once on first use of the function.
>>> @deprecated() # doctest: +SKIP
... def f():
... pass
>>> f() # doctest: +SKIP
f is deprecated
"""
def __call__(self, func):
self.func = func
self.count = 0
return self._wrapper
def _wrapper(self, *args, **kwargs):
self.count += 1
if self.count == 1:
print(self.func.__name__, 'is deprecated')
return self.func(*args, **kwargs)

它也可以作为函数实现。

def deprecated(func):
"""Print a deprecation warning once on first use of the function.
>>> @deprecated # doctest: +SKIP
... def f():
... pass
>>> f() # doctest: +SKIP
f is deprecated
"""
count = [0]
def wrapper(*args, **kwargs):
count[0] += 1
if count[0] == 1:
print(func.__name__, 'is deprecated')
return func(*args, **kwargs)
return wrapper

2.1.2.6. 一个 while-loop 移除装饰器

假设我们有一个函数返回一个事物列表,而这个列表是由循环创建的。如果我们不知道需要多少个对象,标准的做法是类似于

def find_answers():
answers = []
while True:
ans = look_for_next_answer()
if ans is None:
break
answers.append(ans)
return answers

这很好,只要循环体比较紧凑。一旦它变得更复杂,就像在实际代码中经常发生的那样,这就会变得难以阅读。我们可以使用 yield 语句来简化它,但用户必须显式地调用 list(find_answers())

我们可以定义一个装饰器,它为我们构造列表

def vectorized(generator_func):
def wrapper(*args, **kwargs):
return list(generator_func(*args, **kwargs))
return functools.update_wrapper(wrapper, generator_func)

我们的函数 then 变成

@vectorized
def find_answers():
while True:
ans = look_for_next_answer()
if ans is None:
break
yield ans

2.1.2.7. 插件注册系统

这是一个类装饰器,它不修改类,只是将它放在一个全局注册表中。它属于返回原始对象的装饰器类别

class WordProcessor(object):
PLUGINS = []
def process(self, text):
for plugin in self.PLUGINS:
text = plugin().cleanup(text)
return text
@classmethod
def plugin(cls, plugin):
cls.PLUGINS.append(plugin)
@WordProcessor.plugin
class CleanMdashesExtension(object):
def cleanup(self, text):
return text.replace('&mdash;', u'\N{em dash}')

这里我们使用一个装饰器来分散插件的注册。我们用名词而不是动词来调用装饰器,因为我们用它来声明我们的类是 WordProcessor 的插件。方法 plugin 只是将类追加到插件列表中。

关于插件本身的一句话:它用真正的 Unicode em-dash 字符替换 HTML 实体中的 em-dash。它利用了 unicode 字面量符号,通过在 unicode 数据库中使用其名称(“EM DASH”)来插入字符。如果 Unicode 字符直接插入,则无法在程序源代码中将其与 en-dash 区分开来。

另请参阅

更多示例和阅读

2.1.3. 上下文管理器

上下文管理器是一个带有 __enter____exit__ 方法的对象,它可以在 with 语句中使用

with manager as var:
do_something(var)

在最简单的情况下等效于

var = manager.__enter__()
try:
do_something(var)
finally:
manager.__exit__()

换句话说,在 PEP 343 中定义的上下文管理器协议允许将 try..except..finally 结构中无聊的部分提取到一个单独的类中,只留下有趣的 do_something 块。

  1. __enter__ 方法首先被调用。它可以返回一个值,该值将被赋值给 varas 部分是可选的:如果它不存在,则 __enter__ 返回的值将被简单地忽略。

  2. with 下面的代码块被执行。就像使用 try 子句一样,它可以成功执行到结尾,也可以 breakcontinuereturn,或者它可以抛出异常。无论哪种方式,在代码块完成后,__exit__ 方法将被调用。如果抛出了异常,则有关异常的信息将被传递给 __exit__,这将在下一小节中描述。在正常情况下,可以忽略异常,就像在 finally 子句中一样,并且将在 __exit__ 完成后重新抛出。

假设我们想要确保文件在完成写入后立即关闭

>>> class closing(object):
... def __init__(self, obj):
... self.obj = obj
... def __enter__(self):
... return self.obj
... def __exit__(self, *args):
... self.obj.close()
>>> with closing(open('/tmp/file', 'w')) as f:
... f.write('the contents\n')

在这里,我们确保当 with 代码块退出时,会调用 f.close()。由于关闭文件是一种非常常见的操作,因此 file 类中已经存在对这种操作的支持。它有一个 __exit__ 方法,该方法调用 close,并且可以作为上下文管理器本身使用

>>> with open('/tmp/file', 'a') as f:
... f.write('more contents\n')

try..finally 的常见用途是释放资源。各种不同的情况都是以类似的方式实现的:在 __enter__ 阶段获取资源,在 __exit__ 阶段释放资源,如果抛出异常,则传播异常。与文件一样,在对象使用完后通常会有一个自然的执行操作,并且最方便的做法是内置支持。随着每个版本的发布,Python 在更多地方提供了支持

2.1.3.1. 捕获异常

当在 with 代码块中抛出异常时,它将作为参数传递给 __exit__。使用三个参数,与 sys.exc_info() 返回的相同:类型、值、回溯。当没有抛出异常时, None 用于所有三个参数。上下文管理器可以通过从 __exit__ 返回一个真值来“吞掉”异常。可以轻松地忽略异常,因为如果 __exit__ 不使用 return 并且只是从结尾掉落,则将返回 None,一个假值,因此异常将在 __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']

2.1.3.2. 使用生成器定义上下文管理器

在讨论生成器时,我们说过,我们更喜欢使用生成器而不是用类实现的迭代器,因为生成器更短、更简洁,并且状态以局部变量而不是实例变量的形式存储。另一方面,正如在双向通信中描述的那样,生成器与其调用者之间的数据流可以是双向的。这包括异常,可以抛出到生成器中。我们希望将上下文管理器实现为特殊的生成器函数。事实上,生成器协议被设计为支持这种用例。

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

contextlib.contextmanager助手接收一个生成器并将其转换为上下文管理器。生成器必须遵循一些规则,这些规则由包装函数强制执行——最重要的是,它必须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')

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