62、Python之函数高级:装饰器导致函数元数据丢失?三种方法搞定

引言

前面我们通过几篇文章介绍了关于高阶函数中装饰器的内容,我们已经能够实现对函数的动态增强,在遵从开闭原则的基础上,动态提高代码的可复用性。如果对装饰器的基础不太了解,可以回看前面几篇文章。

装饰器的引入带了极大的便利,但是,有一个小小的缺陷,可能对函数的使用者带来不便,也就是原函数的元数据会丢失,毕竟函数对象已经发生了变更。

此外,如果函数调用方写了一些依赖函数元数据的操作,就不是小小的不便了,而是可能是大大的bug了。

本文的主要内容有:

1、函数对象的主要元数据

2、装饰器导致的元数据丢失

3、修复函数元数据问题的3种方法

函数对象的主要元数据

关于函数对象的元数据的内容,之前也有介绍过,这里通过代码,简单回顾一下。

def work(name='张三'):
    """这是一个普通的业务函数"""
    print(f'{name}不能一直闲着,总得做点啥')


print(work.__name__)
print(work.__module__)
print(work.__doc__)
print(work.__dict__)
print(work.__defaults__)

执行结果:

37f04028a208b8a7c9cf37faf723ee64.jpeg

通常情况下,更常用的可能是__doc__,因为我们要使用一个函数,还是需要看一下函数的docstring(如果有的话……)。

这里,就不再展开了,可以稍作了解就可以了。

装饰器导致的元数据丢失

其实,使用装饰器会导致函数的元数据丢失,应该比较好理解。

毕竟,虽然函数名还是同样的名字,但是与这个名称绑定的函数对象,已经是另一个新的对象了。

以代码为例:

import time


# 定义一个装饰器函数
def wait(func):
    def wrap(*args, **kwargs):
        time.sleep(3)
        return func(*args, **kwargs)

    return wrap


# 为了看清楚装饰器前后的变化,这里使用@wait语法糖
def work(name='张三'):
    """这是一个普通的业务函数"""
    print(f'{name}不能一直闲着,总得做点啥')


print('装饰前:')
print(work)
print(work.__name__)
print(work.__module__)
print(work.__doc__)
print(work.__dict__)
print(work.__defaults__)
print('=' * 20)
# 进行装饰
work = wait(work)
print('装饰后:')
print(work)
print(work.__name__)
print(work.__module__)
print(work.__doc__)
print(work.__dict__)
print(work.__defaults__)

执行结果:

aa055d74322a693111e05d8468afeb0f.jpeg

执行装饰的那一行代码,或者对应的@装饰器名的语法糖,就会导致函数对象发生变更。

这个问题的更加准确的描述是,并不是原函数对象的元数据丢失了,只是函数对象本身变了,通过新的函数对象不可能获取到原函数对象的元数据。

所以,从研发的视角来看,这个不应该叫bug,应该是新需求:

怎么把装饰器装饰的原函数的元数据添加到新函数对象中

修复函数元数据问题的3种方法

其实这个需求比较简单,最简单的方式是,我们直接把对应的元数据原样存下来就好了,所以,这就有了第一种简单粗暴的方法。

方法1:

import time


# 定义一个装饰器函数
def wait(func):
    def wrap(*args, **kwargs):
        time.sleep(3)
        return func(*args, **kwargs)

    wrap.__doc__ = func.__doc__
    wrap.__name__ = func.__name__
    wrap.__module__ = func.__module__
    wrap.__defaults__ = func.__defaults__
    wrap.__dict__ = func.__dict__

    return wrap


# 为了看清楚装饰器前后的变化,这里使用@wait语法糖
def work(name='张三'):
    """这是一个普通的业务函数"""
    print(f'{name}不能一直闲着,总得做点啥')


print('装饰前:')
print(work)
print(work.__name__)
print(work.__module__)
print(work.__doc__)
print(work.__dict__)
print(work.__defaults__)
print('=' * 20)
# 进行装饰
work = wait(work)
print('装饰后:')
print(work)
print(work.__name__)
print(work.__module__)
print(work.__doc__)
print(work.__dict__)
print(work.__defaults__)

执行结果:

bb395321ccf7f95bb6edba47321a57b0.jpeg

简单粗暴,想保留哪个元数据,就保留哪个。

但是,有点麻烦,这种方法意味着,我们每次创建一个装饰器,都要写这么几行没有营养的重复代码……

基于尽量能够偷懒的原则,有了后面两种方法。

方法2:

我们开始使用functools模块中的update_wrapper()来来完成这个需求。

首先,我们来看下update_wrapper()的定义:

df9bae506cb9ac1efa58d6c74f6aea9c.jpeg

从定义中可以看到,本质上还是进行遍历,从原函数对象进行getattr(),然后对装饰器内部函数进行setattr()。

接下来,我们通过update_wrapper()函数来实现:

import time
from functools import update_wrapper


# 定义一个装饰器函数
def wait(func):
    def wrap(*args, **kwargs):
        time.sleep(3)
        return func(*args, **kwargs)

    update_wrapper(wrap, func)

    return wrap


# 为了看清楚装饰器前后的变化,这里使用@wait语法糖
def work(name='张三'):
    """这是一个普通的业务函数"""
    print(f'{name}不能一直闲着,总得做点啥')


print('装饰前:')
print(work)
print(work.__name__)
print(work.__module__)
print(work.__doc__)
print(work.__dict__)
print(work.__defaults__)
print('=' * 20)
# 进行装饰
work = wait(work)
print('装饰后:')
print(work)
print(work.__name__)
print(work.__module__)
print(work.__doc__)
print(work.__dict__)
print(work.__defaults__)

修改的代码只有这里:

ee30572b9acbb74d044756747b12ce03.jpeg

执行结果也是相同的,这里就不贴图了。

方法3:

同样是通过functools模块,但是,不再是调用函数。我们经常说,“只有魔法才能打败魔法”,装饰器引发的问题,我们还是通过装饰器来解决吧。

直接看代码:

import time
from functools import wraps


# 定义一个装饰器函数
def wait(func):
    @wraps(func)
    def wrap(*args, **kwargs):
        time.sleep(3)
        return func(*args, **kwargs)

    return wrap


# 为了看清楚装饰器前后的变化,这里使用@wait语法糖
def work(name='张三'):
    """这是一个普通的业务函数"""
    print(f'{name}不能一直闲着,总得做点啥')


print('装饰前:')
print(work)
print(work.__name__)
print(work.__module__)
print(work.__doc__)
print(work.__dict__)
print(work.__defaults__)
print('=' * 20)
# 进行装饰
work = wait(work)
print('装饰后:')
print(work)
print(work.__name__)
print(work.__module__)
print(work.__doc__)
print(work.__dict__)
print(work.__defaults__)

执行结果:

68ae81a70b814614ac23d811684277ca.jpeg

需要说明的是,之所以__default__属性没有同步保存,是由于不管是update_wrapper()函数还是wraps()函数,都有一个默认的参数:assigned:

76bcaaf2dbd933d9f6414198b81bfa21.jpeg

如果有需要保存的其他元数据,只需要自行指定这个参数即可。

总结

本文简单回顾了函数对象中主要的元数据(本质上就是对象中的属性),然后正视了装饰器的引入所导致的函数对象元数据丢失同步的问题。最后,通过3个方法实现了装饰器内部函数与原始函数的元数据同步的需求。

感谢您的拨冗阅读,希望对您有所帮助!

283203c68b884cc7563864e1f4a4f49b.jpeg

  • 9
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南宫理的日知录

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值