1

流畅的python--第九章 装饰器和闭包

装饰器基础知识

装饰器是一种可调用对象,其参数是另一个函数(被装饰的函数)。装饰器可能会对被装饰的函数做些处理,然后返回函数,或者把函数替换成另一个函数或可调用对象。
假如有一个名为decorate的装饰器:

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

以下写法与上面的效果一样。

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

target = decorate(target)

两种写法的最终结果一样:上面两个代码片段执行完毕后,target名称都会绑定decorate(target)返回的函数--可能是原来那个名为target的函数,也可能是另一个函数。
示例9-1 装饰器通常会把一个函数替换成另一个函数

严格来说,装饰器只是语法糖。如前所述,装饰器可以像常规的可调用对象那样调用,传入另一个函数。有时,这样做其实更方便,尤其是做元编程(在运行时改变程序的行为)时。
综上所述,装饰器有以下3个基本性质:

  • 装饰器是一个函数或其他可调用对象
  • 装饰器可以把被装饰的函数替换成别的函数
  • 装饰器在加载模块时立即执行

Python何时执行装饰器

装饰器的一个关键性质是,它们在被装饰的函数定义之后立即执行。这通常是在导入时(例如,当python加载模块时)。
示例9-2 registration.py 模块

registration.py当做脚本运行,得到的输出如下所示。

🚩 register在模块中其他函数之前运行(该例中是两次)。调用register时,传给它的参数是被装饰的函数,例如<function f1 at 0x100631bf8>

通过debug,在第一行 registry = []前面设置断点进行debug,代码执行顺序如下:

如果在main()处设置断点进行debug,代码执行顺序如下:

但是结果显示

则说明装饰器在main函数执行之前就已经执行了,因此多了红框的两个结果。

加载模块后,registry中有两个被装饰函数(f1和f2)的引用。这两个函数以及f3,只在main显示调用它们时才执行。如果是导入registration.py模块(不作为脚本运行),则输出如下所示。

这种情况下,如果查看registry的值,则得到的输出如下所示。

示例9-2主要强调,函数封装器在导入模块时立即执行,而被装饰的函数只在显示调用时运行。因此导入时和运行时存在区别。

注册装饰器

考虑到装饰器在真实代码中的常用方式,示例9-2有两处不寻常的地方。

  • 装饰器函数与被装饰的函数在同一个模块中定义。实际情况是,装饰器通常在一个模块中定义,然后再应用到其他模块中的函数上。
  • register装饰器返回的函数与通过参数传入的函数相同。实际上,大多数装饰器会在内部定义一个函数,然后将其返回。

大多数装饰器会更改被装饰的函数。通常的做法是,返回在装饰器内部定义的函数,取代被装饰的函数。涉及内部函数的代码基本上离不开闭包。为了理解闭包,需要后退一步,先研究Python中的变量作用域规则。

变量作用域规则

示例9-3 一个函数,该函数会读取一个局部变量和一个全局变量

示例9-3定义并测试了一个函数,该函数会读取两个变量的值:一个是通过函数的参数传入的局部变量a,另一个是函数没有定义的变量b。
出现错误并不奇怪。接着示例9-3,如果先给全局变量b赋值,然后再调用f1,则不会出现错误。

示例9-4 b是局部变量,因为在函数主体中给它赋值了

注意,首先输出的是3,这表明执行了print(a)语句。但是,第二个语句,即print(b),绝对不会执行。事实上,Python编译函数主体时,判断b是局部变量,因为在函数内给它赋值了。生成的字节码正是了这种判断。所以,
Python会尝试从局部作用域中获取b。后面调用f2(3)时,f2的主体顺利获取并打印局部变量a的值,但是在尝试获取局部变量b的值时,发现b没有绑定值。

🚩 Python不要求声明变量,但是会假定在函数主体中赋值的变量是局部变量。这比JavaScript的行为好多了,JavaScript也不要求声明变量,但是如果忘记把变量声明为局部变量(使用var),则可能会在不知情的情况下破坏全局变量。

在函数中赋值时,如果想让解释器把b当成全局变量,为它分配一个新值,就要使用global声明。

可以发现有两种作用域:

  • 模块全局作用域
    在类或函数块外部分配值的名称
  • f3函数局部作用域
    通过参数或者在函数主体中直接分配值的名称
    变量还有可能出现在第 3 个作用域中,我们称之为“非局部”作用域。这
    个作用域是闭包的基础,稍后详述。

比较字节码

dis模块为反汇编Python函数字节码提供了简单的方式。示例9-5和示例9-6分别是示例9-3中的f1和示例9-4中的f2的字节码。

示例9-5 反汇编示例9-3中的f1函数

几点说明:

  • 0 (print) 加载全局名称print
  • 0 (a) 加载局部名称a
  • 1 (b) 加载全局名称 b
    示例9-6 反汇编示例9-4中的f2函数
  • 1 (b) 加载局部名称 b。这表明,虽然在后面才为 b 赋值,但是编译器
    会把 b 视作局部变量,因为变量的种类(是不是局部变量)在函数
    主体中不能改变。

🚩运行字节码的 CPython 虚拟机(virtual machineVM)是栈机器,
因此 LOAD 操作和 POP 操作引用的是栈。

闭包

闭包是延伸了作用域的函数,包括函数(f)主体中引用的非全局变量和局部变量。这些变量必须来自包含f的外部函数的局部作用域。
函数是不是匿名的没有关系,关键是它能访问主体之外定义的非全局变量。
假如有个名为avg的函数,它的作用是计算不断增加的系列值的平均值,例如计算整个历史中某个商品的平均收盘价。新价格每天都在增加,因此计算平均值时要考虑到目前为止的所有价格。
示例9-7 average_oo.py: 一个计算累计平均值的类

示例9-8 average.py: 一个计算累计平均值的高阶函数

调用make_averager,返回一个averager函数对象,每次调用,averager都会把参数添加到系列值中,然后计算当前平均值。
注意这两个示例有相似之处:调用Averager()make_averager()得到一个可调用对象avg,它会更新历史值,然后计算当前平均值。在示例9-7中avgAverager类的实例,在示例9-8中,avg是内部函数averager。不管怎样,只需要调用
avg(n),把n放入系列值中,然后重新计算平均值即可。
作为 Averager 类的实例,avg 在哪里存储历史值很明显:实例属性self.series
注意,seriesmake_averager函数的局部变量,因为赋值语句series = []make_averager函数的主体中。但是调用avg(10)时,make_averager函数已经返回,局部作用域早就消失。


图9-1: averager函数的闭包延伸到自身的作用域之外,包含自由变量series的绑定

如图所示,在averager函数中,series是自由变量(free variable)。自由变量是一个术语,指未在局部作用域中绑定的变量。
示例9-10 查看make_averager创建的函数

series的值在返回的avg函数的__closure__属性中。avg.__closure__中的各项对应avg.__code__.co_freevars
中的一个名词。这些项是cell对象,有一个名为cell_contents的属性,保存着真正的值。
示例9-11 接续示例9-9

综上所述,闭包是一个函数,它保留了定义函数时存在的自由变量的绑
定。如此一来,调用函数时,虽然定义作用域不可用了,但是仍能使用
那些绑定。

🚩 注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中
的外部变量。这些外部变量位于外层函数的局部作用域内。

nonlocal 声明

前面实现make_averager函数的方法效率不高。在示例9-8中,我们把所有值存储在历史数列中,然后在每次调用averager时使用sum求和。更好的实现方式是,只存触目前的总和和项数,根据这两个数计算平均值。
示例9-12 一个计算累计平均值的高阶函数,不保存所有历史值,但有缺陷。

问题是,对于数值或任何不可变类型,count += 1 语句的作用其实与
count = count + 1 一样。因此,实际上我们在 averager 的主体中
count 赋值了,这会把 count 变成局部变量。total 变量也受这个
问题影响。示例 9-8 则没有这个问题,因为没有给 series 赋值,只是调用
series.append,并把它传给 sum len。也就是说,我们利用了“列
表是可变对象”这一事实。但是,数值、字符串、元组等不可变类型只能读取,不能更新。如果像
count = count + 1 这样尝试重新绑定,则会隐式创建局部变量。为了解决上面的问题,引入了nonlocal关键字。它的作用是把
变量标记为自由变量,即便在函数中为变量赋予了新值。如果为
nonlocal 声明的变量赋予新值,那么闭包中保存的绑定也会随之更新。
count。如此一来,count 就不是自由变量了,因此不会保存到闭包中。
示例9-13 计算累计平均值,不保存所有历史(使用nonlocal修正)

变量查找逻辑

Python 字节码编译器根据以下规则获取函数主体中出现的变量 x

  • 如果是 global x 声明,则 x 来自模块全局作用域,并赋予那个作用域中 x 的值。
  • 如果是 nonlocal x 声明,则 x 来自最近一个定义它的外层函数,并赋予那个函数中局部变量 x 的值。
  • 如果 x 是参数,或者在函数主体中赋了值,那么 x 就是局部变量。
  • 如果引用了 x,但是没有赋值也不是参数,则遵循以下规则。
    • 在外层函数主体的局部作用域(非局部作用域)内查找 x
    • 如果在外层作用域内未找到,则从模块全局作用域内读取。
    • 如果在模块全局作用域内未找到,则从__builtins__.__dict__ 中读取。

实现一个简单的装饰器

示例9-14 clockdeco0.py:一个会显示函数运行时间的简单的装饰器

import time
def clock(func):
    def clocked(*args):# 定义内部函数 clocked,它接受任意个位置参数
        t0 = time.perf_counter()
        result = func(*args)
        # 这行代码行之有效,因为 clocked 的闭包中包含自由变量 func。
        elasped = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print(f'[{elasped:0.8f}s] {name}({arg_str}) - > {result!r}')
        return result
    return clocked # 返回内部函数,取代被装饰的函数。

示例9-15 使用clock装饰器, clock_test.py

import time
from clockdeco0 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)')
    print('6!='), factorial(6)


工作原理:
如前所述,以下代码:

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

等价于

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

也就是说,在这两种情况下,factorial 函数都作为 func 参数传给
clock 函数(参见示例 9-14),clock 函数返回 clocked 函数,然后
Python 解释器把 clocked 赋值给 factorial(前一种情况是在背后赋
值)。导入 clockdeco_demo 模块,查看 factorial__name__
性,会看到如下结果。

可见,现在 factorial 保存的其实是 clocked 函数的引用。自此之
后,每次调用 factorial(n) 执行的都是 clocked(n)clocked 大致
做了下面几件事。

  1. 记录初始时间 t0
  2. 调用原来的 factorial 函数,保存结果。
  3. 计算运行时间。
  4. 格式化收集的数据,然后打印出来。
  5. 返回第 2 步保存的结果。
    这是装饰器的典型行为:把被装饰的函数替换成新函数,新函数接受的
    参数与被装饰的函数一样,而且(通常)会返回被装饰的函数本该返回
    的值,同时还会做一些额外操作。
    示例 9-14 实现的 clock 装饰器有几个缺点:不支持关键字参数,而且
    遮盖了被装饰函数的 __name__ 属性和__doc__属性。示例 9-16 使用
    functools.wraps 装饰器把相关的属性从 func 身上复制到了
    clocked 中。此外,这个新版还能正确处理关键字参数。
    示例9-16 clockdeco.py:改进后的clock装饰器
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 = [repr(arg) for arg in args]
        arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
        arg_str = ', '.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked

functools.wraps 只是标准库中开箱即用的装饰器之一。9.9 节将介绍
functools 模块中最让人印象深刻的装饰器,即 cache

9.9标准库中的装饰器

Python 内置了 3 个用于装饰方法的函数:propertyclassmethod
staticmethod
示例 9-16 用到了另一个重要的装饰器,即 functools.wraps。它的作
用是协助构建行为良好的装饰器。标准库中最吸引人的几个装饰器,即
cachelru_cachesingledispatch,均来自 functools 模块。

使用 functools.cache 做备忘

functools.cache 装饰器实现了备忘(memoization)。 这是一项优
化技术,能把耗时的函数得到的结果保存起来,避免传入相同的参数时
重复计算。
生成第 n 个斐波那契数这种慢速递归函数适合使用 @cache,如示例 9-
17 所示。
示例9-17 生成第n个斐波那契数列,递归方式非常耗时 fibo_demo.py

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

运行 fibo_demo.py 的结果如下所示。除了最后一行,其他输出都是
clock 装饰器生成的。

浪费时间的地方很明显:fibonacci(1) 调用了 8 次,fibonacci(2)
调用了 5 次……但是,如果增加两行代码,使用 cache,那么性能将显
著改善。
示例9-18 使用缓存实现,速度更快

import functools
from clockdeco import clock
@functools.cache
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
    print(fibonacci(6))


两点注意点:

  • @functools.cache 可在 Python 3.9 或以上版本中使用
  • @clock 这里叠放了装饰器:@cache 应用到 @clock 返回的函数上

叠放装饰器
如果想理解叠放装饰器,那么需要记住一点:@ 是一种语法糖,其
作用是把装饰器函数应用到下方的函数上。多个装饰器的行为就像
调用嵌套函数一样。

@alpha
@beta
def my_fn():
  ...

等同于以下内容。

my_fn = alpha(beta(my_fn))

也就是说,首先应用 beta 装饰器,然后再把返回的函数传给
alpha
如果要计算 fibonacci(30),使用示例 9-18 中的版本,总计会调用 31
次,耗时 0.000 17 秒,而示例 9-17 中未做缓存的版本在配有 Intel Core i7 处理器的笔记本计算机中则耗时 12.09 秒,因为仅 fibonacci(1)
要调用 832 040 次,总计调用 2 692 537 次。
被装饰的函数所接受的参数必须可哈希,因为底层 lru_cache 使用
dict 存储结果,字典的键取自传入的位置参数和关键字参数。
除了优化递归算法,@cache 在从远程 API 中获取信息的应用程序中也
能发挥巨大作用。

🚩 如果缓存较大,则 functools.cache 有可能耗尽所有可用
内存。在我看来,@cache 更适合短期运行的命令行脚本使用。对
于长期运行的进程,推荐使用 functools.lru_cache,并合理设
maxsize 参数。

使用 lru_cache

functools.cache 装饰器只是对较旧的functools.lru_cache 函数
的简单包装。其实,functools.lru_cache 更灵活,而且兼容 Python3.8 及之前的版本。
@lru_cache 的主要优势是可以通过 maxsize 参数限制内存用量上
限。maxsize参数的默认值相当保守,只有 128,即缓存最多只能有128 条。LRU 是“Least Recently Used”的首字母缩写,表示一段时间不用的缓存
条目会被丢弃,为新条目腾出空间。
从 Python 3.8 开始,lru_cache` 有两种使用方式。下面是最简单的方式。

  • 其一:
@lru_cache
def costly_function(a, b):
  ...
  • 其二:
@lru_cache()
def costly_function(a, b):
  ...

两种用法都采用以下默认参数。

  • maxsize=128
    设定最多可以存储多少条目。缓存满了之后,最不常用的条目会被
    丢弃,为新条目腾出空间。为了得到最佳性能,应将 maxsize 设为 2
    的次方。如果传入 maxsize=None,则 LRU 逻辑将被彻底禁用,因此
    缓存速度更快,但是条目永远不会被丢弃,这可能会消耗过多内
    存。@functools.cache 就是如此。
  • typed=False
    决定是否把不同参数类型得到的结果分开保存。例如,在默认设置
    下,被认为是值相等的浮点数参数和整数参数只存储一次,即 f(1)
    用和 f(1.0) 调用只对应一个缓存条目。如果设为 typed=True,则在
    不同的条目中存储可能不一样的结果。

以下示例不使用参数的默认值调用 @lru_cache

@lru_cache(maxsize=2**20, typed=True)
def costly_function(a, b):
  ...

单分派泛化函数

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

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

这个函数适用于任何 Python 类型,但是现在我们想扩展一下,以特别
的方式显示如下类型。

  • str
    把内部的换行符替换为 '<br/>\n',不使用 <pre> 标签,而使用<p>
  • int
    以十进制和十六进制显示数(bool 除外)。
  • list
    输出一个 HTML 列表,根据各项的类型进行格式化
  • floatDecimal
    正常输出值,外加分数形式

示例9-19 生成 HTMLhtmlize() 函数,调整了几种对象的输出

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

functools.singledispatch 装饰器可以把整体方案拆分成多个模
块,甚至可以为第三方包中无法编辑的类型提供专门的函数。使用
singledispatch 装饰的普通函数变成了泛化函数(generic function
指根据第一个参数的类型,以不同方式执行相同操作的一组函数)的入
口。这才称得上是单分派。如果根据多个参数选择专门的函数,那就是
多分派。
示例9-20 使用 @singledispatch 创建 @htmlize.register
饰器,把多个函数绑在一起组成一个泛化函数

from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers
@singledispatch #❶
def htmlize(obj: object) -> str:
    content = html.escape(repr(obj))
    return f'<pre>{content}</pre>'
@htmlize.register #❷
def _(text: str) -> str: #❸
    content = html.escape(text).replace('\n', '<br/>\n')
    return f'<p>{content}</p>'
@htmlize.register #❹
def _(seq: abc.Sequence) -> str:
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'
@htmlize.register #❺
def _(n: numbers.Integral) -> str:
    return f'<pre>{n} (0x{n:x})</pre>'
@htmlize.register #❻
def _(n: bool) -> str:
    return f'<pre>{n}</pre>'
@htmlize.register(fractions.Fraction) #❼
def _(x) -> str:
    frac = fractions.Fraction(x)
    return f'<pre>{frac.numerator}/{frac.denominator}</pre>'
@htmlize.register(decimal.Decimal) #❽
@htmlize.register(float)
def _(x) -> str:
    frac = fractions.Fraction(x).limit_denominator()
    return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'

@singledispatch 标记的是处理 object 类型的基函数。
❷ 各个专门函数使用 @«base».register 装饰。
❸ 运行时传入的第一个参数的类型决定何时使用这个函数。专门函数
的名称无关紧要,_ 是一个不错的选择,简单明了
❹ 为每个需要特殊处理的类型注册一个函数,把第一个参数的类型提
示设为相应的类型。
singledispatch 支持使用 numbers 包中的抽象基类。
boolnumbers.Integral 的子类型,但是 singledispatch逻辑
会寻找与指定类型最匹配的实现,与实现在代码中出现的顺序无关。
❼ 如果不想或者不能为被装饰的类型添加类型提示,则可以把类型传
@«base».register 装饰器。Python 3.4 或以上版本支持这种句法。
@«base».register 装饰器会返回装饰之前的函数,因此可以叠放
多个 register 装饰器,让同一个实现支持两个或更多类型。
应尽量注册处理抽象基类(例如 numbers.Integral
abc.MutableSequence)的专门函数,而不直接处理具体实现(例如
intlist)。这样的话,代码支持的兼容类型更广泛。例如,Python
扩展可以子类化 numbers.Integral,使用固定的位数实现 int 类型。
在单分派中使用抽象基类或 typing.Protocol 可以让代码支
持抽象基类或实现协议的类当前和未来的具体子类或虚拟子类。
singledispatch 机制的一个显著特征是,你可以在系统的任何地方和
任何模块中注册专门函数。如果后来在新模块中定义了新类型,则可以
轻易添加一个新的自定义函数来处理新类型。此外,还可以为不是自己
编写的或者不能修改的类编写自定义函数。

singledispatch 是经过深思熟虑之后才添加到标准库中的,功能很
多,这里无法一一说明。“PEP 443—Single-dispatch generic functions”是
不错的参考资料,不过没有讲到类型提示,毕竟类型提示出现得较
晚。functools 模块文档有所改善,singledispatch 条目下增加了几
个使用类型提示的示例。

参数化装饰器

解析源码中的装饰器时,Python 会把被装饰的函数作为第一个参数传给
装饰器函数。那么,如何让装饰器接受其他参数呢?答案是创建一个装
饰器工厂函数来接收那些参数,然后再返回一个装饰器,应用到被装饰
的函数上。是不是有点儿迷惑?肯定的。下面以我们目前见到的最简单
的装饰器 register 为例说明,如示例 9-21 所示。
示例9-21 示例 9-2 中 registration.py 模块的删减版,再次给出,方便查看

一个参数化注册装饰器

为了便于启用或禁用 register 执行的函数注册功能,为它提供一个可
选的 active 参数,当设为 False 时,不注册被装饰的函数。实现方式
如示例 9-22 所示。从概念上看,这个新的 register 函数不是装饰
器,而是装饰器工厂函数。调用 register 函数才能返回应用到目标函
数上的装饰器。
示例 9-22 为了接受参数,新的 register 装饰器必须作为函数调用

registry 现在是一个 set 对象,这样添加和删除函数的速度更快。
register 接受一个可选的关键字参数。
❸ 内部函数 decorate 是真正的装饰器。注意,它的参数是一个函数。
❹ 只有 active 参数的值(从闭包中获取)是 True 时才注册 func
❺ 如果 active 不为 True,而且 funcregistry 中,那就把它删
除。
❻ 因为 decorate 是装饰器,所以必须返回一个函数。
register是装饰器工厂函数,因此返回 decorate
@register 工厂函数必须作为函数调用,并且传入所需的参数。
❾ 即使不传入参数,register 也必须作为函数调用(@register()),返回真正的装饰器 decorate
关键是,register() 要返回 decorate。应用到被装饰的函数上的是decorate

参数化 clock 装饰器

示例 9-24 clockdeco_param.py 模块:参数化 clock 装饰器

import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): #❶
    def decorate(func): #❷
        def clocked(*_args): #❸
            t0 = time.perf_counter()
            _result = func(*_args) #❹
            elapsed = time.perf_counter() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args) #❺
            result = repr(_result)#❻
            print(fmt.format(**locals())) #❼
            return _result #❽
        return clocked #❾
    return decorate #❿
if __name__ == '__main__':
    @clock()    #⓫
    def snooze(seconds):
        time.sleep(seconds)
    for i in range(3):
        snooze(.123)

clock 是参数化装饰器工厂函数。
decorate 是真正的装饰器。
clocked 包装被装饰的函数。
_result 是被装饰的函数返回的真正结果。
_args 用于存放 clocked 的真正参数,args 是用于显示的字符串。
result_result 的字符串表示形式,用于显示。
❼ 这里使用 **locals() 是为了在 fmt 中引用 clocked 的局部变量
clocked 将取代被装饰的函数,因此它应该返回被装饰的函数返回
的值。
decorate 返回 clocked
clock 返回 decorate
⓫ 在当前模块中测试,调用 clock() 时不传入参数,因此所应用的装
饰器将使用默认的格式字符串。


示例9-25 clockdeco_param_demo1.py

import time
from clockdeco_param import clock
@clock('{name}: {elapsed}s')
def snooze(seconds):
    time.sleep(seconds)
for i in range(3):
    snooze(.123)


示例 9-26 clockdeco_param_demo2.py

import time
from clockdeco_param import clock
@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
    time.sleep(seconds)
for i in range(3):
    snooze(.123)

基于类的 clock 装饰器

示例 9-27 clockdeco_cls.py 模块:通过类实现参数化装饰器clock

import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
class clock: #❶
    def __init__(self, fmt=DEFAULT_FMT): #❷
        self.fmt = fmt
    def __call__(self, func): #❸
        def clocked(*_args):
            t0 = time.perf_counter()
            _result = func(*_args) #❹
            elapsed = time.perf_counter() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)
            result = repr(_result)
            print(self.fmt.format(**locals()))
            return _result
        return clocked

❶ 不用定义外层函数 clock 了,现在 clock 类是参数化装饰器工厂。
类名使用的是小写字母 c,以此表明这里的实现可以直接替代示例 9-24。
clock(my_format) 传入的参数赋值给这里的 fmt 参数。类构造函
数返回一个 clock 实例,my_format 被存储为 self.fmt
❸ 有了 __call__ 方法,clock 实例就成为可调用对象了。调用实例的
结果是把被装饰的函数替换成 clocked
clocked 包装被装饰的函数。

posted @ 2024-06-07 18:51  Bonne_chance  阅读(27)  评论(0编辑  收藏  举报
1