python装饰器的几个进阶用法

在之前的博客《一篇文章汇总Python装饰器全知识图谱(使用场景,基本用法,参数传递,闭包操作,类装饰器和AOP)》中,装饰器的基本使用都涵盖到了,不过继续深入还是有一些细枝末节可以继续讨论的,下面让我们用实例来看看装饰的几个进阶使用。

类装饰器

类装饰器中的__init__方法相当于装饰器的外层函数,而__call__方法相当于装饰器的内层函数。创建一个装饰器使函数延迟2秒执行

class DelayFunc:
    def __init__(self,func):
        self.__func = func

    def __call__(self, *args, **kwargs):
        print('wait for 2 seconds...')
        time.sleep(2)
        self.__func(*args, **kwargs)
        
@DelayFunc
def test(a, b):
    print(a + b)


if __name__ == '__main__':
    test(1, 2)

执行之后的结果如下

wait for 2 seconds...
3

带参数的类装饰器

对上面的装饰器做一个改进,想要用户在使用装饰器的时候去任意指定延迟的时间,这就需要带参数的类装饰器了。

在使用函数装饰器时,是在原装饰器外添加了一层函数,专门去接受装饰器的参数。如果使用类装饰器,也可以依法炮制。

下面的代码就是在__call__方法里面有返回了一个新的函数,同时用类的构造函数接收参数,而__call__方法接受原函数

class DelayFunc:
    def __init__(self, seconds):
        self.seconds = seconds

    def __call__(self, func):
        self.func=func
        def wrapper(*args,**kwargs):
            print('Wait for {} seconds'.format(self.seconds))
            time.sleep(self.seconds)
            self.func(*args,**kwargs)
        return wrapper
        
class TestSum:
    @DelayFunc(4)
    def sum(self, a, b):
        print(a + b)
        
if __name__ == '__main__':
    testSum = TestSum()
    testSum.sum(2, 3)

打印结果如下

Wait for 4 seconds
5

当然,方法不止一种,下面再带来一种方式。

首先,修改下类装饰器,给构造器添加一个参数seconds用来设定延迟的时间

class DelayFunc:
    def __init__(self, seconds, func):
        self.__func = func
        self.seconds = seconds

    def __call__(self, *args, **kwargs):
        print('Delay for {} seconds'.format(self.seconds))
        time.sleep(self.seconds)
        self.__func(*args, **kwargs)

此时构造函数因为不再是只接受函数名做为参数,不能看做装饰器的外层函数。于是我们单独再创建一个装饰器,装饰器内部的函数直接返回类的一个实例,如下

def delay(seconds):
    def delayFunc(func):
        return DelayFunc(seconds,func)
    return delayFunc

这里也可以用偏函数delayFunc = functools.partial(DelayFunc, seconds),就避免了去定义一个新的内层函数。偏函数会根据第一个参数函数名携带上指定的固定参数返回一个新的函数,该函数每次执行都会携带该固定参数,自己添加的参数会排在固定参数的后面

于是原先的类装饰器变成了新的函数装饰器的内层函数的一部分,这说明可以用装饰器去嵌套装饰器以添加额外功能,只不过内层的装饰器必须要执行进行返回。

用新的函数装饰器去验证下

@delay(5)
def test(a, b):
    print(a + b)

if __name__ == '__main__':
    test(1, 2)

打印结果为

Delay for 5 seconds
3

也许有朋友会有疑问,完全可以用函数装饰器去实现的情况下,何必大费周章用类装饰器呢。其中一个原因,就是类中的属性比闭包函数的变量保持更容易理解,使用起来更直观不易出错。还有一个原因,就是下面即将要演示的,为函数添加接口提供可能性。

函数扩充接口

既然通过类装饰器,将函数可以等价于一个类的实例,那么在类中如果定义了类方法,岂不是可以直接拿来被使用装饰器的函数调用。

还是使用上面的原始类装饰器,只不过这次多定义了一个方法instant_call用来不延迟直接执行被装饰的函数

class DelayFunc:
    def __init__(self, func):
        self.__func = func

    def __call__(self, *args, **kwargs):
        print('wait for 2 seconds...')
        time.sleep(2)
        self.__func(*args, **kwargs)

    def instant_call(self, *args, **kwargs):
        print('no need to wait')
        self.__func(*args, **kwargs)

然后将函数改为类装饰器

@DelayFunc
def test(a, b):
    print(a + b)

验证下

if __name__ == '__main__':
    test(1, 2)
    test.instant_call(2,3)

打印如下

wait for 2 seconds...
3
no need to wait
5

其中第二次就没有等待,直接打印了出来。

所以可以看出,类装饰器除了可以有一个默认装饰效果,还可以对函数进行接口的扩充,实现更多的功能。

类装饰器使用wraps恢复元信息

在函数装饰器里面,大家都知道被装饰的函数元信息会丢失,并且可以通过functools.wraps来恢复。但是当使用类装饰器时,不可以直接像函数装饰器那样,在内层函数加上@wraps就能恢复函数的元信息。

既然wraps也是一个装饰器,就得从装饰器的本质说起了。

本质上@只是一个语法糖,背后就是函数的一次调用和重命名,例如下面的格式

@wraps(func)
def wrapper():
	pass

和下面的表达式是等价的

wrapper = wraps(func)(wrapper)

回到类装饰器,我们也可以用类似的方法来恢复元数据。只不过要注意,此时的wrapper变成了装饰器类的实例对象,而不再是一个简单的函数

self = wraps(func)(self)

像下面这样

class DelayFunc:
    def __init__(self, func):
        self = functools.wraps(func)(self)  # 完成元数据恢复
        self.func=func

    def __call__(self, *args, **kwargs):
        print('wait for 2 seconds...')
        time.sleep(2)
        # self.__wrapped__(*args, **kwargs)  # 可以像下面那样,也可以直接用__wrapped__指向原函数
        self.func(*args, **kwargs)
        
@DelayFunc
def test(a, b):
    print(a + b)
    
if __name__ == '__main__':
    print(test.__name__)
    test(1, 2)

最后的打印结果为

test
wait for 2 seconds...
3

可见元数据已经恢复。

除了用已知的self.func=func去调用原函数,还可以通过wraps装饰器返回的对象的__wrapped__方法来引用原函数,如上面的注释行。

函数装饰器装饰类方法

装饰器既然可以装饰普通函数,那就当然也可以装饰类方法。我们先来看看函数装饰器,然后再看类装饰器,因为两者所面临的问题不太一样。

一般情况下,如果只是通过*args**kwargs在装饰器内层函数中传递参数给原函数就还好。但是一旦装饰器需要去获取具体的参数做处理,就要特别小心,因为类方法传递的第一个参数是self

def TestYourPara(func):
    @functools.wraps(func)
    def wrapper(para, *args, **kwargs):
        print(inspect.ismethod(func))
        print(isinstance(func,types.MethodType))
        print(para)
        print(*args)
        print(**kwargs)
        func(para, *args, **kwargs)
    return wrapper

class TestSum:
    @TestYourPara
    def sum(self, a, b):
        print(a + b)
        
if __name__ == '__main__':
    testSum = TestSum()
    testSum.sum(2, 3)

上面这段代码的执行结果如下

False
False
<__main__.TestSum object at 0x7f84ba196b00>
2 3

5

可以看出,打印出来的第一个参数para是一个类实例,而不是我们以为的2。这时候如果我们想当然地对para进行处理就会报错。

原本我以为可以通过判断函数是不是类方法来进行一些处理,结果两个判断类方法的函数在这里都返回False

所以只能是提醒各位,如果一定要在装饰器里面处理参数,可以尽量使用关键字参数,便于查找到

类装饰器装饰类方法

看完了函数装饰器,再来看看类装饰器。

相信在使用函数装饰器的时候,像下面这样的操作不会有任何疑问

def DelayFunc(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('wait for 2 seconds...')
        time.sleep(2)
        func(*args, **kwargs)
    return wrapper

class TestSum:
    @DelayFunc
    def sum(self,a,b):
        print(a+b)
        
if __name__ == '__main__':
    testSum=TestSum()
    testSum.sum(2,3)

顺利打印下面的结果

wait for 2 seconds...
5

一切看起来很美。

但是如果将函数装饰器改为类装饰器,问题就来了

class DelayFunc:
    def __init__(self, func):
        self = functools.wraps(func)(self)
        self.func=func

    def __call__(self, *args, **kwargs):
        print('wait for 2 seconds...')
        time.sleep(2)
        # self.__wrapped__(*args, **kwargs)
        self.func(*args, **kwargs)
        
class TestSum:
    @DelayFunc
    def sum(self,a,b):
        print(a+b)
        
if __name__ == '__main__':
    testSum=TestSum()
    testSum.sum(2,3)

此时做同样的测试就会有如下报错

wait for 2 seconds...
Traceback (most recent call last):
  File "/home/fuhx/python_projects/datanalgo/linkedlist/partial_01.py", line 73, in <module>
    testSum.sum(2,3)
  File "/home/fuhx/python_projects/datanalgo/linkedlist/partial_01.py", line 32, in __call__
    self.func(*args, **kwargs)
TypeError: sum() missing 1 required positional argument: 'b'

提示我们少传递了一个参数。这到底是咋回事呢?

其实原因我在之前讲描述符的博客《python中@property以及描述符descriptor详解》中有提到了,根本原因就在于python中的函数也是一种描述符,其定义如下

class Function(object):
    . . .
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return types.MethodType(self, obj)

当作为类方法时,一旦被类实例调用,就会执行这里的types.MethodType(self, obj)将函数绑定到该实例,好处就是可以自动去处理self的传递。

前面函数装饰器的时候,被装饰完的函数还是一个函数对象,同样是一个描述符,所以不会有问题。而被类装饰器装饰完的函数变成了一个类实例,不再是一个函数对象,失去了描述符中的绑定功能,所以出了问题。

要解决也很简单,将类装饰器也变成一个具有绑定功能的描述符类即可,所需要的就是添加一个一模一样的__get__方法,如下

class DelayFunc:
    def __init__(self, func):
        self = functools.wraps(func)(self)
        self.func = func

    def __call__(self, *args, **kwargs):
        print('wait for 2 seconds...')
        time.sleep(2)
        # self.__wrapped__(*args, **kwargs)
        self.func(*args, **kwargs)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

再次执行测试代码成功打印如下内容

wait for 2 seconds...
5

所以以后再定义类装饰器的时候,除了必须的__init__方法和__call__方法,还得带上__get__方法,固定格式如下

class DelayFunc:
    def __init__(self, func):
        self = functools.wraps(func)(self)
        self.func = func

    def __call__(self, *args, **kwargs):
        pass
        self.func(*args, **kwargs)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

总结

总结下知识点

  • 类装饰器的使用在某些场景下很强大,例如给函数提供额外接口
  • 但是要注意带参数的类装饰器要进行一次装饰器的嵌套
  • 同时恢复元信息也不能用常规装饰器方式,而必须用函数调用
  • 如果是对类方法进行装饰记得定义__get__方法绑定实例
  • 综合起来,类装饰器使用起来还有有点麻烦,大多数时候还是尽量考虑函数装饰器
  • 函数装饰器如果是装饰类方法,要注意self参数的处理

最后,经过上述的讨论,定义一个类装饰器的通用格式如下

class DelayFunc:
    def __init__(self, func):
        self = functools.wraps(func)(self)
        self.func = func

    def __call__(self, *args, **kwargs):
        pass
        self.func(*args, **kwargs)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值