装饰器一层套一层时,解读起来尤其困难。关键在于理解程序执行的流程;对于带参数的装饰器,关键是发现使用上 @log
和 @log()
的区别。
python 执行程序前,会先对整个模块 “编译” 一遍,把上下文(如函数,变量)翻译成代码,然后拿着上下文,照着代码逐行解释执行。
在这个过程中,如果碰到了 “@” 修饰的行,就会立即执行后面的语句,如果@后面是“log”,它修饰在 def fn1上,就会立即执行 log=log(func)
不信可以运行一下以下代码:
def log(func):
[断点A]print(func.__name__)
@log
def fn():
print(1+1)
以上代码仅仅是定义,但仍然会命中断点 A,因为在定义阶段有执行了。
完全执行后,vscode 里可以看到 None 被绑定到“fn” 函数名上,这是因为 python 里无 return 的函数其实是有返回类型 None 的,在执行fn=log(fn)时,就得到了 None
因此一般性的装饰器是这样
def log(func):
def wrapper():
return func() # 调用log.<locals> 作用域中的 func 参数。
return wrapper
@log
def fn():
先看返回, return wrapper 意味着函数 wrapper 被绑定到变量名上,wrapper是在 log 的局部作用域中像定义变量那样定义的一个函数变量。
因此,调试会发现 vscode 告诉你绑定到 fn
上的东西是 <function log.<locals>.wrapper at...>
所以代码里执行 fn 的时候,执行的是 wrapper()
,此时 fn
是 log.wrapper
的别名(alias)
那么参数怎么入参?
写这样:
def log(func):
def wrapper(*a,**ka):
return func(*a,**ka)
return wrapper
@log
def fn():
*
和**
分别是打包和解包运算符。在遇到 def 时,*会按照位置打包传入参数为元组(position arguments),**
会按照打包指定了参数的入参(named arguments),并且打包为词典。
python 约定,传参时位置参数必须在前,具名参数在后,两者囊括全部的入参类型。
在执行阶段 *
和**
会解包变量。因此本段代码定义后, 执行 fn相当于执行 wrapper(*a,**ka)
如果要在装饰器中加入参数,应该怎么做?
不妨先从使用侧来看:装饰器使用时,会由“@log
”变成“@log(time="Now")
”, 这里很绕的一个点是括号。
初学者的想法是,被装饰函数会由魔法传入括号里,因此写出这样的函数。
错误示范:!
def log(func,time):
def wrapper(*a,**ka):
return func(*a,**ka)
return wrapper
@log(time="Now") # 定义阶段跳 “没有func参数”
def fn(time):
正确的理解是,** “@” 修饰的行会把后续字符当做描述型的字符,会先解释一遍,再作为函数执行。如果 @ 后面是“log()”,它执行的是fn=log()(fn)
,,是返回值充当函数。**
因此,应当在第二层检查func
的入参
def log4(time="Now"):
def decorator(func):
此时应该理解为,func=log4(time="Now")(func)
,如果decorator执行的结果是返回func(*a,*kw)
,那么就达成我们的目的了:
def log4(time="Now"):
def decorator(func): # 这一层会在定义阶段执行,返回 wrapper会绑定到 fn 上
def wrapper(time)
return func(time)
return wrapper
return decorator
第一层在定义阶段的执行,发现返回了一个 decorator,于是继续执行,并将被修饰函数 func 作为为入参。
这么解释对吗?大错特错。
如果 @log() 是执行 log()的返回值,并用返回值执行(fn),那为什么没有继续执行 fn 的返回值?这不和谐。
这是因为,上面的解释是错的,"@"并不是一个运算符,而是一个语法糖。它是一种省略,而不是运算,
它标注的时候,就是以下的省略而已。
def fn():
pass
fn = log4()(fn)
所以,正确的理解是,它字面上怎么写的,就是把字面上的东西改写成一条额外的语句,然后再执行的。这条语句是绑定东西到变量,你甚至可以在里面写个类。所以,没有所谓的 “编译阶段”,我骗你的。
高级用例
- 如何使用正确的函数名?
- 如何为函数提供额外的上下文?
def tensorboard_logger(log_dir='runs/experiment'):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
writer = SummaryWriter(log_dir) #提供一个 writter 在func.<outter>
kwargs['writer'] = writer
try:
result = func(*args, **kwargs) # 没有定义 writer 却依然可以接受writer
finally:
# 确保 writer 在函数结束时关闭
writer.close()
return result
return wrapper
return decorator
@wraps(func)
给入参函数标注了“我只是一个 wrap” 的属性。这个标注会将 wrapper 的一些metadata更改为和 func 一致。从而更好地表现函数的反射行为。
在这个函数中, 注意 kwargs['writer'] = writer
,它额外地向 func 函数的入参表注入了一个 writer,因此提供了上下文。但这种方式会将调用时的替换掉。
脑洞开一下,装饰器实际上为函数的入参提供了巨大的编辑空间。如下 decorator 可以将函数的入参转换为全部大写。
def uppercase_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 转换位置参数
new_args = [arg.upper() if isinstance(arg, str) else arg for arg in args]
# 转换关键字参数
new_kwargs = {k: v.upper() if isinstance(v, str) else v for k, v in kwargs.items()}
# 调用原始函数
return func(*new_args, **new_kwargs)
return wrapper
另外,try: finally 也给函数提供了资源回收机制。
但是,如果被修饰的函数可能调用一个被修饰的子函数,这种方式就会出问题,因此使用更复杂的装饰器:
def tensorboard_logger(log_dir='runs/experiment'):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 创建 SummaryWriter
if 'writer' not in kwargs:
writer = SummaryWriter(log_dir)
kwargs['writer'] = writer
close_writer = True
else:
writer = kwargs['writer']
close_writer = False
# 执行函数
result = func(*args, **kwargs)
# 如果这个 wrapper 创建了 writer,就关闭它
if close_writer:
writer.close()
return result
return wrapper
return decorator
高级用例4: 类封装器
如果你希望放一个对象在外层作用域里,并在不同的函数中调用,可以用 nonlocal 声明写
def tensorboard_logger(log_dir='runs/experiment'):
def decorator(func): #定义阶段调用
writer = None #定义阶段调用
@wraps(func)
def wrapper(*args, **kwargs): #实际执行函数
nonlocal writer
if writer is None:
writer = SummaryWriter(log_dir)
...
但如果你的目的是让 writer 变量在内部函数中被创建一次,并在多个函数调用中持续使用,更灵活的方案是类封装装饰器。
class TensorboardLogger:
def __init__(self, log_dir='runs/experiment'):
self.writer = None
self.log_dir = log_dir
def __call__(self, func):
def wrapper(*args, **kwargs):
if self.writer is None:
self.writer = SummaryWriter(self.log_dir)
result = func(*args, **kwargs)
return result
return wrapper
使用时,你需要创建一个装饰器实例:
@TensorboardLogger(log_dir='runs/my_experiment')
def train():
...
还记得吗?@只是一个语法糖。
因此等价于
train=TensorboardLogger(log_dir='runs/my_experiment')(train)
装饰后的函数,在定义阶段会执行第一个括号,调用 init 初始化一个 TensorboardLogger 实例,并立即执行实例方法 call(train),它会返回一个等价的 wrapper.
假如装饰了多个函数,那么在init的时候
它不会再进行更多的实例化了,