2.1. Python的高级结构

作者 ZbigniewJę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之外的迭代器还需要具有返回迭代器(self)的方法__iter__

支持迭代在Python中是普遍的:标准库中的所有序列和无序容器都允许这样做。这个概念也延伸到其他事情:例如。 file对象支持对行的迭代。

>>> f = open('/etc/fstab')
>>> 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的“pairs”时,将创建dict

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

应该提到一个gotcha:在旧的Pythons中,索引变量(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("-- middle --")
... yield 4
... print("-- finished --")
>>> gen = f()
>>> next(gen)
-- start --
3
>>> next(gen)
-- middle --
4
>>> next(gen)
-- finished --
Traceback (most recent call last):
...
StopIteration

正常情况下在执行f()时将导致第一个print立即执行,与正常函数相反,gen被赋值而不会执行函数体中的任何语句。只有当next调用gen.next()时,才执行到第一个yield的语句。第二个next打印-- middle --,然后执行在第二个yield处停止。第三个next打印-- finished --,然后到达函数的末尾。由于未达到yield,则引发异常。

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

生成器为什么有用?如在关于迭代器的部分中所指出的,生成器函数仅仅是创建迭代器对象的不同方式。可以使用yield语句完成的任何操作也可以使用next方法完成。然而,使用函数并使解释器执行其魔术来创建迭代器具有优点。函数可以比具有所需next__iter__方法的类的定义短得多。更重要的是,生成器的作者更容易理解保存在局部变量中的状态,而不是必须用于在next

一个更广泛的问题是为什么迭代器有用?当迭代器用于控制循环时,循环变得非常简单。初始化状态、决定循环是否完成、并找到下一个值的代码被提取到一个单独的地方。这突出了循环的主体 —— 受关注的部分。此外,可以在其他地方重用迭代器代码。

2.1.1.4. 双向通信

每个yield语句都会将一个值传递给调用者。这是通过 PEP 255(在Python 2.2中实现)引入生成器的原因。但是在相反方向上的通信也是有用的。一个明显的方式是一些外部状态,一个全局变量或一个共享的可变对象。由于 PEP 342(在2.5中实现),可以进行直接通信。这是通过将先前钻孔的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--

next__next__

在Python 2.x中,检索下一个值的迭代器方法称为next它通过全局函数next隐式调用,这意味着它应该被称为__next__就像全局函数iter调用__iter__这种不一致性在Python 3.x中更正,其中it.next变为it.__next__对于其他生成器方法 —— sendthrow —— 情况更复杂,因为它们不由解释器隐式调用。然而,有一个提议的语法扩展允许continue接受一个参数,该参数将传递给循环迭代器的send如果接受此扩展程序,则gen.send很可能会成为gen.__send__生成器最后的方法close很明显命名不正确,因为它已被隐式调用。

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. 装饰

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

有两个东西隐藏在名称“装饰器”后面 - 一个是执行装饰工作的函数,即执行真正的工作,另一个是遵守装饰器语法的表达式,即at符号和名称的装饰功能。

函数可以通过使用装饰器语法的功能来装饰:

@decorator             # ②
def function(): # ①
pass
  • 函数以标准方式定义。
  • 以放置在函数定义之前的@开头的表达式是装饰器②。@之后的部分必须是一个简单的表达式,通常这只是一个函数或类的名称。首先评估此部分,在下面定义的函数准备就绪后,调用装饰器,将新定义的函数对象作为单个参数。装饰器返回的值附加到函数的原始名称。

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

在实现装饰器语法之前( PEP 318),可以通过将函数或类对象赋给临时变量然后明确调用装饰器来实现相同的效果然后将返回值分配给函数的名称。这听起来更像打字,它是,并且装饰函数加倍作为临时变量的名称必须使用至少三次,这是容易出现错误。然而,上述示例等效于:

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

装饰器可以堆叠 - 应用顺序是从下到上或从内到外。语义是这样的:最初定义的函数被用作第一装饰器的参数,不管由第一装饰器返回的是用作第二装饰器的参数,...,并且最后装饰器返回的任何内容都附加下的原始功能的名称。

选择装饰器语法的可读性。因为装饰器在函数的头部之前被指定,所以显然它不是函数体的一部分,并且清楚它仅能对整个函数进行操作。因为表达式前缀为@是很突出的,很难错过(“在你的脸”,根据PEP :))。当应用多个装饰器时,每个装饰器以容易阅读的方式放置在单独的线上。

2.1.2.1. 替换或调整原始对象

装饰器可以返回相同的函数或类对象,也可以返回完全不同的对象。在第一种情况下,装饰器可以利用函数和类对象是可变的并且添加属性的事实。向类添加一个docstring。装饰器可能做一些有用的事情,即使没有修改对象,例如在全局注册表中注册装饰类。在第二种情况下,几乎任何事情都是可能的:当用不同的东西取代原始函数或类时,新对象可以完全不同。然而,这样的行为不是装饰器的目的:它们旨在调整装饰的对象,而不是做不可预测的事情。因此,当通过将函数替换为不同的函数来“修饰”函数时,新函数通常在进行一些准备工作之后调用原始函数。同样,当一个类通过用新类替换if来“修饰”时,新类通常从原始类派生。当装饰器的目的是“每次”做某事时,喜欢将每个调用记录到装饰函数,只能使用第二类装饰器。另一方面,如果第一类型是足够的,则最好使用它,因为它更简单。

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__(docstring),__module____name__(第一个),可以将原始函数的这些属性部分“移植”到新函数函数的全名)和__annotations__(有关参数的附加信息和Python 3中可用的函数的返回值)。这可以使用functools.update_wrapper自动完成。

functools.update_wrapper(wrapper, wrapped)

“更新包装函数看起来像包装函数。

>>> import functools
>>> def replacing_decorator_with_args(arg):
... print("defining the decorator")
... def _decorator(function):
... print("doing decoration, %r" % arg)
... def _wrapper(*args, **kwargs):
... print("inside wrapper, %r %r" % (args, kwargs))
... return function(*args, **kwargs)
... return functools.update_wrapper(_wrapper, function)
... return _decorator
>>> @replacing_decorator_with_args("abc")
... def function():
... "extensive documentation"
... print("inside function")
... return 14
defining the decorator
doing decoration, 'abc'
>>> function
<function function at 0x...>
>>> print(function.__doc__)
extensive documentation

一个重要的事情是从属性列表中缺少可以复制到替换函数:参数列表。可以通过__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是对getters和setter问题的pythonic回答。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的docstring。a定义为属性允许它是即时计算的,并且具有使其为只读的副作用,因为没有定义setter。

    要有一个setter和一个getter,显然需要两个方法。自从Python 2.6以下语法是首选:

    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下的property对象,并且使用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方法。

一些较新的示例包括:

  • functools.lru_cache记住一个任意函数,保持有限的参数缓存:answer对(Python 3.2)
  • functools.total_ordering是一个类装饰器,它填充缺少的排序方法(__lt____gt____le__,.. 。)基于一个可用的(Python 2.7)。

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循环的装饰器

让我们假设我们有函数返回一个列表,并且这个列表是通过运行循环创建的。如果我们不知道需要多少对象,标准的做法是:

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)

我们的函数然后变成:

@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字符代替HTML实体的em-dash。它利用unicode字面符号使用unicode数据库中的名称(“EM DASH”)插入字符。如果直接插入Unicode字符,则不可能将其与程序源中的破折号区分开。

另见

更多示例和阅读

2.1.3. 上下文管理器

上下文管理器是具有可以在with语句中使用的__enter____exit__方法的对象:

with manager as var:
do_something(var)

是在最简单的情况下相当于

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

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

  1. 首先调用__enter__方法。它可以返回将分配给var的值。as -part是可选的:如果不存在,则__enter__返回的值将被忽略。
  2. 将执行withtry子句一样,它可以成功执行到结束,也可以breakcontinuet4>,否则它会抛出异常。无论哪种方式,在块完成后,调用__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类中。它有一个调用close__exit__方法,可以用作上下文管理器本身:

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

try..finally的常见用法是释放资源。各种不同的情况类似地实现:在__enter__阶段中,获取资源,在__exit__阶段中,释放该资源,并且如果抛出该异常,则传播该异常。和文件一样,在对象被使用之后通常有一个自然的操作,并且内置支持是最方便的。在每个版本中,Python在更多地方提供支持:

2.1.3.1. 捕获异常

When an exception is thrown in the with-block, it is passed as arguments to __exit__. 使用三个参数,与sys.exc_info()返回的参数相同:type,value,traceback。当没有抛出异常时,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__中执行。If an exception is thrown, the interpreter hands it to the wrapper through __exit__ arguments, and the wrapper function then throws it at the point of the yield statement. 通过使用生成器,上下文管理器更短和更简单。

让我们将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')

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