Skip to content

Latest commit

 

History

History
1289 lines (979 loc) · 47.5 KB

File metadata and controls

1289 lines (979 loc) · 47.5 KB

五、装饰器——通过装饰实现代码重用

在本章中,您将学习 Python 装饰器。装饰器本质上是函数/类包装器,可用于在执行之前修改输入、输出,甚至修改函数/类本身。通过使用单独的函数调用内部函数或通过 mixin,也可以轻松实现这种类型的包装。与许多 Python 构造一样,decorator 不是实现目标的唯一方法,但在许多情况下确实很方便。

虽然您可以在不太了解装饰器的情况下完美地生活,但装饰器给了您很多“重用能力”,因此在诸如 web 框架之类的框架库中被大量使用。Python 实际上附带了一些有用的 decorator,最著名的是propertydecorator。

但是,有一些特殊性需要注意:包装一个函数会创建一个新函数,并且使其更难访问内部函数及其属性。Python 的help(function)功能就是一个例子;默认情况下,您将丢失函数属性,例如帮助文本和函数所在的模块。

本章将介绍函数和类装饰器的用法,以及在类中装饰函数时需要了解的复杂细节。

以下是所涵盖的主题:

  • 装饰功能
  • 装饰类函数
  • 装饰类
  • 使用类作为装饰器
  • Python 标准库中有用的装饰器

装饰功能

本质上,装饰器只不过是一个函数或类包装器。如果我们有一个名为spam的函数和一个名为eggs的装饰器,那么下面的将用eggs装饰spam

spam = eggs(spam)

为了使语法更易于使用,Python 为这种情况提供了一种特殊的语法。因此,不必在函数下面添加前一行,只需使用@运算符修饰函数即可:

@eggs
def spam():
    pass

装饰器只接收函数并返回一个通常不同的函数。最简单的装饰器是:

def eggs(function):
    return function

看看前面的例子,我们意识到它将spam作为function的参数,并再次返回该函数,实际上什么也没有改变。然而,大多数装饰器嵌套函数。以下装饰程序将打印发送给spam的所有参数,并将其未经修改地传递给spam

>>> import functools

>>> def eggs(function):
...    @functools.wraps(function)
...    def _eggs(*args, **kwargs):
...        print('%r got args: %r and kwargs: %r' % (
...            function.__name__, args, kwargs))
...        return function(*args, **kwargs)
...
...    return _eggs

>>> @eggs
... def spam(a, b, c):
...     return a * b + c

>>> spam(1, 2, 3)
'spam' got args: (1, 2, 3) and kwargs: {}
5

这应该表明装饰器的功能有多强大。通过修改*args**kwargs,您可以完全添加、修改和删除参数。此外,还可以修改 return 语句。如果您愿意,可以返回完全不同的内容,而不是return function(...)

为什么 functools.wrapps 很重要

无论何时您编写装饰程序,始终确保添加functools.wraps来包装内部函数。如果不包装它,您将丢失原始函数的所有属性,这可能会导致混淆。请看下面没有functools.wraps的代码:

>>> def eggs(function):
...    def _eggs(*args, **kwargs):
...        return function(*args, **kwargs)
...    return _eggs

>>> @eggs
... def spam(a, b, c):
...     '''The spam function Returns a * b + c'''
...     return a * b + c

>>> help(spam)
Help on function _eggs in module ...:
<BLANKLINE>
_eggs(*args, **kwargs)
<BLANKLINE>

>>> spam.__name__
'_eggs'

现在,我们的spam方法不再有文档,名称也不见了。已重命名为_eggs。因为我们确实在调用_eggs,这是可以理解的,但是对于依赖于此信息的代码来说,这是非常不方便的。现在,我们将尝试相同的代码,但有细微的区别;我们将使用functools.wraps

>>> import functools

>>> def eggs(function):
...     @functools.wraps(function)
...     def _eggs(*args, **kwargs):
...         return function(*args, **kwargs)
...     return _eggs

>>> @eggs
... def spam(a, b, c):
...     '''The spam function Returns a * b + c'''
...     return a * b + c

>>> help(spam)
Help on function spam in module ...:
<BLANKLINE>
spam(a, b, c)
 The spam function Returns a * b + c
<BLANKLINE>

>>> spam.__name__
'spam'

在没有任何进一步更改的情况下,我们现在有了文档和预期的函数名。functools.wraps的工作并不是什么神奇的事情;它只是复制和更新几个属性。具体而言,将复制以下属性:

  • __doc__
  • __name__
  • __module__
  • __annotations__
  • __qualname__

此外,使用_eggs.__dict__.update(spam.__dict__)更新__dict__,并添加一个名为__wrapped__的新属性,该属性包含原始的(spam在本例中)函数。Python 发行版的functools.py文件中提供了实际的wraps函数。

装饰师如何发挥作用?

装饰器的用例非常丰富,但一些最有用的用例是调试。更广泛的例子将在第 11 章调试-解决 bug中介绍,但我可以给您一个如何使用装饰器跟踪代码执行情况的预览。

让我们假设您有一组函数,它们可能被调用,也可能不被调用,并且您不完全确定每个函数得到的是什么类型的输入和输出。在这种情况下,您当然可以修改函数并在开头和结尾添加一些 print 语句来打印输出。然而,这很快就会变得单调乏味,这是一个简单的装饰师可以轻松完成相同工作的案例之一。

在这个例子中,我们使用了一个非常简单的函数,但我们都知道,在现实生活中,我们并不总是那么幸运:

>>> def spam(eggs):
...     return 'spam' * (eggs % 5)
...
>>> output = spam(3)

让我们以简单的spam函数为例,添加一些输出,以便查看内部发生的情况:

>>> def spam(eggs):
...     output = 'spam' * (eggs % 5)
...     print('spam(%r): %r' % (eggs, output))
...     return output
...
>>> output = spam(3)
spam(3): 'spamspamspam'

虽然这是可行的,但有一个小装饰师来处理这个问题不是更好吗?

>>> def debug(function):
...     @functools.wraps(function)
...     def _debug(*args, **kwargs):
...         output = function(*args, **kwargs)
...         print('%s(%r, %r): %r' % (function.__name__, args, kwargs, output))
...         return output
...     return _debug
...
>>>
>>> @debug
... def spam(eggs):
...     return 'spam' * (eggs % 5)
...
>>> output = spam(3)
spam((3,), {}): 'spamspamspam'

现在我们有了一个 decorator,我们可以轻松地为任何打印输入、输出和函数名的函数重用它。这种类型的装饰器对于日志记录应用也非常有用,我们将在第 10 章测试和日志记录–为 bug 做准备中看到。需要注意的是,即使您无法修改包含原始代码的模块,也可以使用此示例。我们可以在本地包装函数,甚至在需要时对模块进行猴子补丁:

import some_module

# Regular call
some_module.some_function()

# Wrap the function
debug_some_function = debug(some_module.some_function)

# Call the debug version
debug_some_function()

# Monkey patch the original module
some_module.some_function = debug_some_function

# Now this calls the debug version of the function
some_module.some_function()

当然,猴子补丁在生产代码中不是一个好主意,但在调试时它可能非常有用。

使用装饰器进行记忆

记忆是一个简单的技巧,可以让一些代码运行得更快一些。这里的基本技巧是存储输入和预期输出的映射,以便只需计算一次值。这种技术最常见的例子之一是在演示天真(递归)的斐波那契函数时:

>>> import functools

>>> def memoize(function):
...     function.cache = dict()
...
...     @functools.wraps(function)
...     def _memoize(*args):
...         if args not in function.cache:
...             function.cache[args] = function(*args)
...         return function.cache[args]
...     return _memoize

>>> @memoize
... def fibonacci(n):
...     if n < 2:
...         return n
...     else:
...         return fibonacci(n - 1) + fibonacci(n - 2)

>>> for i in range(1, 7):
...     print('fibonacci %d: %d' % (i, fibonacci(i)))
fibonacci 1: 1
fibonacci 2: 1
fibonacci 3: 2
fibonacci 4: 3
fibonacci 5: 5
fibonacci 6: 8

>>> fibonacci.__wrapped__.cache
{(5,): 5, (0,): 0, (6,): 8, (1,): 1, (2,): 1, (3,): 2, (4,): 3}

虽然这个示例在没有任何记忆的情况下工作得很好,但对于较大的数字,它会杀死系统。对于n=2,函数将递归执行fibonacci(n - 1)fibonacci(n - 2),有效地给出指数时间复杂度。同样,对于n=30,斐波那契函数有效地被调用了 2692537 次,这仍然是可行的。在n=40,您需要花费相当长的时间来计算。

然而,记忆化的版本甚至都不会流汗,只需要对n=30执行31次。

此装饰器还显示了如何将上下文附加到函数本身。在这种情况下,cache 属性成为内部(包装的fibonacci函数的属性,因此不同对象的额外memoize装饰器不会与任何其他装饰函数冲突。

然而,请注意,自 Python 在 Python 3.2 中引入了lru_cache(最近使用最少的缓存)以来,自行实现记忆功能通常不再有用。lru_cache与前面的记忆功能类似,但更高级一些。它只维护一个固定的(128默认值)缓存大小以节省内存,并使用一些统计信息来检查是否应该增加缓存大小。

为了演示lru_cache如何在内部工作,我们将计算fibonacci(100),这将使我们的计算机在没有任何缓存的情况下一直处于忙碌状态。此外,为了确保我们能够实际看到fibonacci函数被调用了多少次,我们将添加一个额外的装饰器,用于跟踪计数,如下所示:

>>> import functools

# Create a simple call counting decorator
>>> def counter(function):
...     function.calls = 0
...     @functools.wraps(function)
...     def _counter(*args, **kwargs):
...         function.calls += 1
...         return function(*args, **kwargs)
...     return _counter

# Create a LRU cache with size 3 
>>> @functools.lru_cache(maxsize=3)
... @counter
... def fibonacci(n):
...     if n < 2:
...         return n
...     else:
...         return fibonacci(n - 1) + fibonacci(n - 2)

>>> fibonacci(100)
354224848179261915075

# The LRU cache offers some useful statistics
>>> fibonacci.cache_info()
CacheInfo(hits=98, misses=101, maxsize=3, currsize=3)

# The result from our counter function which is now wrapped both by
# our counter and the cache
>>> fibonacci.__wrapped__.__wrapped__.calls
101

你可能想知道为什么我们只需要 101 个缓存大小为3的调用。这是因为我们递归地只需要n - 1n - 2,所以在这种情况下我们不需要更大的缓存。但对于其他人来说,这仍然是有用的。

此外,本示例还显示了对单个函数使用两个装饰器的情况。你可以把它们看作是洋葱的一层。第一层是外层,它朝里工作。调用fibonacci时,会首先调用lru_cache,因为它是列表中的第一个装饰器。假设还没有可用的缓存,将调用counter装饰器。在计数器内,将调用实际的fibonacci函数。

返回值的顺序与返回值的顺序相反;fibonacci将其值返回给counter,由counter将该值传递给lru_cache

具有(可选)参数的装饰器

前面的示例大多使用没有任何参数的简单修饰符。正如我们在lru_cache中已经看到的,装饰器也可以接受参数,因为它们只是常规函数,但这为装饰器增加了一个额外的层。这意味着添加一个参数可以非常简单,如下所示:

>>> import functools

>>> def add(extra_n=1):
...     'Add extra_n to the input of the decorated function'
...
...     # The inner function, notice that this is the actual
...     # decorator
...     def _add(function):
...         # The actual function that will be called
...         @functools.wraps(function)
...         def __add(n):
...             return function(n + extra_n)
...
...         return __add
...
...     return _add

>>> @add(extra_n=2)
... def eggs(n):
...     return 'eggs' * n

>>> eggs(2)
'eggseggseggseggs'

然而,可选参数是另一回事,因为它们使额外的函数层成为可选的。有了参数,您需要三层,但没有参数,您只需要两层。由于 decorator 本质上是返回函数的常规函数,因此不同之处在于根据参数返回子函数或子函数。这只留下了一个问题,即检测参数是函数还是常规参数。为了举例说明,实际调用的参数如下所示:

add(extra_n=2)(eggs)(2)

而不带参数的调用如下所示:

add(eggs)(2)

为了检测是否使用函数或常规参数作为参数调用装饰器,我们有几个选项,在我看来没有一个是完全理想的:

  • 使用关键字参数作为装饰参数,使常规参数始终是函数
  • 检测第一个也是唯一一个参数是否可调用

在我的观点中,第一个使用关键字参数的方法是两个选项中更好的一个,因为它更加明确,并且不太容易混淆。如果出于某种原因,您的参数也可以调用,那么第二个选项可能会有问题。

使用第一种方法,normal(非关键字)参数必须是修饰函数,其他两个检查仍然可以应用。我们仍然可以检查函数是否确实可调用,以及是否只有一个参数可用。以下是使用前一示例的修改版本的示例:

>>> import functools

>>> def add(*args, **kwargs):
...     'Add n to the input of the decorated function'
...
...     # The default kwargs, we don't store this in kwargs
...     # because we want to make sure that args and kwargs
...     # can't both be filled
...     default_kwargs = dict(n=1)
...
...     # The inner function, notice that this is actually a
...     # decorator itself
...     def _add(function):
...         # The actual function that will be called
...         @functools.wraps(function)
...         def __add(n):
...             default_kwargs.update(kwargs)
...             return function(n + default_kwargs['n'])
...
...         return __add
...
...     if len(args) == 1 and callable(args[0]) and not kwargs:
...         # Decorator call without arguments, just call it
...         # ourselves
...         return _add(args[0])
...     elif not args and kwargs:
...         # Decorator call with arguments, this time it will
...         # automatically be executed with function as the
...         # first argument
...         default_kwargs.update(kwargs)
...         return _add
...     else:
...         raise RuntimeError('This decorator only supports '
...                            'keyword arguments')

>>> @add
... def spam(n):
...     return 'spam' * n

>>> @add(n=3)
... def eggs(n):
...     return 'eggs' * n

>>> spam(3)
'spamspamspamspam'

>>> eggs(2)
'eggseggseggseggseggs'

>>> @add(3)
... def bacon(n):
...     return 'bacon' * n
Traceback (most recent call last):
  ...
RuntimeError: This decorator only supports keyword arguments

无论何时你有选择余地,我建议你要么有带参数的装饰器,要么不带参数,而不要有可选参数。然而,如果你有一个很好的理由让参数成为可选的,那么你有一个相对安全的方法让它成为可能。

使用类创建装饰器

与创建常规函数修饰符的方式类似,也可以使用类来创建修饰符。毕竟,函数只是一个可调用对象,类也可以实现可调用接口。下面的 decorator 与我们前面使用的debugdecorator 的工作原理类似,但使用类而不是常规函数:

>>> import functools

>>> class Debug(object):
...
...     def __init__(self, function):
...         self.function = function
...         # functools.wraps for classes
...         functools.update_wrapper(self, function)
...
...     def __call__(self, *args, **kwargs):
...         output = self.function(*args, **kwargs)
...         print('%s(%r, %r): %r' % (
...             self.function.__name__, args, kwargs, output))
...         return output

>>> @Debug
... def spam(eggs):
...     return 'spam' * (eggs % 5)
...
>>> output = spam(3)
spam((3,), {}): 'spamspamspam'

函数和类之间唯一显著的区别是在__init__方法中functools.wraps现在被functools.update_wrapper取代。

装饰类函数

装饰类函数与常规函数非常相似,但您需要注意所需的第一个参数self——类实例。您很可能已经使用了一些类函数装饰器。例如,classmethodstaticmethodproperty装饰器被用于许多不同的项目中。为了解释这一切是如何工作的,我们将构建自己版本的classmethodstaticmethodproperty装饰器。首先,让我们看一看类函数的简单装饰器,以显示与常规装饰器的区别:

>>> import functools

>>> def plus_one(function):
...     @functools.wraps(function)
...     def _plus_one(self, n):
...         return function(self, n + 1)
...     return _plus_one

>>> class Spam(object):
...     @plus_one
...     def get_eggs(self, n=2):
...         return n * 'eggs'

>>> spam = Spam()
>>> spam.get_eggs(3)
'eggseggseggseggs'

与常规函数的情况一样,类函数装饰器现在作为实例传递self。没什么意外!

跳过实例–classmethod 和 staticmethod

classmethodstaticmethod之间的差异相当简单。classmethod传递一个类对象而不是类实例(self),并且staticmethod完全跳过该类和实例。这实际上使staticmethod非常类似于类外的常规函数。

在我们重新创建classmethodstaticmethod之前,我们需要了解这些方法的预期行为:

>>> import pprint

>>> class Spam(object):
...
...     def some_instancemethod(self, *args, **kwargs):
...         print('self: %r' % self)
...         print('args: %s' % pprint.pformat(args))
...         print('kwargs: %s' % pprint.pformat(kwargs))
...
...     @classmethod
...     def some_classmethod(cls, *args, **kwargs):
...         print('cls: %r' % cls)
...         print('args: %s' % pprint.pformat(args))
...         print('kwargs: %s' % pprint.pformat(kwargs))
...
...     @staticmethod
...     def some_staticmethod(*args, **kwargs):
...         print('args: %s' % pprint.pformat(args))
...         print('kwargs: %s' % pprint.pformat(kwargs))

# Create an instance so we can compare the difference between
# executions with and without instances easily
>>> spam = Spam()

# With an instance (note the lowercase spam)
>>> spam.some_instancemethod(1, 2, a=3, b=4)
self: <...Spam object at 0x...>
args: (1, 2)
kwargs: {'a': 3, 'b': 4}

# Without an instance (note the capitalized Spam)
>>> Spam.some_instancemethod()
Traceback (most recent call last):

 ...
TypeError: some_instancemethod() missing 1 required positional argument: 'self'

# But what if we add parameters? Be very careful with these!
# Our first argument is now used as an argument, this can give
# very strange and unexpected errors
>>> Spam.some_instancemethod(1, 2, a=3, b=4)
self: 1
args: (2,)
kwargs: {'a': 3, 'b': 4}

# Classmethods are expectedly identical
>>> spam.some_classmethod(1, 2, a=3, b=4)
cls: <class '...Spam'>
args: (1, 2)
kwargs: {'a': 3, 'b': 4}

>>> Spam.some_classmethod()
cls: <class '...Spam'>
args: ()
kwargs: {}

>>> Spam.some_classmethod(1, 2, a=3, b=4)
cls: <class '...Spam'>
args: (1, 2)
kwargs: {'a': 3, 'b': 4}

# Staticmethods are also identical
>>> spam.some_staticmethod(1, 2, a=3, b=4)
args: (1, 2)
kwargs: {'a': 3, 'b': 4}

>>> Spam.some_staticmethod()
args: ()
kwargs: {}

>>> Spam.some_staticmethod(1, 2, a=3, b=4)
args: (1, 2)
kwargs: {'a': 3, 'b': 4}

请注意,在没有实例的情况下调用some_instancemethod会导致错误self丢失。正如所预期的(因为我们在这种情况下没有实例化该类),对于带有参数的版本,它似乎可以工作,但实际上已经坏了。这是因为现在假设第一个参数为self。在这种情况下,这显然是不正确的,您传递了一个整数,但是如果您传递了其他类实例,这可能是非常奇怪的 bug 的来源。classmethodstaticmethod均正确处理此问题。

在继续使用 decorator 之前,您需要了解 Python 描述符是如何工作的。描述符可用于修改对象属性的绑定行为。这意味着,如果将描述符用作属性的值,则在对属性调用这些操作时,可以修改要设置、获取和删除的值。以下是此行为的基本示例:

>>> class MoreSpam(object):
...
...     def __init__(self, more=1):
...         self.more = more
...
...     def __get__(self, instance, cls):
...         return self.more + instance.spam
...
...     def __set__(self, instance, value):
...         instance.spam = value - self.more

>>> class Spam(object):
...
...     more_spam = MoreSpam(5)
...
...     def __init__(self, spam):
...         self.spam = spam

>>> spam = Spam(1)
>>> spam.spam
1
>>> spam.more_spam
6

>>> spam.more_spam = 10
>>> spam.spam
5

正如您所看到的,无论何时我们从more_spam设置或获取值,它都会在MoreSpam上调用__get____set__。对于自动转换和类型检查来说,这是一项非常有用的壮举,我们将在下一段中看到的property装饰器就是这项技术的一个更方便的实现。

现在我们知道了描述符是如何工作的,我们可以继续创建classmethodstaticmethod装饰器。对于这两种情况,我们只需要修改__get__而不是__call__,这样我们就可以控制传递哪种类型的实例(或者根本不传递):

import functools

class ClassMethod(object):

    def __init__(self, method):
        self.method = method

    def __get__(self, instance, cls):
        @functools.wraps(self.method)
        def method(*args, **kwargs):
            return self.method(cls, *args, **kwargs)
        return method

class StaticMethod(object):

    def __init__(self, method):
        self.method = method

    def __get__(self, instance, cls):
        return self.method

ClassMethoddecorator 仍然具有一个子功能,可以实际生成一个工作的 decorator。看看这个函数,你很可能猜到它是如何工作的。它没有将instance作为第一个参数传递给self.method,而是传递了cls

StaticMethod更简单,因为它完全忽略了instancecls。它可以只返回未修改的原始方法。因为它返回的是原始方法,没有任何修改,所以我们也不需要functools.wraps调用。

属性–智能描述符用法

property装饰器可能是 Python 领域最常用的装饰器。它允许您向现有实例属性添加 getter/setter,以便在将值设置为实例属性之前添加验证器和修改值。property修饰符既可以用作赋值,也可以用作修饰符。下面的示例显示了这两种语法,以便我们知道从property装饰器可以得到什么:

>>> class Spam(object):
...
...     def get_eggs(self):
...         print('getting eggs')
...         return self._eggs
...
...     def set_eggs(self, eggs):
...         print('setting eggs to %s' % eggs)
...         self._eggs = eggs
...
...     def delete_eggs(self):
...         print('deleting eggs')
...         del self._eggs
...
...     eggs = property(get_eggs, set_eggs, delete_eggs)
...
...     @property
...     def spam(self):
...         print('getting spam')
...         return self._spam
...
...     @spam.setter
...     def spam(self, spam):
...         print('setting spam to %s' % spam)
...         self._spam = spam
...
...     @spam.deleter
...     def spam(self):
...         print('deleting spam')
...         del self._spam

>>> spam = Spam()
>>> spam.eggs = 123
setting eggs to 123
>>> spam.eggs
getting eggs
123
>>> del spam.eggs
deleting eggs

请注意,property装饰器仅在类继承object时工作。

与如何实现classmethodstaticmethod修饰符类似,我们再次需要 Python 描述符。这一次,我们需要描述符的全部功能,但不仅仅是__get__,还有__set____delete__

class Property(object):
    def __init__(self, fget=None, fset=None, fdel=None,
                 doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        # If no specific documentation is available, copy it
        # from the getter
        if fget and not doc:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, instance, cls):
        if instance is None:
            # Redirect class (not instance) properties to
            # self
            return self
        elif self.fget:
            return self.fget(instance)
        else:
            raise AttributeError('unreadable attribute')

    def __set__(self, instance, value):
        if self.fset:
            self.fset(instance, value)
        else:
            raise AttributeError("can't set attribute")

    def __delete__(self, instance):
        if self.fdel:
            self.fdel(instance)
        else:
            raise AttributeError("can't delete attribute")

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel)

正如您所看到的,大多数的Property实现只是描述符方法的实现。gettersetterdeleter功能只是使装饰器的使用成为可能的捷径,这就是为什么如果没有instance可用,我们必须使用return self

当然,有更多的方法可以达到这种效果。在上一段中,我们看到了裸描述符实现,在上一个示例中,我们看到了属性装饰器。对于类,一个更通用的解决方案是实现__getattr____getattribute__。下面是一个简单的演示:

>>> class Spam(object):
...     def __init__(self):
...         self.registry = {}
...
...     def __getattr__(self, key):
...         print('Getting %r' % key)
...         return self.registry.get(key, 'Undefined')
...
...     def __setattr__(self, key, value):
...         if key == 'registry':
...             object.__setattr__(self, key, value)
...         else:
...             print('Setting %r to %r' % (key, value))
...             self.registry[key] = value
...
...     def __delattr__(self, key):
...         print('Deleting %r' % key)
...         del self.registry[key]

>>> spam = Spam()

>>> spam.a
Getting 'a'
'Undefined'

>>> spam.a = 1
Setting 'a' to 1

>>> spam.a
Getting 'a'
1

>>> del spam.a
Deleting 'a'

__getattr__方法首先在instance.__dict__中查找密钥,只有当该密钥不存在时才会调用。这就是为什么我们从来没有看到注册表属性的__getattr__。在所有情况下都会调用__getattribute__方法,这使得使用它有点危险。使用__getattribute__方法,您将需要对registry进行特定的排除,因为如果您尝试访问self.registry,它将递归执行。

很少需要查看描述符,但几个内部 Python 进程都使用它们,比如继承类时的super()方法。

装饰类

Python 2.6 引入了类修饰符语法。与函数 decorator 语法一样,这也不是什么新技术。即使没有语法,也可以通过执行DecoratedClass = decorator(RegularClass)来修饰类。在前面的段落之后,您应该熟悉编写装饰程序。类装饰器与常规装饰器没有什么不同,只是它们采用的是类而不是函数。与函数的情况一样,这发生在声明时,而不是发生在实例化/调用时。

因为有很多替代方法可以修改类的工作方式,例如标准继承、混合和元类(更多信息请参见第 8 章元类–使类(而不是实例)更智能,所以从来都不严格需要类装饰器。这并不会降低它们的有用性,但它确实解释了为什么您很可能不会在野外看到太多的类装饰示例。

Singleton–具有单个实例的类

Singleton 是始终只允许存在单个实例的类。因此,不是专门为您的呼叫获取实例,而是始终获取相同的实例。对于诸如数据库连接池之类的情况,这些功能非常有用,因为您不希望一直打开连接,而是希望重用原始连接:

>>> import functools

>>> def singleton(cls):
...     instances = dict()
...     @functools.wraps(cls)
...     def _singleton(*args, **kwargs):
...         if cls not in instances:
...             instances[cls] = cls(*args, **kwargs)
...         return instances[cls]
...     return _singleton

>>> @singleton
... class Spam(object):
...     def __init__(self):
...         print('Executing init')

>>> a = Spam()
Executing init
>>> b = Spam()

>>> a is b
True

>>> a.x = 123
>>> b.x
123

正如在a is b比较中所看到的,两个对象具有相同的身份,因此我们可以得出结论,它们确实是相同的对象。与常规装饰器一样,由于functools.wraps功能,如果需要,我们仍然可以通过Spam.__wrapped__访问原始类。

is操作符通过身份比较对象,身份在 CPython 中实现为内存地址。如果a is b返回True,我们可以得出结论ab都是同一个实例。

总排序–可排序类的简单方法

在某种程度上,您可能需要对数据结构进行排序。虽然使用sorted函数的关键参数很容易实现,但如果您需要经常执行__gt____ge____lt____le____eq__函数,则有一种更方便的方法。这似乎有点冗长,不是吗?如果你想要最好的性能,这仍然是一个好主意,但是如果你可以接受一个微小的性能影响和一些稍微复杂的堆栈跟踪,那么total_ordering可能是一个不错的选择。total_ordering类装饰器可以基于具有__eq__函数和其中一个比较函数(__lt____le____gt____ge__的类来实现所有必需的排序函数。这意味着您可以大大缩短函数定义。让我们比较一下常规的和使用total_ordering装饰器的:

>>> import functools

>>> class Value(object):
...     def __init__(self, value):
...         self.value = value
...
...     def __repr__(self):
...         return '<%s[%d]>' % (self.__class__, self.value)

>>> class Spam(Value):
...     def __gt__(self, other):
...         return self.value > other.value
...
...     def __ge__(self, other):
...         return self.value >= other.value
...
...     def __lt__(self, other):
...         return self.value < other.value
...
...     def __le__(self, other):
...         return self.value <= other.value
...
...     def __eq__(self, other):
...         return self.value == other.value

>>> @functools.total_ordering
... class Egg(Value):
...     def __lt__(self, other):
...         return self.value < other.value
...
...     def __eq__(self, other):
...         return self.value == other.value

>>> numbers = [4, 2, 3, 4]
>>> spams = [Spam(n) for n in numbers]
>>> eggs = [Egg(n) for n in numbers]

>>> spams
[<<class 'H05.Spam'>[4]>, <<class 'H05.Spam'>[2]>,
<<class 'H05.Spam'>[3]>, <<class 'H05.Spam'>[4]>]

>>> eggs
[<<class 'H05.Egg'>[4]>, <<class 'H05.Egg'>[2]>,
<<class 'H05.Egg'>[3]>, <<class 'H05.Egg'>[4]>]

>>> sorted(spams)
[<<class 'H05.Spam'>[2]>, <<class 'H05.Spam'>[3]>,
<<class 'H05.Spam'>[4]>, <<class 'H05.Spam'>[4]>]

>>> sorted(eggs)
[<<class 'H05.Egg'>[2]>, <<class 'H05.Egg'>[3]>,
<<class 'H05.Egg'>[4]>, <<class 'H05.Egg'>[4]>]

# Sorting using key is of course still possible and in this case
# perhaps just as easy:
>>> values = [Value(n) for n in numbers]
>>> values
[<<class 'H05.Value'>[4]>, <<class 'H05.Value'>[2]>,
<<class 'H05.Value'>[3]>, <<class 'H05.Value'>[4]>]

>>> sorted(values, key=lambda v: v.value)
[<<class 'H05.Value'>[2]>, <<class 'H05.Value'>[3]>,
<<class 'H05.Value'>[4]>, <<class 'H05.Value'>[4]>]

现在,您可能会想,“为什么没有一个类修饰符来使用指定的键属性使类可排序?”对于functools库来说,这确实是一个好主意,但它还没有出现。那么让我们看看如何实现类似的功能:

>>> def sort_by_attribute(attr, keyfunc=getattr):
...     def _sort_by_attribute(cls):
...         def __gt__(self, other):
...             return getattr(self, attr) > getattr(other, attr)
...
...         def __ge__(self, other):
...             return getattr(self, attr) >= getattr(other, attr)
...
...         def __lt__(self, other):
...             return getattr(self, attr) < getattr(other, attr)
...
...         def __le__(self, other):
...             return getattr(self, attr) <= getattr(other, attr)
...
...         def __eq__(self, other):
...             return getattr(self, attr) <= getattr(other, attr)
...
...         cls.__gt__ = __gt__
...         cls.__ge__ = __ge__
...         cls.__lt__ = __lt__
...         cls.__le__ = __le__
...         cls.__eq__ = __eq__
...
...         return cls
...     return _sort_by_attribute

>>> class Value(object):
...     def __init__(self, value):
...         self.value = value
...
...     def __repr__(self):
...         return '<%s[%d]>' % (self.__class__, self.value)

>>> @sort_by_attribute('value')
... class Spam(Value):
...     pass

>>> numbers = [4, 2, 3, 4]
>>> spams = [Spam(n) for n in numbers]
>>> sorted(spams)
[<<class '...Spam'>[2]>, <<class '...Spam'>[3]>,
<<class '...Spam'>[4]>, <<class '...Spam'>[4]>]

当然,这个大大简化了可排序类的创建。如果你想拥有自己的按键功能而不是getattr,那就更容易了。只需将getattr(self, attr)调用替换为key_function(self),对other也这样做,并将 decorator 的参数更改为您的函数。您甚至可以将其用作基本函数,通过简单地传递一个包装好的getattr函数来实现sort_by_attribute

有用的装饰师

除了本章中已经提到的那些,Python 还附带了一些其他有用的装饰器。有一些标准库中还没有。

Python 中的单分派-多态性

如果您以前使用过 C++或 java,那么您可能习惯于使用 Ad Hoc 多态性,根据参数类型调用不同的函数。Python 是一种动态类型化语言,大多数人不会期望单一分派模式的可能性。然而,Python 是一种不仅是动态类型而且是强类型的语言,这意味着我们可以依赖于我们接收的类型。

动态类型化语言不需要严格的类型定义。另一方面,像 C 这样的语言需要以下内容来声明整数:

int some_integer = 123;

Python 只接受您的值具有以下类型:

some_integer = 123

然而,与 JavaScript 和 PHP 等语言相反,Python 很少进行隐式类型转换。在 Python 中,以下内容将返回一个错误,而 JavaScript 将毫无问题地执行该错误:

'spam' + 5

在 Python 中,结果是一个TypeError。在 Javascript 中,它是'spam5'

单一分派的思想是,根据传递的类型,调用正确的函数。由于str + int会导致 Python 中出现错误,因此在将参数传递给函数之前自动转换参数非常方便。这有助于将函数的实际工作与类型转换分开。

自 Python3.4 以来,有一个 decorator 可以轻松地在 Python 中实现单一分派模式。对于其中一种情况,您需要处理与正常执行不同的特定类型。以下是一个基本示例:

>>> import functools

>>> @functools.singledispatch
... def printer(value):
...     print('other: %r' % value)

>>> @printer.register(str)
... def str_printer(value):
...     print(value)

>>> @printer.register(int)
... def int_printer(value):
...     printer('int: %d' % value)

>>> @printer.register(dict)
... def dict_printer(value):
...     printer('dict:')
...     for k, v in sorted(value.items()):
...         printer('    key: %r, value: %r' % (k, v))

>>> printer('spam')
spam

>>> printer([1, 2, 3])
other: [1, 2, 3]

>>> printer(123)
int: 123

>>> printer({'a': 1, 'b': 2})
dict:
 key: 'a', value: 1
 key: 'b', value: 2

看看是如何根据类型调用其他函数的?此模式对于降低接受多种类型参数的单个函数的复杂性非常有用。

命名函数时,确保不覆盖原始的singledispatch函数。如果我们将str_printer命名为printer,它将覆盖最初的printer函数。这将导致无法访问原始的printer功能,并且在该功能之后所有register操作也将失败。

现在,一个更有用的区分文件名和文件处理程序的示例:

>>> import json
>>> import functools

>>> @functools.singledispatch
... def write_as_json(file, data):
...     json.dump(data, file)

>>> @write_as_json.register(str)
... @write_as_json.register(bytes)
... def write_as_json_filename(file, data):
...     with open(file, 'w') as fh:
...         write_as_json(fh, data)

>>> data = dict(a=1, b=2, c=3)
>>> write_as_json('test1.json', data)
>>> write_as_json(b'test2.json', 'w')
>>> with open('test3.json', 'w') as fh:
...     write_as_json(fh, data)

所以现在我们有一个write_as_json函数;它根据类型调用正确的代码。如果它是一个strbytes对象,它会自动打开文件并调用接受文件对象的write_as_json常规版本。

当然,编写一个实现这一点的装饰器并不难,但是将它放在基本库中仍然非常方便。它肯定比函数中的几个isinstance调用要好。要查看将调用哪个函数,可以使用具有特定类型的write_as_json.dispatch函数。通过str时,您将获得write_as_json_filename功能。应该注意的是,被调度函数的名称完全是任意的。当然,它们可以作为常规函数访问,但是您可以随意命名它们。

要检查注册的类型,您可以通过write_as_json.registry访问注册表,它是一个字典:

>>> write_as_json.registry.keys()
dict_keys([<class 'bytes'>, <class 'object'>, <class 'str'>])

Contextmanager,简单陈述

使用contextmanager类,我们可以很容易地创建上下文包装器。无论何时使用with语句,都会使用上下文包装器。一个示例是 open 函数,它也可以作为上下文包装,允许您使用以下代码:

with open(filename) as fh:
    pass

现在让我们假设open函数不可用作上下文管理器,我们需要构建自己的函数来实现这一点。创建上下文管理器的标准方法是创建一个实现__enter____exit__方法的类,但这有点冗长。我们可以让它更短更简单:

>>> import contextlib

>>> @contextlib.contextmanager
... def open_context_manager(filename, mode='r'):
...     fh = open(filename, mode)
...     yield fh
...     fh.close()

>>> with open_context_manager('test.txt', 'w') as fh:
...     print('Our test is complete!', file=fh)

很简单,对吧?但是,我应该提到的是,对于这个特定的情况,关闭对象在contextlib中有一个专用的功能,它更容易使用。让我们来演示一下:

>>> import contextlib

>>> with contextlib.closing(open('test.txt', 'a')) as fh:
...     print('Yet another test', file=fh)

对于file对象,这当然不是必需的,因为它已经可以作为上下文管理器使用。但是,一些对象,例如urllib发出的请求,不支持以这种方式自动关闭,因此受益于此功能。

但是等待;还有更多!除了可以在with语句中使用外,contextmanager的结果实际上还可以用作 Python 3.2 中的装饰器。在较旧的 Python 版本中,它只是一个小包装器,但自 Python 3.2 以来,它基于ContextDecorator类,这使它成为一个装饰器。前面的 decorator 并不真正适合该任务,因为它会产生一个结果(更多信息请参见第 6 章生成器和协同程序–无限,一次一步,但我们可以考虑其他功能:

>>> @contextlib.contextmanager
... def debug(name):
...     print('Debugging %r:' % name)
...     yield
...     print('End of debugging %r' % name)

>>> @debug('spam')
... def spam():
...     print('This is the inside of our spam function')

>>> spam()
Debugging 'spam':
This is the inside of our spam function
End of debugging 'spam'

这方面有很多不错的用例,但至少,这是一种方便的方式,可以在没有所有(嵌套的)with语句的情况下将函数包装到上下文中。

验证、类型检查和转换

虽然在 Python 中检查类型通常不是最好的方法,但如果您知道将需要特定类型(或可以转换为该类型的内容),那么有时它会很有用。为了实现这一点,Python 3.5 引入了一个类型暗示系统,以便您可以执行以下操作:

def spam(eggs: int):
    pass

由于 Python3.5 还不太常见,这里有一个装饰器,它通过更高级的类型检查实现了相同的功能。为了允许这种类型的检查,必须使用一些魔法,特别是使用inspect模块。就我个人而言,我不太喜欢通过检查代码来执行这些技巧,因为它们很容易被破坏。当在函数和该装饰器之间使用常规装饰器(不复制argspec的装饰器)时,这段代码实际上会中断,但这是一个很好的示例:

>>> import inspect
>>> import functools

>>> def to_int(name, minimum=None, maximum=None):
...     def _to_int(function):
...         # Use the method signature to map *args to named
...         # arguments
...         signature = inspect.signature(function)
...
...         # Unfortunately functools.wraps doesn't copy the
...         # signature (yet) so we do it manually.
...         # For more info: http://bugs.python.org/issue23764
...         @functools.wraps(function, ['__signature__'])
...         @functools.wraps(function)
...         def __to_int(*args, **kwargs):
...             # Bind all arguments to the names so we get a single
...             # mapping of all arguments
...             bound = signature.bind(*args, **kwargs)
...
...             # Make sure the value is (convertible to) an integer
...             default = signature.parameters[name].default
...             value = int(bound.arguments.get(name, default))
...
...             # Make sure it's within the allowed range
...             if minimum is not None:
...                 assert value >= minimum, (
...                     '%s should be at least %r, got: %r' %
...                     (name, minimum, value))
...
...             if maximum is not None:
...                 assert value <= maximum, (
...                     '%s should be at most %r, got: %r' %
...                     (name, maximum, value))
...
...             return function(*args, **kwargs)
...         return __to_int
...     return _to_int

>>> @to_int('a', minimum=10)
... @to_int('b', maximum=10)
... @to_int('c')
... def spam(a, b, c=10):
...     print('a', a)
...     print('b', b)
...     print('c', c)

>>> spam(10, b=0)
a 10
b 0
c 10

>>> spam(a=20, b=10)
a 20
b 10
c 10

>>> spam(1, 2, 3)
Traceback (most recent call last):
 ...
AssertionError: a should be at least 10, got: 1

>>> spam()
Traceback (most recent call last):
 ...
TypeError: 'a' parameter lacking default value

>>> spam('spam', {})
Traceback (most recent call last):
 ...
ValueError: invalid literal for int() with base 10: 'spam'

因为inspect魔法的,我仍然不确定是否会推荐使用这样的装饰器。相反,我会选择一个更简单的版本,它不使用任何inspect,只解析kwargs中的参数:

>>> import functools

>>> def to_int(name, minimum=None, maximum=None):
...     def _to_int(function):
...         @functools.wraps(function)
...         def __to_int(**kwargs):
...             value = int(kwargs.get(name))
...
...             # Make sure it's within the allowed range
...             if minimum is not None:
...                 assert value >= minimum, (
...                     '%s should be at least %r, got: %r' %
...                     (name, minimum, value))
...
...             if maximum is not None:
...                 assert value <= maximum, (
...                     '%s should be at most %r, got: %r' %
...                     (name, maximum, value))
...
...             return function(**kwargs)
...         return __to_int
...     return _to_int

>>> @to_int('a', minimum=10)
... @to_int('b', maximum=10)
... def spam(a, b):
...     print('a', a)
...     print('b', b)

>>> spam(a=20, b=10)
a 20
b 10

>>> spam(a=1, b=10)
Traceback (most recent call last):
 ...
AssertionError: a should be at least 10, got: 1

然而,正如所展示的,支持argskwargs并不是不可能的,只要您记住__signature__不是默认复制的。如果没有__signature__,检查模块将不知道哪些参数是允许的,哪些是不允许的。

缺少的__signature__问题目前正在讨论中,可能会在未来的 Python 版本中解决:

http://bugs.python.org/issue23764

无用的警告——如何忽略它们

通常在编写 Python 时,当您第一次实际编写代码时,警告非常有用。但是,在执行它时,每次运行脚本/应用时获取相同的消息是没有用的。因此,让我们创建一些代码,允许轻松隐藏预期的警告,但不是所有的警告,以便我们可以轻松捕获新警告:

import warnings
import functools

def ignore_warning(warning, count=None):
    def _ignore_warning(function):
        @functools.wraps(function)
        def __ignore_warning(*args, **kwargs):
            # Execute the code while recording all warnings
            with warnings.catch_warnings(record=True) as ws:
                # Catch all warnings of this type
                warnings.simplefilter('always', warning)
                # Execute the function
                result = function(*args, **kwargs)

            # Now that all code was executed and the warnings
            # collected, re-send all warnings that are beyond our
            # expected number of warnings
            if count is not None:
                for w in ws[count:]:
                    warnings.showwarning(
                        message=w.message,
                        category=w.category,
                        filename=w.filename,
                        lineno=w.lineno,
                        file=w.file,
                        line=w.line,
                    )

            return result
        return __ignore_warning
    return _ignore_warning

@ignore_warning(DeprecationWarning, count=1)
def spam():
    warnings.warn('deprecation 1', DeprecationWarning)
    warnings.warn('deprecation 2', DeprecationWarning)

使用此方法,我们可以捕获第一个(预期的)警告,但仍然可以看到第二个(不预期的)警告。

总结

本章向我们展示了一些地方,在这些地方,可以使用装饰器简化代码,并向非常简单的函数添加一些相当复杂的行为。老实说,大多数装饰器比直接添加功能的常规函数更复杂,但将相同模式应用于许多函数和类的附加优势通常是值得的。

装饰器有很多用途,可以使函数和类更智能、使用更方便:

  • 调试
  • 验证
  • 参数便利性(预填充或转换参数)
  • 输出便利性(将输出转换为特定类型)

本章最重要的内容应该是在包装函数时永远不要忘记functools.wraps。由于(意外的)行为修改,调试修饰函数可能相当困难,但丢失属性也会使问题更加严重。

下一章将向我们展示如何以及何时使用generatorscoroutines。本章已经稍微向我们展示了with语句的用法,但是generatorscoroutines更进一步。尽管如此,我们仍然会经常使用装饰器,所以请确保您对它们的工作方式有很好的了解。