前言

函数装饰器用于在代码中“标记”函数,以某种方式增强函数的行为。
想理解与掌握这一功能必须先理解闭包;除此之外,闭包还是回调式异步编程和函数式编程风格的基础

装饰器

装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把装饰后的函数返回,或者将其替换成另一个函数或可调用对象,然后返回。python也支持类装饰器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def deco(func):
    def inner():
        print('running inner()...')
    return inner

@deco
def target():
    print('running target()...')
target()
>>>running inner()...
print(target)
>>><function deco.<locals>.inner at 0x7f63096898c8>

可见target现在是对inner的引用

python何时执行装饰器

装饰器的一个关键特性是,它们在被装饰的函数定义后立即执行。这通常是在导入时(即python加载模块时)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
registry = list()

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()

>>>running register(<function f1 at 0x7f30252f0620>)
>>>>>>running register(<function f2 at 0x7f30252f08c8>)
>>>running main()
>>>registry ->  [<function f1 at 0x7f30252f0620>, <function f2 at 0x7f30252f08c8>]
>>>running f1()...
>>>running f2()...
>>>running f3()...

变量作用域规则

一个有意思的例子

1
2
3
4
5
6
7
8
9
b = 6
def fun(a):
    print(a)
    print(b)
    b = 7

fun(3)
>>>3
>>>UnboundLocalError: local variable 'b' referenced before assignment

产生这种现象的原因是:python假定在函数定义体中赋值的变量是局部变量

使用global关键字可以解决这一问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
b = 6
def fun(a):
    global b
    print(a)
    print(b)
    b = 7

fun(3)
>>>3
>>>6

闭包

闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def make_averager():
    series = list()

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total / len(series)
    return averager

avg = make_averager()
print(avg(10))
>>>10.0
print(avg(11))
>>>10.5
print(avg(12))
>>>11.0

seriesmake_averager函数的局部变量,因为那个函数的定义体中初始化了series=list()。注意,调用avg(10)时,make_averager函数已经返回了,而它的本地作用域也一去不复返了。在averager函数中,series是自由变量(free variable),指未在本地作用域中绑定的变量。

![fff](/home/lrhaoo/图片/2020-03-06 02-28-28 的屏幕截图.png)

averager的闭包延伸到那个函数的作用域之外,包含自由变量series的绑定

利用__code__属性(表示编译后的函数定义体中保存局部变量和自由变量的名称)审查make_averager

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def make_averager():
    series = list()

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total / len(series)
    return averager

avg = make_averager()
avg(10)
avg(11)
avg(12)

print(avg.__code__.co_varnames)
>>>('new_value', 'total')
print(avg.__code__.co_freevars)
>>>('series',)
print(avg.__closure__)
>>>(<cell at 0x7f647d0c6cd8: list object at 0x7f647d02e9c8>,)
print(avg.__closure__[0].cell_contents)
>>>[10, 11, 12]

series的定在返回的avg函数的__closure__属性中。avg.__closure__中的各个元素对应于avg.__code__.co_freevars中的一个名称。这些元素是cell对象,有个cell_contents属性,保存着真正的值

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量

nonlocal声明

一个有问题的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    return averager

avg = make_averager()
avg(10)
>>>UnboundLocalError: local variable 'count' referenced before assignment

问题和之前遇到的一样,当count是数字或任何不可变类型时,count += 1语句的作用其实与count = count + 1一样,所以count会被认为是局部变量,而不是外部变量

使用nonlocal解决上述问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    return averager

avg = make_averager()
print(avg(10))
>>>10.0
print(avg(11))
>>>10.5
print(avg(12))
>>>11.0

python2中没有nonlocal,只能使用可变对象进行处理

实现一个简单的装饰器(函数运行计时器)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import time

def clock(func):
    def clocked(*args):
        start = time.perf_counter()
        res = func(*args)
        end = time.perf_counter()
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (end-start, func.__name__, arg_str, res))
        return res
    return clocked

@clock
def factorial(n):
    return 1 if n < 2 else n * factorial(n-1)


if __name__ == '__main__':
    print('6! =', factorial(6))
    >>>[0.00000067s] factorial(1) -> 1
    >>>[0.00004895s] factorial(2) -> 2
    >>>[0.00006290s] factorial(3) -> 6
    >>>[0.00008678s] factorial(4) -> 24
    >>>[0.00009936s] factorial(5) -> 120
    >>>[0.00011125s] factorial(6) -> 720
    >>>6! = 720

    print(factorial.__name__)
    >>>clocked

上面的clock饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的__name____doc__属性。

使用functools.wraps装饰器可以把相关的属性从func复制到clocke

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import time
from functools import wraps

def clock(func):
    @wraps(func)
    def clocked(*args, **kwargs):
        start = time.perf_counter()
        res = func(*args, **kwargs)
        end = time.perf_counter()
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (end-start, func.__name__, arg_str, res))
        return res
    return clocked

@clock
def factorial(n):
    return 1 if n<2 else n*factorial(n-1)

print(factorial.__name__)
>>>factorial

此外,使用functools.wraps还能正确处理关键字参数kwargs

标准库中的装饰器

python内置了三个用于装饰方法的装饰器:@property, @classmethod, @staticmethod 除了上面提到的functools.wraps,还有functools.lru_cachesingledispatch

使用functools.lru_cache做备忘

functools.lru实现了记忆化的功能,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算(lrh:Least Recently Used), 缓存不会无限增长,一段时间不用的缓存条目会被扔掉

使用生成n个斐波那切数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import time
from functools import wraps, lru_cache

def clock(func):
    @wraps(func)
    def clocked(*args, **kwargs):
        start = time.perf_counter()
        res = func(*args, **kwargs)
        end = time.perf_counter()
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (end-start, func.__name__, arg_str, res))
        return res
    return clocked

@clock 
def naive_fibonacci(n):
    if n < 2:
        return n
    return naive_fibonacci(n-2) + naive_fibonacci(n-1)

@lru_cache()
@clock
def lru_fibonacci(n):
    if n < 2:
        return n
    return lru_fibonacci(n-2) + lru_fibonacci(n-1)

print(naive_fibonacci(6))
[0.00000058s] naive_fibonacci(0) -> 0
[0.00000077s] naive_fibonacci(1) -> 1
[0.00009734s] naive_fibonacci(2) -> 1
[0.00000043s] naive_fibonacci(1) -> 1
[0.00000098s] naive_fibonacci(0) -> 0
[0.00000061s] naive_fibonacci(1) -> 1
[0.00003599s] naive_fibonacci(2) -> 1
[0.00007170s] naive_fibonacci(3) -> 2
[0.00020467s] naive_fibonacci(4) -> 3
[0.00000045s] naive_fibonacci(1) -> 1
[0.00000046s] naive_fibonacci(0) -> 0
[0.00000053s] naive_fibonacci(1) -> 1
[0.00003116s] naive_fibonacci(2) -> 1
[0.00006405s] naive_fibonacci(3) -> 2
[0.00000046s] naive_fibonacci(0) -> 0
[0.00000054s] naive_fibonacci(1) -> 1
[0.00004143s] naive_fibonacci(2) -> 1
[0.00000042s] naive_fibonacci(1) -> 1
[0.00000059s] naive_fibonacci(0) -> 0
[0.00000080s] naive_fibonacci(1) -> 1
[0.00003937s] naive_fibonacci(2) -> 1
[0.00007146s] naive_fibonacci(3) -> 2
[0.00014021s] naive_fibonacci(4) -> 3
[0.00022997s] naive_fibonacci(5) -> 5
[0.00046144s] naive_fibonacci(6) -> 8
8


print(lru_fibonacci(6))
[0.00000024s] lru_fibonacci(0) -> 0
[0.00000029s] lru_fibonacci(1) -> 1
[0.00001679s] lru_fibonacci(2) -> 1
[0.00000151s] lru_fibonacci(3) -> 2
[0.00003300s] lru_fibonacci(4) -> 3
[0.00000093s] lru_fibonacci(5) -> 5
[0.00004966s] lru_fibonacci(6) -> 8
8

lru_cache以使用两个可选的参数来配置

1
functools.lru_cache(maxsize=128, typed=False)

maxsize数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize应该设为2的幂typed参数如果设为True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。顺便说一下,因为lru_cache使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数都必须是可散列的。

单分派泛函数

python不支持重载(支持重写),使用functools.singledispatch装饰器可以实现类似于重载的功能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from functools import singledispatch
from collections import abc
import numbers
import html

@singledispatch
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

@htmlize.register(str)
def _(text):
    content = html.escape(text).replace('\n', '<br>\n')
    return '<p>{0}</p>'.format(content)

@htmlize.register(numbers.Integral)
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)

@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'

singledispatch机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数

叠放装饰器

1
2
3
4
@d1
@d2
@def f():
    pass

等价于

1
2
3
def f():
    pass
f = d1(d2(f))

参数化装饰器

创建含参数装饰器的方法:创建一个装饰器工厂函数,把参数传给它,返回一个 装饰器,然后再把它应用到要装饰的函数上

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
registry = set()
def register(active=True):
    def decorate(func):
        print('running register(active=%s) -> decorate(%s)') % (active, func)
        if active:
            registry.add(func)
        else:
            registry.discard(func)
        return func
    return decorate

@register(active=False)
def f1():
    print('running f1()...')

@register(active=True)
def f2():
    print('running f2()...')

def f3():
    print('running f3()...')

注意区别,这个例子里最外层的函数的参数不是函数.通常含有参数的装饰器有三层

参数化clock装饰器

clock装饰器添加一个功能,允许用户传入一个格式字符串,控制被装饰函数的输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({arg_str}) -> {res}'

def clock(fmt=DEFAULT_FMT):
    def decorate(func):
        def clocked(*args):
            start = time.time()
            res = func(*args)
            end = time.time()
            elapsed = end - start
            name = func.__name__
            arg_str = ', '.join(repr(arg) for arg in args)
            print(fmt.format(**locals()))
            return res
        return clocked
    return decorate

@clock()
def my_sleep():
    time.sleep(2)

my_sleep()

Graham Dumpleton 和 Lennart Regebro(本书的技术审校之一)认为,装饰器最好通过实现 call 方法的类实现,不应该像本章的示例那样通过函数实现

本章延展阅读部分有很多关于装饰器的资料推荐