python 装饰器如何理解不费劲?

装饰器一层套一层时,解读起来尤其困难。关键在于理解程序执行的流程;对于带参数的装饰器,关键是发现使用上 @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() ,此时 fnlog.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)

所以,正确的理解是,它字面上怎么写的,就是把字面上的东西改写成一条额外的语句,然后再执行的。这条语句是绑定东西到变量,你甚至可以在里面写个类。所以,没有所谓的 “编译阶段”,我骗你的。

高级用例

  1. 如何使用正确的函数名?
  2. 如何为函数提供额外的上下文?
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的时候

它不会再进行更多的实例化了,

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值