分享

Python 装饰器

 lllsztz 2019-11-23

简介

Python中的装饰器是一种可调用的对象(它可以是函数、或实现call的类),它将被装饰对象(函数、或类)作为参数传入,再经过执行所需要的处理后,返回另一个可调用对象。装饰器的本质就是把一个可调用对象变成另一个可调用对象。

装饰器分为两类:函数装饰器与类装饰器。

在函数上添加包装器

问题

你想在函数上添加一个包装器,增加额外的操作处理(比如日志、计时等)。

解决方案

如果你想使用额外的代码包装一个函数,可以定义一个装饰器函数,例如:

import timefrom functools import wrapsdef timethis(func): ''' Decorator that reports the execution time. ''' @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(func.__name__, end-start) return result return wrapper

下面是使用装饰器的例子:

>>> @timethis... def countdown(n):... '''... Counts down... '''... while n > 0:... n -= 1...>>> countdown(100000)countdown 0.008917808532714844>>> countdown(10000000)countdown 0.87188299392912>>>

讨论

一个装饰器就是一个函数,它接受一个函数作为参数并返回一个新的函数。 当你像下面这样写:

@timethisdef countdown(n): pass

跟像下面这样写其实效果是一样的:

def countdown(n): passcountdown = timethis(countdown)

顺便说一下,内置的装饰器比如 @staticmethod, @classmethod,@property 原理也是一样的。 例如,下面这两个代码片段是等价的:

class A: @classmethod def method(cls): passclass B: # Equivalent definition of a class method def method(cls): pass method = classmethod(method)

在上面的 wrapper() 函数中, 装饰器内部定义了一个使用 *args 和 **kwargs 来接受任意参数的函数。 在这个函数里面调用了原始函数并将其结果返回,不过你还可以添加其他额外的代码(比如计时)。 然后这个新的函数包装器被作为结果返回来代替原始函数。

需要强调的是装饰器并不会修改原始函数的参数签名以及返回值。 使用 *args 和 **kwargs 目的就是确保任何参数都能适用。 而返回结果值基本都是调用原始函数 func(*args, **kwargs) 的返回结果,其中func就是原始函数。

刚开始学习装饰器的时候,会使用一些简单的例子来说明,比如上面演示的这个。 不过实际场景使用时,还是有一些细节问题要注意的。 比如上面使用 @wraps(func) 注解是很重要的, 它能保留原始函数的元数据(下一小节会讲到),新手经常会忽略这个细节。 接下来的几个小节我们会更加深入的讲解装饰器函数的细节问题,如果你想构造你自己的装饰器函数,需要认真看一下。

创建装饰器时保留函数元信息

问题

你写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了。

解决方案

任何时候你定义装饰器的时候,都应该使用 functools 库中的 @wraps 装饰器来注解底层包装函数。例如:

import timefrom functools import wrapsdef timethis(func): ''' Decorator that reports the execution time. ''' @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(func.__name__, end-start) return result return wrapper

下面我们使用这个被包装后的函数并检查它的元信息:

>>> @timethis... def countdown(n):... '''... Counts down... '''... while n > 0:... n -= 1...>>> countdown(100000)countdown 0.008917808532714844>>> countdown.__name__'countdown'>>> countdown.__doc__'\n\tCounts down\n\t'>>> countdown.__annotations__{'n': <class 'int'>}>>>

讨论

在编写装饰器的时候复制元信息是一个非常重要的部分。如果你忘记了使用 @wraps , 那么你会发现被装饰函数丢失了所有有用的信息。比如如果忽略 @wraps 后的效果是下面这样的:

>>> countdown.__name__'wrapper'>>> countdown.__doc__>>> countdown.__annotations__{}>>>

@wraps 有一个重要特征是它能让你通过属性 __wrapped__ 直接访问被包装函数。例如:

>>> countdown.__wrapped__(100000)>>>

__wrapped__ 属性还能让被装饰函数正确暴露底层的参数签名信息。例如:

>>> from inspect import signature>>> print(signature(countdown))(n:int)>>>

一个很普遍的问题是怎样让装饰器去直接复制原始函数的参数签名信息, 如果想自己手动实现的话需要做大量的工作,最好就简单的使用 @wraps 装饰器。 通过底层的 __wrapped__ 属性访问到函数签名信息。更多关于签名的内容可以参考9.16小节。

解除一个装饰器

问题

一个装饰器已经作用在一个函数上,你想撤销它,直接访问原始的未包装的那个函数。

解决方案

假设装饰器是通过 @wraps来实现的,那么你可以通过访问 __wrapped__ 属性来访问原始函数:

>>> @somedecorator>>> def add(x, y):... return x + y...>>> orig_add = add.__wrapped__>>> orig_add(3, 4)7>>>

讨论

直接访问未包装的原始函数在调试、内省和其他函数操作时是很有用的。 但是我们这里的方案仅仅适用于在包装器中正确使用了 @wraps 或者直接设置了 __wrapped__ 属性的情况。

如果有多个包装器,那么访问 __wrapped__ 属性的行为是不可预知的,应该避免这样做。 在Python3.3中,它会略过所有的包装层,比如,假如你有如下的代码:

from functools import wrapsdef decorator1(func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 1') return func(*args, **kwargs) return wrapperdef decorator2(func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 2') return func(*args, **kwargs) return wrapper@decorator1@decorator2def add(x, y): return x + y

下面我们在Python3.3下测试:

>>> add(2, 3)Decorator 1Decorator 25>>> add.__wrapped__(2, 3)5>>>

下面我们在Python3.4下测试:

>>> add(2, 3)Decorator 1Decorator 25>>> add.__wrapped__(2, 3)Decorator 25>>>注意:此时只解除了@decorator1装饰器的作用, 若要解除@decorator2装饰器的作用, 需使用:add.__wrapped__.__wrapped__(2, 3)

最后要说的是,并不是所有的装饰器都使用了 @wraps ,因此这里的方案并不全部适用。 特别的,内置的装饰器 @staticmethod 和 @classmethod 就没有遵循这个约定 (它们把原始函数存储在属性 __func__ 中)。

定义一个带参数的装饰器

问题

你想定义一个可以接受参数的装饰器

解决方案

我们用一个例子详细阐述下接受参数的处理过程。 假设你想写一个装饰器,给函数添加日志功能,同时允许用户指定日志的级别和其他的选项。 下面是这个装饰器的定义和使用示例:

from functools import wrapsimport loggingdef logged(level, name=None, message=None): ''' Add logging to a function. level is the logging level, name is the logger name, and message is the log message. If name and message aren't specified, they default to the function's module and name. ''' def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) return wrapper return decorate# Example use@logged(logging.DEBUG)def add(x, y): return x + y@logged(logging.CRITICAL, 'example')def spam(): print('Spam!')

初看起来,这种实现看上去很复杂,但是核心思想很简单。 最外层的函数 logged() 接受参数并将它们作用在内部的装饰器函数上面。 内层的函数 decorate() 接受一个函数作为参数,然后在函数上面放置一个包装器。 这里的关键点是包装器是可以使用传递给 logged() 的参数的。

讨论

定义一个接受参数的包装器看上去比较复杂主要是因为底层的调用序列。特别的,如果你有下面这个代码:

@decorator(x, y, z)def func(a, b): pass

装饰器处理过程跟下面的调用是等效的;

def func(a, b): passfunc = decorator(x, y, z)(func)

decorator(x, y, z) 的返回结果必须是一个可调用对象,它接受一个函数作为参数并包装它。

可自定义属性的装饰器

问题

你想写一个装饰器来包装一个函数,并且允许用户提供参数在运行时控制装饰器行为。

解决方案

引入一个访问函数,使用 nonlocal 来修改内部变量。 然后这个访问函数被作为一个属性赋值给包装函数。

from functools import wraps, partialimport logging# Utility decorator to attach a function as an attribute of objdef attach_wrapper(obj, func=None): if func is None: return partial(attach_wrapper, obj) setattr(obj, func.__name__, func) return funcdef logged(level, name=None, message=None): ''' Add logging to a function. level is the logging level, name is the logger name, and message is the log message. If name and message aren't specified, they default to the function's module and name. ''' def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) # Attach setter functions @attach_wrapper(wrapper) def set_level(newlevel): nonlocal level level = newlevel @attach_wrapper(wrapper) def set_message(newmsg): nonlocal logmsg logmsg = newmsg return wrapper return decorate# Example use@logged(logging.DEBUG)def add(x, y): return x + y@logged(logging.CRITICAL, 'example')def spam(): print('Spam!')

下面是交互环境下的使用例子:

>>> import logging>>> logging.basicConfig(level=logging.DEBUG)>>> add(2, 3)DEBUG:__main__:add5>>> # Change the log message>>> add.set_message('Add called')>>> add(2, 3)DEBUG:__main__:Add called5>>> # Change the log level>>> add.set_level(logging.WARNING)>>> add(2, 3)WARNING:__main__:Add called5>>>

讨论

这一小节的关键点在于访问函数(如 set_message() 和 set_level() ),它们被作为属性赋给包装器。 每个访问函数允许使用 nonlocal 来修改函数内部的变量。

还有一个令人吃惊的地方是访问函数会在多层装饰器间传播(如果你的装饰器都使用了 @functools.wraps 注解)。 例如,假设你引入另外一个装饰器,比如 @timethis ,像下面这样:

@timethis@logged(logging.DEBUG)def countdown(n): while n > 0: n -= 1

你会发现访问函数依旧有效:

>>> countdown(10000000)DEBUG:__main__:countdowncountdown 0.8198461532592773>>> countdown.set_level(logging.WARNING)>>> countdown.set_message('Counting down to zero')>>> countdown(10000000)WARNING:__main__:Counting down to zerocountdown 0.8225970268249512>>>

你还会发现即使装饰器像下面这样以相反的方向排放,效果也是一样的:

@logged(logging.DEBUG)@timethisdef countdown(n): while n > 0: n -= 1

还能通过使用lambda表达式代码来让访问函数的返回不同的设定值:

@attach_wrapper(wrapper)def get_level(): return level# Alternativewrapper.get_level = lambda: level

一个比较难理解的地方就是对于访问函数的首次使用。例如,你可能会考虑另外一个方法直接访问函数的属性,如下:

@wraps(func)def wrapper(*args, **kwargs): wrapper.log.log(wrapper.level, wrapper.logmsg) return func(*args, **kwargs)# Attach adjustable attributeswrapper.level = levelwrapper.logmsg = logmsgwrapper.log = log

这个方法也可能正常工作,但前提是它必须是最外层的装饰器才行。 如果它的上面还有另外的装饰器(比如上面提到的 @timethis 例子),那么它会隐藏底层属性,使得修改它们没有任何作用。 而通过使用访问函数就能避免这样的局限性。

带可选参数的装饰器

问题

你想写一个装饰器,既可以不传参数给它,比如 @decorator , 也可以传递可选参数给它,比如 @decorator(x,y,z) 。

解决方案

下面是日志装饰器的一个修改版本:

from functools import wraps, partialimport loggingdef logged(func=None, *, level=logging.DEBUG, name=None, message=None): if func is None: return partial(logged, level=level, name=name, message=message) logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) return wrapper# Example use@loggeddef add(x, y): return x + y@logged(level=logging.CRITICAL, name='example')def spam(): print('Spam!')

可以看到,@logged 装饰器可以同时不带参数或带参数。

讨论

这里提到的这个问题就是通常所说的编程一致性问题。 当我们使用装饰器的时候,大部分程序员习惯了要么不给它们传递任何参数,要么给它们传递确切参数。 其实从技术上来讲,我们可以定义一个所有参数都是可选的装饰器,就像下面这样:

@logged()def add(x, y): return x+y

但是,这种写法并不符合我们的习惯,有时候程序员忘记加上后面的括号会导致错误。 这里我们向你展示了如何以一致的编程风格来同时满足没有括号和有括号两种情况。

为了理解代码是如何工作的,你需要非常熟悉装饰器是如何作用到函数上以及它们的调用规则。 对于一个像下面这样的简单装饰器:

# Example use@loggeddef add(x, y): return x + y

这个调用序列跟下面等价:

def add(x, y): return x + yadd = logged(add)

这时候,被装饰函数会被当做第一个参数直接传递给 logged 装饰器。 因此,logged() 中的第一个参数就是被包装函数本身。所有其他参数都必须有默认值。

而对于一个下面这样有参数的装饰器:

@logged(level=logging.CRITICAL, name='example')def spam(): print('Spam!')

调用序列跟下面等价:

def spam(): print('Spam!')spam = logged(level=logging.CRITICAL, name='example')(spam)

初始调用 logged() 函数时,被包装函数并没有传递进来。 因此在装饰器内,它必须是可选的。这个反过来会迫使其他参数必须使用关键字来指定。 并且,但这些参数被传递进来后,装饰器要返回一个接受一个函数参数并包装它的函数。 为了这样做,我们使用了一个技巧,就是利用 functools.partial 。 它会返回一个未完全初始化的自身,除了被包装函数外其他参数都已经确定下来了。

利用装饰器强制函数上的类型检查

问题

作为某种编程规约,你想在对函数参数进行强制类型检查。

解决方案

在演示实际代码前,先说明我们的目标:能对函数参数类型进行断言,类似下面这样:

>>> @typeassert(int, int)... def add(x, y):... return x + y...>>>>>> add(2, 3)5>>> add(2, 'hello')Traceback (most recent call last): File '<stdin>', line 1, in <module> File 'contract.py', line 33, in wrapperTypeError: Argument y must be <class 'int'>>>>

下面是使用装饰器技术来实现 @typeassert :

from inspect import signaturefrom functools import wrapsdef typeassert(*ty_args, **ty_kwargs): def decorate(func): # If in optimized mode, disable type checking if not __debug__: return func # Map function argument names to supplied types sig = signature(func) bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments @wraps(func) def wrapper(*args, **kwargs): bound_values = sig.bind(*args, **kwargs) # Enforce type assertions across supplied arguments for name, value in bound_values.arguments.items(): if name in bound_types: if not isinstance(value, bound_types[name]): raise TypeError( 'Argument {} must be {}'.format(name, bound_types[name]) ) return func(*args, **kwargs) return wrapper return decorate

可以看出这个装饰器非常灵活,既可以指定所有参数类型,也可以只指定部分。 并且可以通过位置或关键字来指定参数类型。下面是使用示例:

>>> @typeassert(int, z=int)... def spam(x, y, z=42):... print(x, y, z)...>>> spam(1, 2, 3)1 2 3>>> spam(1, 'hello', 3)1 hello 3>>> spam(1, 'hello', 'world')Traceback (most recent call last):File '<stdin>', line 1, in <module>File 'contract.py', line 33, in wrapperTypeError: Argument z must be <class 'int'>>>>

讨论

这节是高级装饰器示例,引入了很多重要的概念。

首先,装饰器只会在函数定义时被调用一次。 有时候你去掉装饰器的功能,那么你只需要简单的返回被装饰函数即可。 下面的代码中,如果全局变量 __debug__ 被设置成了False(当你使用-O或-OO参数的优化模式执行程序时), 那么就直接返回未修改过的函数本身:

def decorate(func): # If in optimized mode, disable type checking if not __debug__: return func

其次,这里还对被包装函数的参数签名进行了检查,我们使用了 inspect.signature() 函数。 简单来讲,它运行你提取一个可调用对象的参数签名信息。例如:

>>> from inspect import signature>>> def spam(x, y, z=42):... pass...>>> sig = signature(spam)>>> print(sig)(x, y, z=42)>>> sig.parametersmappingproxy(OrderedDict([('x', <Parameter at 0x10077a050 'x'>),('y', <Parameter at 0x10077a158 'y'>), ('z', <Parameter at 0x10077a1b0 'z'>)]))>>> sig.parameters['z'].name'z'>>> sig.parameters['z'].default42>>> sig.parameters['z'].kind<_ParameterKind: 'POSITIONAL_OR_KEYWORD'>>>>

装饰器的开始部分,我们使用了 bind_partial() 方法来执行从指定类型到名称的部分绑定。 下面是例子演示:

>>> bound_types = sig.bind_partial(int,z=int)>>> bound_types<inspect.BoundArguments object at 0x10069bb50>>>> bound_types.argumentsOrderedDict([('x', <class 'int'>), ('z', <class 'int'>)])>>>

在这个部分绑定中,你可以注意到缺失的参数被忽略了(比如并没有对y进行绑定)。 不过最重要的是创建了一个有序字典 bound_types.arguments 。 这个字典会将参数名以函数签名中相同顺序映射到指定的类型值上面去。 在我们的装饰器例子中,这个映射包含了我们要强制指定的类型断言。

在装饰器创建的实际包装函数中使用到了 sig.bind() 方法。 bind() 跟 bind_partial() 类似,但是它不允许忽略任何参数。因此有了下面的结果:

>>> bound_values = sig.bind(1, 2, 3)>>> bound_values.argumentsOrderedDict([('x', 1), ('y', 2), ('z', 3)])>>>

使用这个映射我们可以很轻松的实现我们的强制类型检查:

>>> for name, value in bound_values.arguments.items():... if name in bound_types.arguments:... if not isinstance(value, bound_types.arguments[name]):... raise TypeError()...>>>

不过这个方案还有点小瑕疵,它对于有默认值的参数并不适用。 比如下面的代码可以正常工作,尽管items的类型是错误的:

>>> @typeassert(int, list)... def bar(x, items=None):... if items is None:... items = []... items.append(x)... return items>>> bar(2)[2]>>> bar(2,3)Traceback (most recent call last): File '<stdin>', line 1, in <module> File 'contract.py', line 33, in wrapperTypeError: Argument items must be <class 'list'>>>> bar(4, [1, 2, 3])[1, 2, 3, 4]>>>

最后一点是关于适用装饰器参数和函数注解之间的争论。 例如,为什么不像下面这样写一个装饰器来查找函数中的注解呢?

@typeassertdef spam(x:int, y, z:int = 42): print(x,y,z)

一个可能的原因是如果使用了函数参数注解,那么就被限制了。 如果注解被用来做类型检查就不能做其他事情了。而且 @typeassert 不能再用于使用注解做其他事情的函数了。 而使用上面的装饰器参数灵活性大多了,也更加通用。

可以在PEP 362以及 inspect 模块中找到更多关于函数参数对象的信息。

将装饰器定义为类的一部分

问题

你想在类中定义装饰器,并将其作用在其他函数或方法上。

解决方案

在类里面定义装饰器很简单,但是你首先要确认它的使用方式。比如到底是作为一个实例方法还是类方法。 下面我们用例子来阐述它们的不同:

from functools import wrapsclass A: # Decorator as an instance method def decorator1(self, func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 1') return func(*args, **kwargs) return wrapper # Decorator as a class method @classmethod def decorator2(cls, func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 2') return func(*args, **kwargs) return wrapper

下面是一使用例子:

# As an instance methoda = A()@a.decorator1def spam(): pass# As a class method@A.decorator2def grok(): pass

仔细观察可以发现一个是实例调用,一个是类调用。

讨论

在类中定义装饰器初看上去好像很奇怪,但是在标准库中有很多这样的例子。 特别的,@property装饰器实际上是一个类,它里面定义了三个方法 getter(), setter(), deleter() , 每一个方法都是一个装饰器。例如:

class Person: # Create a property instance first_name = property() # Apply decorator methods @first_name.getter def first_name(self): return self._first_name @first_name.setter def first_name(self, value): if not isinstance(value, str): raise TypeError('Expected a string') self._first_name = value

它为什么要这么定义的主要原因是各种不同的装饰器方法会在关联的 property 实例上操作它的状态。 因此,任何时候只要你碰到需要在装饰器中记录或绑定信息,那么这不失为一种可行方法。

在类中定义装饰器有个难理解的地方就是对于额外参数 self 或 cls 的正确使用。 尽管最外层的装饰器函数比如 decorator1() 或 decorator2() 需要提供一个 self 或 cls 参数, 但是在两个装饰器内部被创建的 wrapper() 函数并不需要包含这个 self 参数。 你唯一需要这个参数是在你确实要访问包装器中这个实例的某些部分的时候。其他情况下都不用去管它。

对于类里面定义的包装器还有一点比较难理解,就是在涉及到继承的时候。 例如,假设你想让在A中定义的装饰器作用在子类B中。你需要像下面这样写:

class B(A): @A.decorator2 def bar(self): pass

也就是说,装饰器要被定义成类方法并且你必须显式的使用父类名去调用它。 你不能使用 @B.decorator2 ,因为在方法定义时,这个类B还没有被创建。

将装饰器定义为类

问题

你想使用一个装饰器去包装函数,但是希望返回一个可调用的实例。 你需要让你的装饰器可以同时工作在类定义的内部和外部。

解决方案

为了将装饰器定义成一个实例,你需要确保它实现了 __call__() 和 __get__() 方法。 例如,下面的代码定义了一个类,它在其他函数上放置一个简单的记录层:

import typesfrom functools import wrapsclass Profiled: def __init__(self, func): wraps(func)(self) self.ncalls = 0 def __call__(self, *args, **kwargs): self.ncalls += 1 return self.__wrapped__(*args, **kwargs) def __get__(self, instance, cls): if instance is None: return self else: return types.MethodType(self, instance)

你可以将它当做一个普通的装饰器来使用,在类里面或外面都可以:

@Profileddef add(x, y): return x + yclass Spam: @Profiled def bar(self, x): print(self, x)

在交互环境中的使用示例:

>>> add(2, 3)5>>> add(4, 5)9>>> add.ncalls2>>> s = Spam()>>> s.bar(1)<__main__.Spam object at 0x10069e9d0> 1>>> s.bar(2)<__main__.Spam object at 0x10069e9d0> 2>>> s.bar(3)<__main__.Spam object at 0x10069e9d0> 3>>> Spam.bar.ncalls3

讨论

将装饰器定义成类通常是很简单的。但是这里还是有一些细节需要解释下,特别是当你想将它作用在实例方法上的时候。

首先,使用 functools.wraps() 函数的作用跟之前还是一样,将被包装函数的元信息复制到可调用实例中去。

其次,通常很容易会忽视上面的 __get__() 方法。如果你忽略它,保持其他代码不变再次运行, 你会发现当你去调用被装饰实例方法时出现很奇怪的问题。例如:

>>> s = Spam()>>> s.bar(3)Traceback (most recent call last):...TypeError: bar() missing 1 required positional argument: 'x'

出错原因是当方法函数在一个类中被查找时,它们的 __get__() 方法依据描述器协议被调用。在这里,__get__() 的目的是创建一个绑定方法对象 (最终会给这个方法传递self参数)。下面是一个例子来演示底层原理:

>>> s = Spam()>>> def grok(self, x):... pass...>>> grok.__get__(s, Spam)<bound method Spam.grok of <__main__.Spam object at 0x100671e90>>>>>

__get__() 方法是为了确保绑定方法对象能被正确的创建。 type.MethodType() 手动创建一个绑定方法来使用。只有当实例被使用的时候绑定方法才会被创建。 如果这个方法是在类上面来访问, 那么 __get__() 中的instance参数会被设置成None并直接返回 Profiled 实例本身。 这样的话我们就可以提取它的 ncalls 属性了。

如果你想避免一些混乱,也可以考虑另外一个使用闭包和 nonlocal 变量实现的装饰器。例如:

import typesfrom functools import wrapsdef profiled(func): ncalls = 0 @wraps(func) def wrapper(*args, **kwargs): nonlocal ncalls ncalls += 1 return func(*args, **kwargs) wrapper.ncalls = lambda: ncalls return wrapper# Example@profileddef add(x, y): return x + y

这个方式跟之前的效果几乎一样,除了对于 ncalls 的访问现在是通过一个被绑定为属性的函数来实现,例如:

>>> add(2, 3)5>>> add(4, 5)9>>> add.ncalls()2>>>

为类和静态方法提供装饰器

问题

你想给类或静态方法提供装饰器。

解决方案

给类或静态方法提供装饰器是很简单的,不过要确保装饰器在 @classmethod 或 @staticmethod 之前。例如:

import timefrom functools import wraps# A simple decoratordef timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() r = func(*args, **kwargs) end = time.time() print(end-start) return r return wrapper# Class illustrating application of the decorator to different kinds of methodsclass Spam: @timethis def instance_method(self, n): print(self, n) while n > 0: n -= 1 @classmethod @timethis def class_method(cls, n): print(cls, n) while n > 0: n -= 1 @staticmethod @timethis def static_method(n): print(n) while n > 0: n -= 1

装饰后的类和静态方法可正常工作,只不过增加了额外的计时功能:

>>> s = Spam()>>> s.instance_method(1000000)<__main__.Spam object at 0x1006a6050> 10000000.11817407608032227>>> Spam.class_method(1000000)<class '__main__.Spam'> 10000000.11334395408630371>>> Spam.static_method(1000000)10000000.11740279197692871>>>

讨论

如果你把装饰器的顺序写错了就会出错。例如,假设你像下面这样写:

class Spam: @timethis @staticmethod def static_method(n): print(n) while n > 0: n -= 1

那么你调用这个静态方法时就会报错:

>>> Spam.static_method(1000000)Traceback (most recent call last):File '<stdin>', line 1, in <module>File 'timethis.py', line 6, in wrapperstart = time.time()TypeError: 'staticmethod' object is not callable>>>

问题在于 @classmethod 和 @staticmethod 实际上并不会创建可直接调用的对象, 而是创建特殊的描述器对象(参考8.9小节)。因此当你试着在其他装饰器中将它们当做函数来使用时就会出错。 确保这种装饰器出现在装饰器链中的第一个位置可以修复这个问题。

当我们在抽象基类中定义类方法和静态方法(参考8.12小节)时,这里讲到的知识就很有用了。 例如,如果你想定义一个抽象类方法,可以使用类似下面的代码:

from abc import ABCMeta, abstractmethodclass A(metaclass=ABCMeta): @classmethod @abstractmethod def method(cls): pass

在这段代码中,@classmethod 跟 @abstractmethod 两者的顺序是有讲究的,如果你调换它们的顺序就会出错。

装饰器为被包装函数增加参数

问题

你想在装饰器中给被包装函数增加额外的参数,但是不能影响这个函数现有的调用规则。

解决方案

可以使用关键字参数来给被包装函数增加额外参数。考虑下面的装饰器:

from functools import wrapsdef optional_debug(func): @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print('Calling', func.__name__) return func(*args, **kwargs) return wrapper>>> @optional_debug... def spam(a,b,c):... print(a,b,c)...>>> spam(1,2,3)1 2 3>>> spam(1,2,3, debug=True)Calling spam1 2 3>>>

讨论

通过装饰器来给被包装函数增加参数的做法并不常见。 尽管如此,有时候它可以避免一些重复代码。例如,如果你有下面这样的代码:

def a(x, debug=False): if debug: print('Calling a')def b(x, y, z, debug=False): if debug: print('Calling b')def c(x, y, debug=False): if debug: print('Calling c')

那么你可以将其重构成这样:

from functools import wrapsimport inspectdef optional_debug(func): if 'debug' in inspect.getargspec(func).args: raise TypeError('debug argument already defined') @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print('Calling', func.__name__) return func(*args, **kwargs) return wrapper@optional_debugdef a(x): pass@optional_debugdef b(x, y, z): pass@optional_debugdef c(x, y): pass

这种实现方案之所以行得通,在于强制关键字参数很容易被添加到接受 *args 和 **kwargs 参数的函数中。 通过使用强制关键字参数,它被作为一个特殊情况被挑选出来, 并且接下来仅仅使用剩余的位置和关键字参数去调用这个函数时,这个特殊参数会被排除在外。 也就是说,它并不会被纳入到 **kwargs 中去。

还有一个难点就是如何去处理被添加的参数与被包装函数参数直接的名字冲突。 例如,如果装饰器 @optional_debug 作用在一个已经拥有一个 debug 参数的函数上时会有问题。 这里我们增加了一步名字检查。

上面的方案还可以更完美一点,因为精明的程序员应该发现了被包装函数的函数签名其实是错误的。例如:

>>> @optional_debug... def add(x,y):... return x+y...>>> import inspect>>> print(inspect.signature(add))(x, y)>>>

通过如下的修改,可以解决这个问题:

from functools import wrapsimport inspectdef optional_debug(func): if 'debug' in inspect.getargspec(func).args: raise TypeError('debug argument already defined') @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print('Calling', func.__name__) return func(*args, **kwargs) sig = inspect.signature(func) parms = list(sig.parameters.values()) parms.append(inspect.Parameter('debug', inspect.Parameter.KEYWORD_ONLY, default=False)) wrapper.__signature__ = sig.replace(parameters=parms) return wrapper

通过这样的修改,包装后的函数签名就能正确的显示 debug 参数的存在了。例如:

>>> @optional_debug... def add(x,y):... return x+y...>>> print(inspect.signature(add))(x, y, *, debug=False)>>> add(2,3)5>>>

参考9.16小节获取更多关于函数签名的信息。

使用装饰器扩充类的功能

问题

你想通过反省或者重写类定义的某部分来修改它的行为,但是你又不希望使用继承或元类的方式。

解决方案

这种情况可能是类装饰器最好的使用场景了。例如,下面是一个重写了特殊方法 __getattribute__的类装饰器, 可以打印日志:

def log_getattribute(cls): # Get the original implementation orig_getattribute = cls.__getattribute__ # Make a new definition def new_getattribute(self, name): print('getting:', name) return orig_getattribute(self, name) # Attach to the class and return cls.__getattribute__ = new_getattribute return cls# Example use@log_getattributeclass A: def __init__(self,x): self.x = x def spam(self): pass

下面是使用效果:

>>> a = A(42)>>> a.xgetting: x42>>> a.spam()getting: spam>>>

讨论

类装饰器通常可以作为其他高级技术比如混入或元类的一种非常简洁的替代方案。 比如,上面示例中的另外一种实现使用到继承:

class LoggedGetattribute: def __getattribute__(self, name): print('getting:', name) return super().__getattribute__(name)# Example:class A(LoggedGetattribute): def __init__(self,x): self.x = x def spam(self): pass

这种方案也行得通,但是为了去理解它,你就必须知道方法调用顺序、super() 以及继承知识。 某种程度上来讲,类装饰器方案就显得更加直观,并且它不会引入新的继承体系。它的运行速度也更快一些, 因为他并不依赖 super() 函数。

如果你系想在一个类上面使用多个类装饰器,那么就需要注意下顺序问题。 例如,一个装饰器A会将其装饰的方法完整替换成另一种实现, 而另一个装饰器B只是简单的在其装饰的方法中添加点额外逻辑。 那么这时候装饰器A就需要放在装饰器B的前面。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多