《流畅的Python》读书笔记9(第七章:函数装饰器和闭包)

124 阅读8分钟

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。

7.1 装饰器的基础知识

装饰器是可调用对象,其参数是另一个函数。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另外一个函数或可调用对象。

@decorate
def target():
    print('running target()')

上述代码的效果与下述写法一样:

def target():
    print('running target()')
target = decorate(target)    

示例7-1 装饰器通常把函数替换成另一个函数

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

严格来说,装饰器只是语法糖。

装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行。

7.2 Python何时执行装饰器

装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时。

示例7-2 registration.py模块

# encoding:utf-8
registry = []


def register(func):
    print('runing 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()

输出如下:

runing register(<function f1 at 0x000002073FD85730>)
runing register(<function f2 at 0x0000020750562510>)
running main()
registry -> [<function f1 at 0x000002073FD85730>, <function f2 at 0x0000020750562510>]
running f1()
running f2()
running f3()

示例7-2主要想强调,函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。这突出了Python程序员所说的导入时与运行时之间的区别。

示例7-3 promos列表中的值使用promotion装饰器填充

# encoding:utf-8
promos = []


def promotion(promo_func):
    promos.append(promo_func)
    return promo_func


@promotion
def fidelity(order):
    """为积分为1000或以上的顾客提供5%折扣"""
    return order.total() * .5 if order.customer.fidelity >= 1000 else 0


@promotion
def bulk_item(order):
    """单个商品为20个或以上时提供10%的折扣"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


@promotion
def large_order(order):
    """订单中的不同商品达到10个或以上时提供7%的折扣"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0


def best_promo(order):
    """选择可用的最佳折扣"""
    return max(promo(order) for promo in promos)

7.4 变量作用域规则

看一个可能会让你吃惊的例子

>>> b = 6
>>> def f2(a):
...     print(a)
...     print(b)
...     b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment

Python编译函数的定义体时,它判断b是局部变量,因为在函数中给赋值了。生成的字节码证实了这种判断,Python会尝试从本地环境获取b。后面调用f2(3)时,f2的定义体会获取并打印局部变量a的值,但是尝试获取局部变量b的时候,发现b没有绑定值。

这不是缺陷,而是设计选择:Python不要求声明变量,但是假定在函数定义提中赋值的变量是局部变量。

如果在函数中赋值时想让解释器把b当成全局变量,要使用global声明:

>>> b = 6
>>> def f3(a):
...     global b
...     print(a)
...     print(b)
...     b = 9
...
>>> f3(3)
3
6
>>> b
9
>>> f3(3)
3
9
>>> b = 30
>>> b
30

7.5 闭包( Closure)

闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。

示例7-9 average:计算移动平均值的高阶函数


>>> def make_averager():

...     series = []

...     def averager(new_value):

...         series.append(new_value)

...         total = sum(series)

...         return total / len(series)

...     return averager

... 

>>> avg = make_averager()

>>> avg(10)

10.0

>>> avg(11)

10.5

>>> avg(12)

11.0

在averager函数中,series是自由变量。

截屏2021-11-22 下午9.52.45.png

简单来说:闭包=函数+自由变量的引用

>>> avg.__code__.co_varnames # 局部变量

('new_value', 'total')

>>> avg.__code__.co_freevars # 自由变量

('series',)

>>> avg.__closure__ # series的绑定在返回的avg函数的__closure__属性中

(<cell at 0x7fbaa00aad08: list object at 0x7fba80034548>,)

>>> avg.__closure__[0].cell_contents

[10, 11, 12]

7.6 nonlocal声明

前面实现make_averager函数的方法效率不高。我们把所有值存储在历史数列中,然后在每次调用averager时使用sum求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个值计算均值。

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

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

  File "<stdin>", line 5, in averager

UnboundLocalError: local variable 'count' referenced before assignment

当count是数字或任何不可变类型时, count += 1 语句的作用其实与 count = count + 1一样。因此,我们在averager的定义体中为count赋值了,这会把count变成局部变量。

为了解决这个问题,Python3中引入了nonlocal声明。它的作用是把变量标记为自由变量。

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

>>> avg(10)

10.0

>>> avg(11)

10.5

>>> avg(12)

11.0

7.7 实现一个简单的装饰器

示例7-15 一个简单的装饰器,输出函数的运行时间

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

示例7-16 使用clock装饰器

import time
from clockdeco import clock

@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)')
    factorial(6)

运行结果如下:

**************************************** Calling snooze(.123)
[0.12711179s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000054s] factorial(1) -> 1
[0.00021925s] factorial(2) -> 2
[0.00023042s] factorial(3) -> 6
[0.00023575s] factorial(4) -> 24
[0.00024129s] factorial(5) -> 120
[0.00024796s] factorial(6) -> 720

7.8 标准库中的装饰器

Python内种了三个用于装饰方法的函数:property、classmethod和staticmethod。

另一个常见的装饰器是functools.wraps。标准库中最值得关注的两个装饰器是lru_cache和singledispatch。

7.8.1 使用functools.lru_cache做备忘

它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。

示例7-13 生成n个斐波那契数,递归方式非常耗时

from clockdeco import clock

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

if __name__ == "__main__":
    print(fibonacci(6))

运行结果如下:

[0.00000033s] fibonacci(0) -> 0
[0.00000063s] fibonacci(1) -> 1
[0.00059033s] fibonacci(2) -> 1
[0.00000029s] fibonacci(1) -> 1
[0.00000029s] fibonacci(0) -> 0
[0.00000025s] fibonacci(1) -> 1
[0.00000675s] fibonacci(2) -> 1
[0.00001313s] fibonacci(3) -> 2
[0.00061025s] fibonacci(4) -> 3
[0.00000025s] fibonacci(1) -> 1
[0.00000021s] fibonacci(0) -> 0
[0.00000025s] fibonacci(1) -> 1
[0.00000600s] fibonacci(2) -> 1
[0.00001208s] fibonacci(3) -> 2
[0.00000021s] fibonacci(0) -> 0
[0.00000021s] fibonacci(1) -> 1
[0.00000608s] fibonacci(2) -> 1
[0.00000021s] fibonacci(1) -> 1
[0.00000025s] fibonacci(0) -> 0
[0.00000025s] fibonacci(1) -> 1
[0.00000637s] fibonacci(2) -> 1
[0.00001221s] fibonacci(3) -> 2
[0.00002754s] fibonacci(4) -> 3
[0.00004517s] fibonacci(5) -> 5
[0.00066204s] fibonacci(6) -> 8
8

示例7-19 使用缓存实现,速度更快

import functools

from clockdeco import clock

@functools.lru_cache() # 有括号
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

if __name__ == "__main__":
    print(fibonacci(6))

执行结果:

[0.00000042s] fibonacci(0) -> 0
[0.00000058s] fibonacci(1) -> 1
[0.00060767s] fibonacci(2) -> 1
[0.00000079s] fibonacci(3) -> 2
[0.00061683s] fibonacci(4) -> 3
[0.00000054s] fibonacci(5) -> 5
[0.00062571s] fibonacci(6) -> 8
8

7.8.2 单分派泛函数

假设我们在开发一个调试Web应用的工具,我们想生成HTML,显示不同类型的Python对象。

我们可能会编写这样的函数:

import html

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

这个函数适用于任何Python类型,但是我们想做个扩展,让它使用特别的方式显示某些类型。

  • str: 把内部的换行符替换为'<br>\n';不使用<pre>,而是使用<p>。
  • int: 以十进制和十六进制显示数字
  • list: 输出一个HTML列表,根据各个元素的类型进行格式化。

因为Python不支持重载方法或函数,所以我们不能使用不同的签名定义htmlize的变体,也无法使用不同的方式处理不同的数据类型。在Python中,一种常见的做法是把htmlize变成一个分派函数,使用一串if/elif/elif,调用专门的函数,如htmlize_str、htmlize_int等等。这样不便于模块的用户扩展,还显得笨拙,时间一长,分派函数htmlize会变得很大,而且它与各个专门函数之间的耦合也很紧密。

Python3.4新增的functools.singledispatch装饰器可以把整个方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用@singledispatch装饰的普通函数会变成泛函数:根据第一个参数的类型,以不同方式执行相同操作的一组函数。

示例7-21 singledispatch创建一个自定义的htmlize.register装饰器,把多个函数绑在一起组成一个泛函数。

>>> 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>'

...

测试

>>> htmlize({1,2,3})

'<pre>{1, 2, 3}</pre>'

>>> htmlize(abs)

'<pre>&lt;built-in function abs&gt;</pre>'

>>> htmlize('Heimlich a game')

'<p>Heimlich a game</p>'

>>> htmlize('Heimlich &Co.\n a game')

'<p>Heimlich &amp;Co.<br>\n a game</p>'

>>> htmlize(42)

'<pre>42 (0x2a)</pre>'

>>> print(htmlize(['alpha', 66, {3, 2, 1}]))

<ul>

<li><p>alpha</p></li>

<li><pre>66 (0x(0:x))</pre></li>

<li><pre>{1, 2, 3}</pre></li>

</ul>

7.9 叠放装饰器

@d1
@d2
def f():
    print('f')

等同于

def f():
    print('f')

f = d1(d2(f))

7.10 参数化装饰器

Python把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器。

registry = []


def register(func): # 这是一个最简单形式的装饰器,参数函数,返回值也是函数
    print('runing register(%s)' % func)
    registry.append(func)
    return func


@register
def f1():
    print('running f1()')

print('running main()')
print('registry ->', registry)
f1()

7.10.1 一个参数化的注册装饰器

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

...     print('running fl()')

... 

running register(active=False)->decorate(<function fl at 0x7fba903c1f28>)


>>> @register()

... def f2():

...     print('running f2()')

... 

running register(active=True)->decorate(<function f2 at 0x7fba902a92f0>)

>>> def f3():

...     print('running f3()')

...