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
- 定义内部函数clocked,它接受任意个位置参数
- 这行代码行之有效,因为clocked 的闭包中包含自由变量 func
- 返回内部函数,取代被装饰的函数
# 示例 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))
- 这一行可在Python 3.9 或 以上版本使用。
- 这里叠放了装饰器:@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><built-in function abs><pre>'
>>> htmlize('Heimlich & Co.\n- a game') # 2
'<p>Heimlich & 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>'
- htmlize() 函数本身就针对object,相当于不匹配 其他特殊参数类型时的一种兜底实现
- str 对象也做HTML转义,不过是放在
<p></p>
内,而且在每个\n
之前插入换行标签<br>/
- 以十进制和二进制显示int值,放在
<pre><pre>
内 - 列表中的各项根据类型被格式化,整个序列会被渲染成一个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>'
- @singledispatch 标记得是处理object 类型得函数
- 各个专门函数使用 @<>.register 装饰
- 运行时传入得第一个参数得类型决定何时使用这个函数。专门函数得名称无关紧要,_是一个不错得选择。简单明了
- 为每个需要特殊处理得类型注册一个函数。把第一个参数得类型提示设为相应得类型。
- singledispatch 支持使用 numbers 包中的抽象基类
- bool 是numbers.Integral 的子类型,但是singledispatch 逻辑会寻找与指定类型最匹配的实现,与实现在代码中出现的顺序无关。
- 如果不想或者不能为被装饰的类型添加类型提示,则可以把类型传给@<>.register 装饰器。Python 3.4 或以上版本支持这种句法。
- @<>.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()')
- register 现在是一个set对象,这样添加和删除函数的速度更快
- register 接受一个可选的关键字参数
- 内部函数decorate 是真正的装饰器。注意,它的参数是一个函数
- 因为decorate 是装饰器,所以必须返回一个函数
- register 是装饰器工厂函数,因此返回装饰器decorate
- @register 工厂函数必须作为函数调用,并且传入所需的参数
- 即使不传入参数,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>}
- 导入这个模块时,f2 在registry 中
- registe() 表达式返回 decotate 并应用到f3 上
- 前一行把 f3 添加到 registry 中
- 这个调用从registry 中删除f2
- 确认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
- clock 是参数化工厂函数
- decorate 是真正的装饰器函数
- clocked 包装被装饰的函数
- _result 是被装饰函数返回的真正结果
- _args 用于存放clocked 的真正参数,args是用于显示的字符串
- result 是_result 的字符串表示形式,用于显示
- 这里使用**locals() 是为了在fmt中引用 clokced 的局部变量
- clocked 将取代被装饰的函数,因此它应该返回被装饰的函数的返回值
- decorate 返回clocked
- clock 返回 decorate
- 在当前模块中测试,调用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()
本文来自博客园,作者:chuangzhou,转载请注明原文链接:https://www.cnblogs.com/czzz/p/15791576.html