函数装饰器功能

函数装饰器类似于设计模式中装饰器模式的思路,通过某种方式来增强函数的行为,其样子类似下面方式:

@decorate
def target():
    print('running target()')
    
# 等价于
def target():
    print('running target()')
target = decorate(target)

上下两个代码的效果是一样的。在程序加载后,target 函数就不是无装饰器修饰的 target 函数了。当然,如果装饰器返回原对象,那么还是一样的函数,但一般不会这么做。

通过下面的结果可以看出 target 函数的逻辑被装饰器替换了。

>>> def deco(func):
...     def inner():
...         print('running inner()')
...     return inner 
...
>>> @deco
... def target(): 
...     print('running target()')
...
>>> target()
running inner()

一个简单的装饰器,用于输出函数的运行时间:

import time
def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter()-t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs]%s(%s)->%r'%(elapsed, name, arg_str, result))
        return result
    return clocked
    
@clock
def snooze(seconds):
    time.sleep(seconds)
@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)
    
if __name__=='__main__':
    print('*' * 40, 'Calling snooze(.123)')
    snooze(.123)
    print('*' * 40, 'Calling factorial(6)')
    print('6! =', factorial(6))
 
$ python3 clockdeco_demo.py
**************************************** Calling snooze(.123)
[0.12405610s] snooze(.123)-> None
**************************************** Calling factorial(6)
[0.00000191s] factorial(1)-> 1
[0.00004911s] factorial(2)-> 2
[0.00008488s] factorial(3)-> 6
[0.00013208s] factorial(4)-> 24
[0.00019193s] factorial(5)-> 120
[0.00026107s] factorial(6)-> 720
6! = 720

带自定义参数的装饰器

def repeat(num):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(num):
                func(*args, **kwargs)
        return wrapper
    return my_decorator
 
@repeat(4)
def greet(message):
    print(message)
greet('hello world')

类装饰器

类装饰器主要依赖于函数 __call__(),每当你调用一个类的示例时,函数 __call__() 就会被执行一次。

class Count:
    def __init__(self, func):
        self.func = func
        self.num_calls = 0
    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print('num of calls is: {}'.format(self.num_calls))
        return self.func(*args, **kwargs)
@Count
def example():
    print("hello world")
example()
# 输出
num of calls is: 1
hello world
example()
# 输出
num of calls is: 2
hello world

装饰器的顺序

@decorator1
@decorator2
@decorator3
def func():
    ...
    
decorator1(decorator2(decorator3(func)))

标准库内置的装饰器

functools.wraps 构建行为良好的装饰器

上面方式实现的 clock 装饰器存在几个缺点:

  1. 不支持关键字参数。
  2. 经过装饰器装饰后,返回的是内部 clocked 函数,会遮盖了被装饰函数的 __name____doc__ 属性。即,在程序运行后,将 snooze 函数、factorial 函数分别赋值给两个对象时,检查这两个对象的 __name____doc__ 属性会发现值相同,都是 clocked 函数的属性值。

通过 functools.wraps 装饰器可以将相关的属性从 func 复制到 clocked 中,还可以正确处理关键字参数。具体如下:

# clockdeco2.py
import time
import functools
def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time()-t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r'%(k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('[%0.8fs]%s(%s)->%r '%(elapsed, name, arg_str, result))
        return result
    return clocked

functools.lru_cache 数据缓存

functools.singleddispatch 泛函数

叠放装饰器

装饰器的执行时间

装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。一般来说,就是在导入当前模块时执行装饰器函数。

registry = []
def register(func): 
    print('running register(%s)'%func) 
    registry.append(func) 
    return func 
@register
def f1():
    print('running f1()')
@register
def f2():
    print('running f2()')
def f3(): 
    print('running f3()')
def main(): 
    print('running main()')
    print('registry->', registry)
    f1()
    f2()
    f3()
if __name__=='__main__':
    main()

把 registration.py 当作脚本运行得到的输出如下:

$ python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry-> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()

如果导入 registration.py 模块(不作为脚本运行),输出如下:

>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)
>>> registration.registry
[<function f1 at 0x10063b1e0>, <function f2 at 0x10063b268>]

通过上述测试,可以看到函数装饰器在导入模块后就立即执行,而被装饰的函数仅在明确调用的时候才运行。