流畅的python学习笔记(四):把函数视作对象(3:函数装饰器和闭包 )

  • 函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功能,但是若想掌握,必须理解闭包。
  • nonlocal是新近出现的保留关键字,在 Python 3.0 中引入。作为Python 程序员,如果严格遵守基于类的面向对象编程方式,即便不知道这个关键字也不会受到影响。然而,如果你想自己实现函数装饰器,那就必须了解闭包的方方面面,因此也就需要知道 nonlocal。
  • 除了在装饰器中有用处之外,闭包还是回调式异步编程函数式编程风格的基础。
  • 本章的最终目标是解释清楚函数装饰器的工作原理,包括最简单的注册装饰器和较复杂的参数化装饰器。但是,在实现这一目标之前,我们要讨论下述话题:
    • Python 如何计算装饰器句法
    • Python 如何判断变量是不是局部的
    • 闭包存在的原因和工作原理
    • nonlocal 能解决什么问题
  • 掌握这些基础知识后,我们可以进一步探讨装饰器:
    • 实现行为良好的装饰器
    • 标准库中有用的装饰器
    • 实现一个参数化装饰器
1. 装饰器基础知识
  • 装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
  • 假如有个名为 decorate 的装饰器:
@decorate
def target():
	print('running target()')
  • 上述代码的效果与下述写法一样:
def target():
	print('running target()')
target = decorate(target)
  • 两种写法的最终结果一样:上述两个代码片段执行完毕后得到的target 不一定是原来那个 target 函数,而是 decorate(target)返回的函数。
  • 为了确认被装饰的函数会被替换,如下示例:
def deco(func):
    def inner():
        print('running inner()')

    return inner


@deco
def target():
    print("running target()")


if __name__ == '__main__':
    target()
    print(target)
    # running inner()
    # <function deco.<locals>.inner at 0x0000021CB9DD1700>
  • 结果表明:调用被装饰的 target 函数其实会运行 inner,审查对象,发现 target 现在是 inner 的引用。
  • 严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。
  • 综上,装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行。下一节会说明。
2. Python何时执行装饰器
  • 装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时):
registry = []


def register(func):
    print('running register({})'.format(func))
    registry.append(func)
    return func


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


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


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


if __name__ == '__main__':
    print('running main()')
    print('register ->', registry)
    f1()
    f2()
    f3()
  • output
# running register(<function f1 at 0x000001A242CE1670>)
# running register(<function f2 at 0x000001A242CE1700>)
# running main()
# register -> [<function f1 at 0x000001A242CE1670>, <function f2 at 0x000001A242CE1700>]
# running f1()
# running f2()
# running f3()
  • 值得注意的是:register 函数在模块中其他函数之前运行(两次)。调用register 时,传给它的参数是被装饰的函数。加载模块后,registry 中有两个被装饰函数的引用:f1 和 f2。这两个函数,以及 f3,只在 main 明确调用它们时才执行。
  • 我们在另一个模块引入registration.py。无任何其他代码直接执行
import registration
  • 控制台打印:
# running register(<function f1 at 0x0000016ACA862700>)
# running register(<function f2 at 0x0000016ACA862790>)
  • 上例主要为了强调:函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。这突出了 Python 程序员所说的导入时和运行时之间的区别。
  • 考虑到装饰器在真实代码中的常用方式:
    • 装饰器函数与被装饰的函数在同一个模块中定义。实际情况是,装饰器通常在一个模块中定义,然后应用到其他模块中的函数上
    • register 装饰器返回的函数与通过参数传入的相同。实际上,大多数装饰器会在内部定义一个函数,然后将其返回
  • 虽然示例中的 register 装饰器原封不动地返回被装饰的函数,但是这种技术并非没有用处。很多 Python Web 框架使用这样的装饰器把函数添加到某种中央注册处,例如把 URL 模式映射到生成 HTTP 响应的函数上的注册处。这种注册装饰器也能不会修改被装饰的函数。下一节会举例说明。
3. 使用装饰器改进“策略”模式
  • 使用注册装饰器可以改进我上一篇博客中的电商促销折扣示例。在电商策略示例中,定义体中有函数的名称,但是best_promo 用来判断哪个折扣幅度最大的 promos 列表中也有函数名称。这种重复是个问题,因为新增策略函数后可能会忘记把它添加到 promos 列表中,导致 best_promo 忽略新策略,而且不报错,为系统引入了不易察觉的缺陷。使用注册装饰器解决了这个问题:
promos = []


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

 
def best_promo(order):
    return max(promo(order) for promo in  promos)


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


@promotion
def bulk_item_promo(order):
    """单个商品为20个或以上时提供10%折扣"""
    return sum([item.total() * 0.1 for item in order.cart if item.quantity >= 20])


@promotion
def large_order_promo(order):
    """订单中的不同商品达到10个或以上时提供7%折扣"""
    return order.total() * 0.07 if len([item.product for item in order.cart]) >= 10 else 0
  • 一旦给策略函数添加装饰器,就会加载策略函数,也就是说。这里我们定义了三个策略,并添加了装饰器,也就给promos添加了三个策略函数。这样无论什么时候使用promos,promos都会存在已加载的策略函数。该方案的优点总结如下:
    • 促销策略函数无需使用特殊的名称(即不用以 _promo 结尾)。
    • @promotion 装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略:只需把装饰器注释掉。
    • 促销折扣策略可以在其他模块中定义,在系统中的任何地方都行,只要使用 @promotion 装饰即可。
  • 不过,多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函数,然后将其返回,替换被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确运作。为了理解闭包,我们要退后一步,先了解 Python中的变量作用域
4. 变量作用域规则
  • 我们定义并测试了一个函数,它读取两个变量的值:一个是局部变量 a,是函数的参数;另一个是变量 b,这个函数没有定义它。如下示例:
def f1(a):
    print(a)
    print(b)
  • 显而易见,会报错NameError: name 'b' is not defined,全局变量 b 必须定义赋值。你可能对此嗤之以鼻,那再来看一个例子:
b = 6
def f1(a):
    print(a)
    print(b)
    b = 9


if __name__ == '__main__':
    f1(3)
    # 3
    #  ...
    # UnboundLocalError: local variable 'b' referenced before assignment
  • 可以看到只打印a, 也就是说在执行print(b)时报错了,报错的原因也很清楚,局部变量b还未定义。你可能会想:在这里我想打印的是全局变量b, 而不是局部变量b。
  • 事实是:Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。Python 会尝试从本地环境获取 b。后面调用 f1(3) 时, f1 的定义体会获取并打印局部变量 a 的值,但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。
  • 这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。很多语言对此都有各自的方案,比如 JavaScript 在es6中引入了 const 和 let来解决长久以来使用 var 带来的作用域缺陷。
  • 如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明:
b = 6


def f1(a):
    global b
    print(a)
    print(b)
    b = 9


if __name__ == '__main__':
    f1(3)
    print(b)
    # 3
    # 6
    # 9
  • 了解 Python 的变量作用域之后,下一节可以讨论闭包了。
5. 闭包
  • 人们有时会把闭包和匿名函数弄混。这是有历史原因的:在函数内部定义函数不常见,直到开始使用匿名函数才会这样做。而且,只有涉及嵌套函数时才有闭包问题。因此,很多人是同时知道这两个概念的。
  • 其实,闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
  • 这个概念难以掌握,下面通过示例来理解:假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。
  • 起初,avg 是这样使用的:
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
  • avg 从何而来,它又在哪里保存历史值呢?初学者首先想到的就是利用类来实现:
class Averager:

    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total / len(self.series)
  • 由于实现了__call__,Averager 的实例是可调用对象:
if __name__ == '__main__':
    avg = Averager()
    print(avg(10))
    print(avg(11))
    print(avg(12))
    # 10.0
    # 10.5
    # 11.0
  • 接下里使用高阶函数来实现:
'''
	调用 make_averager 时,返回一个 averager 函数对象。每次调用
	averager 时,它会把参数添加到系列值中,然后计算当前平均值
'''
def make_averager():
    series = []

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

    return averager

if __name__ == '__main__':
   avg = make_averager()
   print(avg(10))
   print(avg(11))
   print(avg(12))
   # 10.0
   # 10.5
   # 11.0
  • 上面两个示例有共通之处:调用 Averager() 或make_averager() 得到一个可调用对象 avg,它会更新历史值,然后计算当前均值。在类的示例中,avg 是 Averager 的实例。 在函数示例中,avg 是内部函数 averager。不管怎样,我们都只需调用 avg(n),把 n 放入系列值中,然后重新计算均值。
  • Averager 类的实例 avg 在哪里存储历史值很明显:self.series 实例属性。但是第二个示例中的 avg 函数在哪里寻找 series 呢?
  • 注意,series 是 make_averager 函数的局部变量,因为那个函数的定义体中初始化了 series:series = []。可是,调用 avg(10)时,make_averager 函数已经返回了,而它的本地作用域也一去不复返了。
  • 在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量
  • averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定
  • 审查返回的 averager 对象,我们发现 Python 在__code__属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称:
print(avg.__code__.co_varnames) # 局部变量
print(avg.__code__.co_freevars)  # 自由变量
# ('new_value', 'total')
# ('series',)
  • series 的绑定在返回的 avg 函数的 __closure__属性中。avg.__closure__ 中的各个元素对应于avg.__code__.co_freevars 中的一个名称。这些元素是 cell 对象,有个 cell_contents属性,保存着真正的值。这些属性的值如下示例:
print(avg.__closure__)
# (<cell at 0x000001EE6C9F3BE0: list object at 0x000001EE6C912840>,)
print(avg.__closure__[0].cell_contents)
# [10, 11, 12]
  • 综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
  • 注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
6. nonlocal声明
  • 前面实现 make_averager 函数的方法效率不高,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
  • 强行运行上例会出现错误:UnboundLocalError: local variable 'count' referenced before assignment。你可能会存在疑惑为什么会报错?当 count 是数字或任何不可变类型时,count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。total 变量也受这个问题影响。
  • 那 series 为什么没有变为局部变量呢?因为我们没有给 series 赋值,我们只是调用 series.append,并把它传给 sum 和 len。也就是说,我们利用了列表是可变的对象这一事实。
  • 但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如 count = count + 1,其实会隐式创建局部变量 count。这样,count 就不是自由变量了,因此不会保存在闭包中。
  • 为了解决这个问题,Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新:
def make_averager():

    total = 0
    count = 0

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

    return averager

if __name__ == '__main__':
    avg = make_averager()
    print(avg(10))
    print(avg(11))
    print(avg(12))
    # 10.0
    # 10.5
    # 11.0
  • 至此,我们了解了 Python 闭包,下面可以使用嵌套函数正式实现装饰器了。
7. 实现一个简单的装饰器
  • 定义了一个装饰器,它会在每次调用被装饰的函数时计时,然后把经过的时间、传入的参数和调用的结果打印出来:
import time

def clock(func):

    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)  # 这行代码可用,是因为 clocked 的闭包中包含自由变量 func
        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 装饰器:
import time
from clock 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(0.123)')
    snooze(0.123)
    print('*' * 40, 'Calling factorial(6)')
    print('6! =', factorial(6))
  • output:
**************************************** Calling snooze(0.123)
[0.12354610s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000080s] factorial(1) -> 1
[0.00001960s] factorial(2) -> 2
[0.00003330s] factorial(3) -> 6
[0.00004600s] factorial(4) -> 24
[0.00005930s] factorial(5) -> 120
[0.00007420s] factorial(6) -> 720
6! = 720
  • 下面详细来讲解下这个简单装饰器的工作原理:factorial 会作为 func 参数传给 clock 函数 ,clock 函数会返回 clocked 函数。也就是说,factorial 函数此时是 clocked 函数的引用,如下所示:
print(factorial.__name__)
# clocked
  • 这样每次调用 factorial 函数,实际运行的是 clocked 函数。clocked 大致做了下面几件事。
    • 1 记录初始时间 t0
    • 2 调用原来的 factorial 函数,保存结果
    • 3 计算经过的时间
    • 4 格式化收集的数据,然后打印出来
    • 5 返回第 2 步保存的结果
  • 这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。
  • 为了让你更容易理解递归在 clocked 函数中的影响,这里我们做下改动。在 clocked 中注释最后一行代码:
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
  • 再次运行结果报错:
...
  return 1 if n < 2 else n * factorial(n - 1)
TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'
  • 其实不难理解,这里是由于 n * None 带来的错误操作,具体是在 2 * factorial(1)出错, factorial(1) 这里其实是 clocked(1),而clocked(1) 虽说计算出了result = 1, 但是没有返回,在递归函数中也就无法使用 * 相乘。
  • 实现的 clock 装饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的 __name____doc__ 属性。下面使用functools.wraps 装饰器把相关的属性从 func 复制到 clocked中。此外,这个新版还能正确处理关键字参数:
import time
import functools


def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['{}={}'.format(k, v) for k, v 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.wraps验证:
@clock
def factorial(n):
    """n!"""
    return 1 if n < 2 else n * factorial(n - 1)


if __name__ == '__main__':
	print(factorial.__name__, factorial.__doc__)
	# clocked None  没有加 @functools.wraps(func)
	# factorial n!  增加 @functools.wraps(func) 装饰器
  • 可见,functools.wraps的作用是用来消除被装饰函数内置属性和方法被替换的影响。我们需要的只是在当业务需要时替换对应的函数,而尽量不改变其他东西。常用的web框架(flask等)也用到了这个基本的装饰器。
  • functools.wraps 只是标准库中拿来即用的装饰器之一。下一节将介绍 functools 模块中最让人印象深刻的两个装饰器:lru_cachesingledispatch
8. 标准库中的装饰器
8.1 使用functools.lru_cache做备忘
  • functools.lru_cache 是非常实用的装饰器,它实现了备忘(memoization)功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。LRU 三个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉。
  • 生成第 n 个斐波纳契数,递归方式非常耗时:
@clock
def fibonacci(n):
    """第 n 个斐波纳契数"""
    return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == '__main__':
	print(fibonacci(3))
    # [0.00000030s] fibonacci(1) -> 1
    # [0.00000040s] fibonacci(0) -> 0
    # [0.00000040s] fibonacci(1) -> 1
    # [0.00001430s] fibonacci(2) -> 1
    # [0.00003740s] fibonacci(3) -> 2
    # 2
  • 浪费时间的地方很明显,每次必须调用前面 fibonacci(0)fibonacci(1),如果 n 足够大,fibonacci调用次数将呈指数级增加,耗时也相应大幅增加。
  • 但是,如果增加两行代码,使用lru_cache,性能会显著改善:
from functools import lru_cache

@lru_cache()
@clock
def fibonacci(n):
    """第 n 个斐波纳契数"""
    return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)

if __name__ == '__main__':
    print(fibonacci(10))
    # [0.00000030s] fibonacci(0) -> 0
    # [0.00000030s] fibonacci(1) -> 1
    # [0.00002620s] fibonacci(2) -> 1
    # [0.00000060s] fibonacci(3) -> 2
    # [0.00003850s] fibonacci(4) -> 3
    # [0.00000040s] fibonacci(5) -> 5
    # [0.00005040s] fibonacci(6) -> 8
    # [0.00000050s] fibonacci(7) -> 13
    # [0.00006270s] fibonacci(8) -> 21
    # [0.00000050s] fibonacci(9) -> 34
    # [0.00007570s] fibonacci(10) -> 55
    # 55
  • 可见,由于使用了缓存,n 的每个值只调用一次函数,fibonacci(10)只调用了11次 fibonacci 函数。
  • 注意,必须像常规函数那样调用 lru_cache:@lru_cache()。这么做的原因是,lru_cache 可以接受配置参数,稍后说明。
  • 除了优化递归算法之外,lru_cache 在从 Web 中获取信息的应用中也能发挥巨大作用。
  • 特别要注意,lru_cache 可以使用两个可选的参数来配置。它的签名是:
functools.lru_cache(maxsize=128, typed=False)
  • maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的幂。typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。顺便说一下,因为 lru_cache 使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的。
  • 接下来讨论吸引人的 functools.singledispatch 装饰器。
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 会变得很大,而且它与各个专门函数之间的耦合也很紧密。
  • Python 3.4 新增的 functools.singledispatch 装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用 @singledispatch 装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。如下示例:
'''
	singledispatch 创建一个自定义的htmlize.register 装饰器,
	把多个函数绑在一起组成一个泛函数
'''
import numbers, html
from functools import singledispatch
from collections import abc


@singledispatch
def htmlize(obj):
	'''@singledispatch 标记处理 object 类型的基函数。'''
    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):
	'''numbers.Integral 是int 的虚拟超类。'''
    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>'
  • 各个专门函数使用 @«base_function».register(«type»)装饰。专门函数的名称无关紧要;_ 是个不错的选择,简单明了。实际项目根据需要修改专门函数名即可。可以叠放多个 register 装饰器,让同一个函数支持不同类型。生成 HTML 的 htmlize 函数,调整了几种对象的输出:
if __name__ == '__main__':
    print(htmlize({1, 2, 3}))
    # <pre>{1, 2, 3}</pre>
    print((htmlize(abs)))
    # <pre>&lt;built-in function abs&gt;</pre>
    print(repr(htmlize('Heimlich & Co.\n- a game')))
    # '<p>Heimlich &amp; Co.<br/>\n- a game</p>'
    print(htmlize(42))
    # <pre>42 (0x2a)</pre>
    print(htmlize(['alpha', 66, {3, 2, 1}]))
    # <ul>
    # <li><p>alpha</p><li>
    # </li><pre>66 (0x42)</pre><li>
    # </li><pre>{1, 2, 3}</pre></li>
    # </ul>
  • singledispatch 机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数。@singledispath 的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数。
  • singledispatch 是经过深思熟虑之后才添加到标准库中的,它提供的特性很多,这里无法一一说明。具体请查看 singledispatch 官方文档
  • 装饰器是函数,因此可以组合起来使用(即,可以在已经被装饰的函数上应用装饰器)。下一节说明其中的原理。
9. 叠放装饰器
  • 前面在使用@lru_cache装饰器作为缓存的时候使用了叠放装饰器:@lru_cache 应用到 @clock装饰fibonacci得到的结果上。在注册泛函数时应用了两个@htmlize.register 装饰器。这两个都是叠放装饰器的典型例子。
  • 把 @d1 和 @d2 两个装饰器按顺序应用到 f 函数上,作用相当于 f =d1(d2(f))。也就是说,下述代码:
@d1
@d2
def f():
	print('f')
  • 等同于:
def f():
	print('f')
f = d1(d2(f))
  • 除了叠放装饰器之外,本章还用到了几个接受参数的装饰器,例如@lru_cache() 和 @singledispatch 生成的htmlize.register(«type»)。下一节说明如何构建接受参数的装饰器。
10. 参数化装饰器
  • 解析源码中的装饰器时,Python 把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。不明白什么意思?下面以我们见过的最简单的装饰器为例说明:
registry = []


def register(func):
    print('running register({})'.format(func))
    registry.append(func)
    return func


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

if __name__ == '__main__':
    print('running main()')
    print('registry ->', registry)
    f1()
    # running register(<function f1 at 0x000002259B9035E0>)
    # running main()
    # registry -> [<function f1 at 0x000002259B9035E0>]
    # running f1()
10.1 一个参数化的注册装饰器
  • 为了便于启用或禁用 register 执行的函数注册功能,我们为它提供一个可选的 active 参数,设为 False 时,不注册被装饰的函数:
# 使用set,这样添加和删除更快
registry = set()


def register(active=True):
    def decorate(func):
        print('running register(active={}) -> decorate({})'.format(active, func))
        # active参数的值从闭包中的自由变量中获取,为 True 时才注册 func
        registry.add(func) if active else registry.discard(func)
        # decorate 是装饰器,必须返回一个函数
        return func 

    return decorate


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


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


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


if __name__ == '__main__':
    print('running main()')
    print('registry ->', registry)
    # running register(active=False) -> decorate(<function f1 at 0x000001D21CD03670>)
    # running register(active=True) -> decorate(<function f2 at 0x000001D21CD03700>)
    # running main()
    # registry -> {<function f2 at 0x000001D21CD03700>}
  • 注意,只有 f2 函数在 registry 中;f1 不在其中,因为传给register 装饰器工厂函数的参数是 active=False,所以应用到 f1上的 decorate 没有把它添加到 registry 中。
  • 如果不使用 @ 句法,那就要像常规函数那样使用 register;若想把 f 添加到 registry 中,则装饰 f 函数的句法是 register()(f);不想添加(或把它删除)的话,句法是register(active=False)(f)。示例 演示了如何把函数添加到 registry 中,以及如何从中删除函数:
if __name__ == '__main__':
    # running register(active=False) -> decorate(<function f1 at 0x0000023F19A83670>)
    # running register(active=True) -> decorate(<function f2 at 0x0000023F19A83700>)
    print('running main()')
    # running main()
    print('registry ->', registry)
    # registry -> {<function f2 at 0x0000023F19A83700>}
    register()(f3)  # register() 表达式返回 decorate,然后把它应用到 f3 上。
    # running register(active=True) -> decorate(<function f3 at 0x0000023F19A835E0>)
    print('registry ->', registry)
    # registry -> {<function f2 at 0x0000023F19A83700>, <function f3 at 0x0000023F19A835E0>}
    register(active=False)(f2)  # 调用从 registry 中删除 f2
    # running register(active=False) -> decorate(<function f2 at 0x0000023F19A83700>)
    print('registry ->', registry)
    # registry -> {<function f3 at 0x0000023F19A835E0>}
  • 参数化装饰器的原理相当复杂,我们刚刚讨论的那个比大多数都简单。参数化装饰器通常会把被装饰的函数替换掉,而且结构上需要多一层嵌套。接下来会探讨这种函数金字塔。
10.2 参数化clock装饰器
  • 本节再次探讨 clock 装饰器,为它添加一个功能:让用户传入一个格式字符串,控制被装饰函数的输出:
import time

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


def clock(fmt=DEFAULT_FMT):
    def decorate(func):
        def clocked(*args, **kwargs):
            t0 = time.perf_counter()
            result = func(*args, **kwargs)
            elapsed = time.perf_counter() - t0
            name = func.__name__
            arg_lst = []
            if args:
                arg_lst.append(', '.join(repr(arg) for arg in args))
            if kwargs:
                pairs = ['{}={}'.format(k, v) for k, v in sorted(kwargs.items())]
                arg_lst.append(', '.join(pairs))
            arg_str = ', '.join(arg_lst)
            # 这里使用 **locals() 是为了在 fmt 中引用 clocked 的局部变量。
            print(fmt.format(**locals()))
            # print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
            return result

        return clocked

    return decorate


@clock()
def snooze(seconds):
    time.sleep(seconds)


if __name__ == '__main__':
    for i in range(3):
        snooze(0.123)
  • 在这个模块中测试,不传入参数调用 clock(),因此应用的装饰器使用默认的格式 str。打印结果如下:
[0.12296050s] snooze((0.123,)) -> None
[0.12289020s] snooze((0.123,)) -> None
[0.12296370s] snooze((0.123,)) -> None
  • 给 clock 装饰器设置如下参数格式化参数以及打印如下例所示:
@clock(fmt='{name}: {elapsed}s')
def snooze(seconds):
    time.sleep(seconds)


if __name__ == '__main__':
    for i in range(3):
        snooze(0.123)
        # snooze: 0.12223020000000001s
        # snooze: 0.12385490000000005s
        # snooze: 0.12288070000000006s
@clock(fmt='{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
    time.sleep(seconds)


if __name__ == '__main__':
    for i in range(3):
        snooze(0.123)
        # snooze((0.123,)) dt=0.123s
        # snooze((0.123,)) dt=0.123s
        # snooze((0.123,)) dt=0.123s
小结
  • 本章介绍了很多基础知识,虽然学习之路崎岖不平,我还是尽可能让路途平坦顺畅。毕竟,我们已经进入元编程领域了。
  • 开始,我们先编写了一个没有内部函数的 @register 装饰器;最后,我们实现了有两层嵌套函数的参数化装饰器 @clock()。
  • 尽管注册装饰器在多数情况下都很简单,但是在高级的 Python 框架中却有用武之地。我们使用注册方式对之前的“策略”模式做了重构。
  • 参数化装饰器基本上都涉及至少两层嵌套函数,如果想使用@functools.wraps 生成装饰器,为高级技术提供更好的支持,嵌套层级可能还会更深,比如前面简要介绍过的叠放装饰器。
  • 我们还讨论了标准库中 functools 模块提供的两个出色的函数装饰器:@lru_cache() 和 @singledispatch。
  • 若想真正理解装饰器,需要区分导入时和运行时,还要知道变量作用域、闭包和新增的 nonlocal 声明。掌握闭包和 nonlocal 不仅对构建装饰器有帮助,还能协助你在构建 GUI 程序时面向事件编程,或者使用回调处理异步 I/O。
  • 总而言之,装饰器是你作为python程序员进阶必须迈过的一道坎,如果你看到了这篇文章还不理解什么是装饰器,建议多看几遍。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值