Python - 一文搞懂装饰器和闭包

装饰器

装饰器是一种可调用对象,其参数是另外一个函数(被装饰的函数)

装饰器可能会对被装饰的函数做些处理,然后返回被装饰的函数。或者把函数替换成另一个函数或可调用对象

>>> def deco(func):
...     def inner():
...             print('running inner()')
...     return inner
...
>>> @deco
... def target():
...     print('running target()')
...
>>> target()
running inner()
>>> t = deco(target)   # 等价于 target()
>>> t
<function deco.<locals>.inner at 0x000001D3F6A1CA60>
>>> t()
running inner()
>>>

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

综上所述,装饰器有以下三个基本特性:

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

需求:扩展如下登录函数,使其登陆前进行验证

def login():
    print('---开始登录---')

实现方式一:

def func_out(func_name):
    def func_in():
        print('---开始验证---')
        # 调用原函数
        func_name()

    return func_in

def login():
    print('---开始登录---')

ret = func_out(login)
ret()

实现方式二:

def func_out(func_name):
    def func_in():
        print('---开始验证---')
        # 调用原函数
        func_name()
    return func_in

@func_out   
def login():
    print('---开始登录---')

# 当调用该方法时,会先执行装饰器并执行内部方法
login()  # login() == func_in()
print(login)
 
# out:
"""
---开始验证---
---开始登录---
<function func_out.<locals>.func_in at 0x000002DF5980EDD0>
"""

装饰器的执行时期

# registration.py 

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

out:

running register(<function f1 at 0x0000029818DBED40>)
running register(<function f2 at 0x0000029818DBEDD0>)
running main()
registry ->  [<function f1 at 0x0000029818DBED40>, <function f2 at 0x0000029818DBEDD0>]
running f1()
running f2()
running f3()

register() 函数在main 函数执行之前已经执行了两次,因此它们是在被装饰的函数定义之后立即运行,这通常也是在Python加载模块时。

如下导入模块时装饰器函数执行了:

注册装饰器

考虑到装饰器在真是代码中的常用方式,上例有两处不寻通常的的地方:

  • 装饰器函数与被装饰的函数在同一个模块中定义。实际情况是,装饰器通常在一个模块中定义,然后再应用到其他模块中的函数上

  • register 装饰器返回的函数与通过参数传入的函数相同。实际上,大多数装饰器会在内部定义一个函数,然后将其返回。

上例的register 装饰器原封不动地返回了被装饰的函数,但是这种技术并非毫无用处。很多Python 框架会使用这样的装饰器把函数添加到某中央注册处,例如把URL模式映射到生成HTTP响应的函数的注册处。这种注册装饰器可能会也可能不会更改被装饰的函数

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

装饰带有参数的函数

def func_out(func_name):
    def func_in(username, password):
        print('---开始验证---')
        print(f"function_in => username:{username}, password:{password}")
        func_name(username, password)

    return func_in



@func_out
def login(username, password):
    print('---开始登录---')
    print(f"login => username:{username}, password:{password}")

login('admin', 123456)


当函数有可变参数该怎么传递?
def func_out(func_name):
    def func_in(*args, **kwargs):
        print('---开始验证---')
        print(f"function_in => args:{args}, kwargs:{kwargs}")
        
        # 参数注意拆包
        func_name(*args, **kwargs)

    return func_in


# @func_out 等价于调用func_out(), 此时login() == func_in()
@func_out
def login(*args, **kwargs):
    print('---开始登录---')
    print(f"login => args:{args}, kwargs:{kwargs}")

login('admin', id = 123456)

"""
output:
---开始验证---
function_in => args:('admin',), kwargs:{'id': 123456}
---开始登录---
login => args:('admin',), kwargs:{'id': 123456}
"""

装饰带有返回值的函数

def func_out(func_name):
    def func_in(num):
        print('扩展.....')
        return func_name(num)

    return func_in

@func_out
def login(num):
    return 10 + num

print(login(12))

变量作用域规则

>>> b = 6
>>> def f1(a):
...     print(a)
...     print(b)
...
>>> f1(3)
3
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

为什么f2()内 print(b) 会报 UnboundLocalError: local variable 'b' referenced before assignment? Python 编译函数的定义体时,它判断b 是局部变量(有赋值操作),所以Python 会尝试从局部作用域中获取b,但发现变量b没有绑定值所以报错

这不是bug,而是一种设计选择:Python 不要求声明变量,但是会假定在函数主体中赋值的变量是局部变量

反汇编结果:

f1 加载全局b:

f2 加载局部b:

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

通过上面示例可以发现两种作用域

  • 模块全局作用域
    在类或函数块外部分配值的名称
  • f3 函数局部作用域
    通过函数或者在函数主体中直接分配值的名称

变量还有可能出现在第3个作用域中,我们称之为“非局部”作用域

闭包概念

闭包(closure):前提涉及嵌套函数,指延伸了作用域的函数(内部函数),包括函数(姑且叫f把) 主体中 引用的非全局变量和局部变量
这些变量必须来自包含f的外部函数的局部作用域

#示例9-8 计算平均值的函数

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()
    a = avg(10)
    b = avg(11)
    c = avg(12) 
    print(f'a = {a}, b = {b}, c = {c}') #out :a = 10.0, b = 10.5, c = 11.0
 
    print(avg.__code__.co_varnames)  # out:('new_value', 'total') ->  averager 中的局部变量
    print(avg.__code__.co_freevars)  # out:('series',) -> averager 中的 自由变量
    # series 的绑定在返回的avg函数的__closure__ 属性中
    print(avg.__closure__)  # out: (<cell at 0x000001FA33C53FD0: list object at 0x000001FA338DD3C0>,)
    print(avg.__closure__[0].cell_contents)  # [10, 11, 12] 

在averager函数中,series是自由变量,这是一个技术术语,指未在本地作用域中绑定的变量

__code__: 表示编译后的函数主体,保存局部变量和自由变量的名称
series 的返回值在avg的__closure__

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

nonlocal 关键字

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

示例 9-12 中的实现有缺陷,只是为了阐明观点。

# 示例 9-12 
>>> 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 += 1 语句的作用其实与 count = count + 1一样。因此,实际上我们在averger的主体中为count 赋值了,这会把 count 变成局部变量。total 变量也受这个影响。

示例9-8则没有这个问题,因为没有给series赋值,只是调用series.append,并把它传给sum和len。也就是说,我们利用了"列表是可变对象"这一事实。

但是,数值、字符串、元组等不可变类型只能读取,不能更新。如果像count = count + 1 这样尝试重新绑定,则会隐士创建局部变量count。如此一来,count就不是自由变量了,因此不会保存到闭包中。为了解决这个问题,Python 3 引入了 nolocal 关键字。它的作用是把变量标记为自由变量,即便在函数中为变量赋予了新值。如果为nolocal 声明的变量赋予新值,那么闭包中保存的绑定也会随更新,最新版make_averager 的正确实现如示例 9-13所示。

# 示例 9-13

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count, total   # 将count, total 声明为自由变量
        count += 1
        total += new_value
        
        return total /  new_value         
    return averager   
if __name__ == '__main__':
    avg = make_averager()
    print(avg.__code__.co_varnames)
    print(avg.__code__.co_freevars)

#out:
'''
('new_value',)
('count', 'total')
'''

变量查找逻辑

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

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

实现一个简单装饰器

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

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

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)')
    # factorial(6)
    print('6!= ', factorial(6))


out:

'''
**************************************** Calling snooze(.123)
[0.13728410s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000070s] factorial(1) -> 1
[0.00001800s] factorial(2) -> 2
[0.00003060s] factorial(3) -> 6
[0.00003780s] factorial(4) -> 24
[0.00004600s] factorial(5) -> 120
[0.00005540s] factorial(6) -> 720
6!=  720
'''

工作原理:

如前所述,以下内容:

@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 函数,clock 函数返回 clocked 函数,然后Python解释器把 clocked 赋值给factorial(前一种情况在背后赋值)。导入clockdeco_demo.py 模块,查看factorial 的 __name__ 属性,会看到结果:

>>> import clockdeco_demo     
>>> clockdeco_demo.factorial.__name__ 
'clocked'
>>>

可见,现在factorial 保存的其实是clocked 函数的引用。自此之后,每次调用 factorial(n) 执行的都是 clocked(n)。

装饰装饰器的典型行为:把被装饰的函数替换成新函数,新函数接收的参数与被装饰的函数一样,而且(通常)会返回被装饰的函数本该返回的值,同时还会做一些额外的操作。
示例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())
        args_str = ', '.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {name}({args_str}) -> {result!r}')
        return result
    return clocked

functools.wraps 的作用:保留被装饰函数的元信息

>>> import clockdeco_demo
>>> clockdeco_demo.factorial.__name__
'factorial'
>>>       

标准库中的装饰器

使用functools.cache 做备忘

functools.cache 装饰器实现了备忘。这是一项优化技术、能把耗时的函数得到的结果保存起来,避免传如相同的参数时重复计算。

生成 n 个 斐波那契数这种慢速递归适合使用 @cache。如示例 9-17 所示。

# 示例9-17,生成 n 个 斐波那契数,递归方式非常耗时
@clock
def fibonacci(n):
    return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)


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

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

'''
[0.00000050s] fibonacci(0) -> 0
[0.00000030s] fibonacci(1) -> 1
[0.00002960s] fibonacci(2) -> 1
[0.00000020s] fibonacci(1) -> 1
[0.00000040s] fibonacci(0) -> 0
[0.00000010s] fibonacci(1) -> 1
[0.00001000s] fibonacci(2) -> 1
[0.00001830s] fibonacci(3) -> 2
[0.00006150s] fibonacci(4) -> 3
[0.00000020s] fibonacci(1) -> 1
[0.00000010s] fibonacci(0) -> 0
[0.00000010s] fibonacci(1) -> 1
[0.00000810s] fibonacci(2) -> 1
[0.00001570s] fibonacci(3) -> 2
[0.00000020s] fibonacci(0) -> 0
[0.00000020s] fibonacci(1) -> 1
[0.00000800s] fibonacci(2) -> 1
[0.00000010s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00000010s] fibonacci(1) -> 1
[0.00000820s] fibonacci(2) -> 1
[0.00001600s] fibonacci(3) -> 2
[0.00003160s] fibonacci(4) -> 3
[0.00005490s] fibonacci(5) -> 5
[0.00012730s] fibonacci(6) -> 8
8
'''

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

# 使用缓存实现,速度更快
from clockdeco0 import clock
import functools

@functools.cache # 1
@clock # 2
def fibonacci(n):
    return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)

if __name__ == '__main__':
    print(fibonacci(6))
  1. 这一行可在Python 3.9 或 以上版本使用。
  2. 这里叠放了装饰器:@cache 应用到 @clock 返回的函数上。

tips: 叠放装饰器

@是一种语法糖,其作用是把装饰器函数应用到下方的函数上。多个装饰器的行为就像调用嵌套函数一样。以下内容

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

等同于以下内容。
  my_fn = alpha(beta(my_fen))
也就是说,首先应用 beat 装饰器,然后再把返回的函数传给 alpha

这样一来,对于每个n值,fibonacci 函数只被调用一次。执行结果:

[0.00000050s] fibonacci(0) -> 0
[0.00000040s] fibonacci(1) -> 1
[0.00004190s] fibonacci(2) -> 1
[0.00000040s] fibonacci(3) -> 2
[0.00005290s] fibonacci(4) -> 3
[0.00000040s] fibonacci(5) -> 5
[0.00006380s] fibonacci(6) -> 8
8

被装饰的函数所接受的参数必须是可哈希的,因此底层lru_cache 使用dict 存储结果,字典的键取自传入的位置参数和关键字参数。除了优化递归算法,@cache 在从远程API 中获取信息的应用程序中也能发挥巨大作用。

使用@lur_cache

functools.cache 装饰器只是对较旧的functools.lru_cache 函数的简单包装。其实,functools.lur_cache 更灵活,而且兼容Python 3.8 及之前的版本。

@lur_cache 的主要优势是可以通过 maxsize 参数限制内容用量上限。maxsize 参数的默认值相当保守,只有128,即缓存最多只能有128条。

LRU是"Least Recently Used" 的首字母缩写,表示一段时间不用的缓存条目会被丢弃,为新条目腾出空间。

从Python 3.8 开始,lru_cache 有使用使用方式。下面是最简单的方式

@lru_cache
def costly_function(a,b):
  pass

另一中方式是从Python 3.2 开始支持的加上()作为函数调用。

@lru_cache()
def costly_function(a,b):
  pass

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

maxsize=128
设定最多可以存储多少条目。缓存满了之后,最不常用的条目会被丢弃,为新条目腾出空间。为了得到最佳的性能,应将maxsize 设为2的次方。如果传入maxsize=None,则LRU 逻辑将被彻底禁用,因此缓存速度更快,但是条目永远不会被丢弃,这可能会消耗过多内存。@functools.cache 就是如此。

typed=Flase
决定是否把不同参数类型得到的结果分开保存。例如,在默认设置下,被认为是值相等的浮点参数和整数参数只存储一次,即f(1)调用和f(1.0)调用只对应一个缓存条目。如果设定为typed=True ,则在不同的条目中存储可能不一样的结果。

参考:

https://zhuanlan.zhihu.com/p/665989168
https://zhuanlan.zhihu.com/p/570330375
https://blog.csdn.net/weixin_44799217/article/details/129944149

单分派泛化函数

假设我们正在开发一个调试 Web 应用程序的工具,想生成HTML,以显示不同类型的Pyhton对象。为此,可能会编写如下函数:

import html

def htmlize(obj: object) -> str:
    content = html.escape(obj)
    return f'<pre>{content}<pre>'

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

str: 把内部的换行符替换为'<br/>\n', 不适用<pre> 标签,而使用<p>

int: 以十进制和十六进制显示数(bool除外)

list: 输出一个HTML列表,根据各项的类型进行格式化

float 和 Decimal: 正常输出值,外加分数形式

我们想要的行为如示例9-19所示:

# 生成HTML 的htmlize()函数,调整了几种对象的输出
>>> htmlize({1,2,3})  # 1
'<pre>{1, 2, 3}<pre>'
>>> htmlize(abs)
'<pre>&lt;built-in function abs&gt;<pre>'
>>> htmlize('Heimlich & Co.\n- a game') # 2
'<p>Heimlich &amp; Co.<br/>\n- a game</p>'
>>> htmlize(42) # 3
'<pre>42 (0x101010)</pre>'
>>> htmlize(['alpha',66,{3,2,1}])  # 4
'<ul>\n<li><p>alpha</p></li>\n<li><pre>66 (0x1000010)</pre></li>\n<li><pre>{1, 2, 3}<pre></li></ul>'
  1. htmlize() 函数本身就针对object,相当于不匹配 其他特殊参数类型时的一种兜底实现
  2. str 对象也做HTML转义,不过是放在<p></p> 内,而且在每个\n 之前插入换行标签 <br>/
  3. 以十进制和二进制显示int值,放在<pre><pre>
  4. 列表中的各项根据类型被格式化,整个序列会被渲染成一个HTML列表

单分派函数

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

使用@singledispathc 装饰得普通函数变成了泛化函数(generic function,指根据第一个参数得类型,以不同方式执行相同操作得一组函数)得入口。这才称得上是单分派。如果根据多个参数选择专门得函数,那就是多分派。具体是做法如示例9-20所示

import html
import functools
from collections import abc
import numbers


@functools.singledispatch # 1
def htmlize(obj: object) -> str: 
    content = html.escape(repr(obj))
    return f'<pre>{content}<pre>'


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


@htmlize.register  # 4
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 # 5
def _(n: numbers.Integral) -> str:
    return f'<pre>{n} (0x{n:b})</pre>'

@htmlize.register # 6
def _(n: bool) -> str:
    return f'<pre>{n}</pre>'


@htmlize.register(fractions.Fraction) # 7
def _(x) -> str:
    frac = fractions.Fraction(x)
    return f'<pre>{frac.numerator}/{frac.denominator}</pre>'


@htmlize.register(decimal.Decimal) # 8
@htmlize.register(float)
def _(x):
    frac = fractions.Fraction(x).limit_denominator()
    return f'<pre> {x}  ({frac.numerator} / {frac.denominator})</pre>'
  1. @singledispatch 标记得是处理object 类型得函数
  2. 各个专门函数使用 @<>.register 装饰
  3. 运行时传入得第一个参数得类型决定何时使用这个函数。专门函数得名称无关紧要,_是一个不错得选择。简单明了
  4. 为每个需要特殊处理得类型注册一个函数。把第一个参数得类型提示设为相应得类型。
  5. singledispatch 支持使用 numbers 包中的抽象基类
  6. bool 是numbers.Integral 的子类型,但是singledispatch 逻辑会寻找与指定类型最匹配的实现,与实现在代码中出现的顺序无关。
  7. 如果不想或者不能为被装饰的类型添加类型提示,则可以把类型传给@<>.register 装饰器。Python 3.4 或以上版本支持这种句法。
  8. @<>.register 装饰器会返回装饰器之前的函数,因此可以叠放多个register 装饰器,让同一个实现支持两个或更多类型

应尽量注册处理抽象基类(例如numbers.Integral 和 abc.MutableSequence) 的专门函数,而不直接处理具体实现(例如int 和 list)。这样的话,代码支持的兼容类型更广泛。例如,Python扩展可以子类化numbers.Integral,使用固定的位数实现int 类型

functools.singledispatch 装饰器可以把整体方案拆分成多个模块,甚至可以为第三方包中无法编辑得类型提供专门得函数。

例如,singledispatch_demo.py 有如下定义:

@functools.singledispatch
def htmlize(obj: object) -> str:
    content = html.escape(repr(obj))
    return f'<pre>{content}<pre>'

我们在另一个模块中导入htmlize后,可以直接定义处理其他类型参数得方法,之后就可以直接使用。

# demo01.py
from singledispatch_demo import htmlize

@htmlize.register
def _(n: bool) -> str:
    return f'<pre>{n}</pre>'  


if __name__ == '__main__':
    print(htmlize(True))  #out: <pre>True</pre>

参数化装饰器

解析源码的中的装饰器时,Python 会把 被装饰的函数作为第一个参数传给装饰器函数。那么,如果让装饰器接受其他参数哪?答案是创建一个装饰器工厂函数来接收那些参数,然后再返回一个装饰器,应用到被装饰的函数上。

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

# 为了接受参数,新的registger 装饰必须作为函数调用
registry = set() # 1

def register(active=True): # 2
    def decorate(func): # 3
        print('running register'
              f'(active={active}) -> decorate({func})')
        
        if active: 
            registry.add(func)
        else:
            registry.discard(func) 
        return func # 4
    return decorate # 5


@register(active=False) # 6
def f1():
    print('running f1()')
    
    
@register()  # 7
def f2():
    print('running f2()')
    

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

  1. register 现在是一个set对象,这样添加和删除函数的速度更快
  2. register 接受一个可选的关键字参数
  3. 内部函数decorate 是真正的装饰器。注意,它的参数是一个函数
  4. 因为decorate 是装饰器,所以必须返回一个函数
  5. register 是装饰器工厂函数,因此返回装饰器decorate
  6. @register 工厂函数必须作为函数调用,并且传入所需的参数
  7. 即使不传入参数,register 也必须作为函数调用(@register()), 返回真正的装饰器decorate。

关键是,register() 要返回 decorate。应用到被装饰的函数上的是decorate

示例9-22中的代码再register_factory.py 。导入该模块,得到的结果如下所示:

>>> import register_factory
running register(active=False) -> decorate(<function f1 at 0x000001D8C6C3BAC0>)
running register(active=True) -> decorate(<function f2 at 0x000001D8C6C3A560>)
>>> register_factory.registry
{<function f2 at 0x000001D8C6C3A560>}
>>>

注意,只有f2 出现在了registry 中,f1不在其中,因为传给registry装饰器工厂函数的参数是active=False,所以应用到f1上的decorate 没有把它添加到registry中。

如果不使用@句法,那么就要像常规函数那样调用register 。如果想把f 添加到 registry 中,那么装饰f函数的句法是register()(f);如果不想添加f(或者把它删除),则句法是register(active=Flase)(f).
示例9-23 演示了如何把函数添加到register中,以及如何把从中删除函数。

>>> from register_factory import *
running register(active=False) -> decorate(<function f1 at 0x0000020A5ED997E0>)
running register(active=True) -> decorate(<function f2 at 0x0000020A5ED99870>)
>>> registry # 1
{<function f2 at 0x0000020A5ED99870>}
>>> register()(f3) # 2
running register(active=True) -> decorate(<function f3 at 0x0000020A5ED991B0>)
<function f3 at 0x0000020A5ED991B0>
>>> registry # 3
{<function f3 at 0x0000020A5ED991B0>, <function f2 at 0x0000020A5ED99870>}
>>> register(active=False)(f2) # 4
running register(active=False) -> decorate(<function f2 at 0x0000020A5ED99870>)
<function f2 at 0x0000020A5ED99870>
>>> registry # 5
{<function f3 at 0x0000020A5ED991B0>}
  1. 导入这个模块时,f2 在registry 中
  2. registe() 表达式返回 decotate 并应用到f3 上
  3. 前一行把 f3 添加到 registry 中
  4. 这个调用从registry 中删除f2
  5. 确认registry 中只有f3

参数化装饰器的原理相当复杂,刚刚讨论的那个例子比大多数例子简单。参数化装饰器通常会把装饰的函数替换掉,而且结构上需要多一层嵌套。

参数化clokc 装饰器

import time

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


def clock(fmt=DEFAULT_FMT): # 1
    def decorate(func):  # 2
        def clocked(*_args): # 3
            print('clocked run') 
            t0 = time.perf_counter()
            _result = func(*_args) # 4
            elapsed = time.perf_counter() - t0
            name = func.__name__
            args = (','.join(repr(item) for item in _args)) # 5
            result = repr(_result) # 6
            print(locals())      # 获得当前函数的所有局部变量
            print(fmt.format(**locals())) # 7     # 关键字参数按键进行传参
            return _result # 8
        return clocked # 9
    return decorate # 10


if __name__ == '__main__':
    @clock()
    def snooze(secsonds):
        time.sleep(secsonds)

    snooze(.123)

输出:

{'_args': (0.123,), 't0': 25155.3770726, '_result': None, 'elapsed': 0.1346266000000469, 'name': 'snooze', 'args': '0.123', 'result': 'None', 'fmt': '[{elapsed:0.8f}s] {name}({args}) -> {result}', 'func': <function snooze at 0x0000028587D391B0>}
[0.13462660s] snooze(0.123) -> None
  1. clock 是参数化工厂函数
  2. decorate 是真正的装饰器函数
  3. clocked 包装被装饰的函数
  4. _result 是被装饰函数返回的真正结果
  5. _args 用于存放clocked 的真正参数,args是用于显示的字符串
  6. result 是_result 的字符串表示形式,用于显示
  7. 这里使用**locals() 是为了在fmt中引用 clokced 的局部变量
  8. clocked 将取代被装饰的函数,因此它应该返回被装饰的函数的返回值
  9. decorate 返回clocked
  10. clock 返回 decorate
  11. 在当前模块中测试,调用clock() 时不传入参数,因此所应用的装饰器将使用默认的格式字符串

装饰器最好通过定义了__call__方法的类实现,不应像文章这样通过函数实现。我同意类更适合创建重要的装饰器,但是为了讲解这个语言功能的基本思想,函数更易于理解

示例 9-14 的等效的类形式的装饰器

import time

class clock:

    def __call__(self, func):
        def clocked(*args):
            t0 = time.perf_counter()
            result = func(*args)
            elapsed = time.perf_counter() - t0
            name = func.__name__
            args_str = ', '.join(repr(arg) for arg in args)
            print(f'[{elapsed:0.8f}s] {name}({args_str}) -> {result!r}')
            return result
        return clocked

if __name__ == '__main__':
    @clock()  # @clokc 不能生效
    def snooze(secsonds):
        time.sleep(secsonds)

    snooze(.123)

基于类的clock装饰器

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


if __name__ == '__main__':

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

    snooze()
posted @ 2022-01-12 10:49  chuangzhou  阅读(23)  评论(0编辑  收藏  举报