• python装饰器3:进阶


    函数装饰器装饰方法

    函数装饰器装饰普通函数已经很容易理解了:

    @decorator
    def func():...
    
    #等价于
    def func():...
    func = decorator(func)
    

    如果装饰器是带参装饰器,那么等价的形式大概是这样的(和装饰器的编码有关,但最普遍的编码形式如下):

    @decorator(x, y, z)
    def func():...
    
    # 等价于
    def func():...
    func = decorator(x, y, z)(func)
    

    这样的函数装饰器也可以去装饰类中的方法。看下面的方法装饰形式:

    class cls:
        @decorator
        def method(self,arg1,arg2):
            ...
    

    它等价于:

    class cls:
        def method(self,arg1,arg2):
            ...
        method = decorator(method)
    

    在decorator的编码中,仍然像普通的函数装饰器一样编写即可。例如:

    def decorator(F):
        @wraps(F)
        def wrapper(*args, **kwargs):
            ...    # args[0] = self_instance
                   # args[1]开始才是手动传给method的参数
        return wrapper
    

    但必须要考虑到method的第一个参数self,所以包装器wrapper()的第一个参数也是self。

    如此一来,函数装饰器既可以装饰函数,又可以装饰方法。

    下面是一个示例:

    from functools import wraps
    
    def decorator(F):
        @wraps(F)
        def wrapper(*args, **kwargs):
            result = F(*args, **kwargs)
            print(args)
            return result
        return wrapper
    
    @decorator
    def func(x,y):
        return x + y
    
    print(func(3, 4))
    
    print("-" * 30)
    
    class cls:
        @decorator
        def method(self, x, y):
            return x + y
    
    c = cls()
    print(c.method(3, 4))
    

    输出结果:

    (3, 4)
    7
    ------------------------------
    (<__main__.cls object at 0x01DF1C50>, 3, 4)
    7
    

    让类称为装饰器

    不仅函数可以作为装饰器,类也可以作为装饰器去装饰其它对象。

    如何让类作为装饰器

    要让类作为装饰器,先看装饰的形式:

    class Decorator:
        ...
    
    @Decorator
    def func():
        ...
    
    func(arg1, arg2)
    

    如果成功装饰,那么它等价于:

    def func(): ...
    func = Decorator(func)
    
    func(arg1, arg2)
    

    这和函数装饰器看上去是一样的,但区别在于Decorator这里是一个类,而不是函数,且Decorator(func)表示的是创建一个Decorator类的实例对象,所以这里赋值符号左边的func是一个对象。所有后面的func(arg1, arg2)是调用对象,而不是调用函数。

    要让实例对象成为可调用对象,它必须实现__call__方法,所以应该在Decorator类中定义一个__call__。而且每次调用实例对象的时候,都是在调用__call__,这里的__call__对等于函数装饰器中的包装器wrapper,所以它的参数和逻辑应当和wrapper一样。

    如下:

    class Decorator():
        def __call__(self, *args, **kwargs):
            ...
    

    再看func = Decorator(func),func是Decorator类创建实例的参数,所以Decorator类还必须实现一个__init__方法,接受func作为参数:

    class Decorator:
        def __init__(self, func):
            ...
        def __call__(self, *args, **kwargs):
            ...
    

    元数据问题

    这样的装饰器已经能正常工作了,但是会丢失func的元数据信息。所以,必须使用functools的wraps()保留func的元数据:

    from functools import wraps
    
    class Decorator:
        def __init__(self, func):
            wraps(func)(self)
            ...
        def __call__(self, *args, **kwargs):
            ...
    

    为什么是wraps(func)(self)?这里显然不能@wraps(func)的方式装饰包装器,所以只能使用wraps()的原始函数形式。在wraps()装饰函数包装器wrapper的时候,@wraps(func)等价于wrapper = wraps(func)(wrapper),所以这里wraps(func)(self)的作用也是很明显的:保留func的元数据,并装饰self。被装饰的self是什么?是Decorator的实例对象,因为Decorator类实现了__call__,所以self是可调用的,所以这里的self类似于函数装饰器返回的wrapper函数(实际上self是Decorator(func)返回的各个实例对象)。

    类作为装饰器的参数问题

    虽然self是Decorator的可调用实例对象,但是上面的代码中self并不具有func属性,也就是说无法从self去调用func()函数,这似乎使得整个过程都崩塌了:废了老大的劲去解决各种装饰器上的问题,结果却不能调用被装饰的函数。

    有两种方式可以解决这个问题:

    1. __init__中使用self.func = func保留func对象作为装饰器的一个属性
    2. 在使用wraps()后直接在包装器__call__中使用__wrapped__调用原始func函数

    这两种方式其实是等价的,因为self.func__wrapped__都指向原始的函数。

    def __init__(self,func):
        wraps(func)(self)
        self.func = func
    def __call__(self, *args, **kwargs):
        result = self.func(*args, **kwargs)
    
    #-------------------------------
    
    def __init__(self, func):
        wraps(func)(self)
    def __call__(self, *args, **kwargs):
        result = self.__wrapped__(*args, **kwargs)
    

    但这两种方式都有缺陷,缺陷在于装饰类中方法时。(注:在装饰普通函数、类方法的时候,上面的方式不会出错)

    class cls:
        @decorator
        def method(self, x, y):...
    

    因为self.func__wrapped__装饰cls中的方法时指向的都是cls的类变量(只不过这个属性是装饰器类decorator的实例对象而已),作为类变量,它无法保存cls的实例对象,也就是说method(self, x, y)的self对装饰器是不可见的。

    用一个示例解释更容易:

    import types
    from functools import wraps
    
    # 作为装饰器的类
    class decorator:
        def __init__(self,func):
            self.func = func
        def __call__(self, *args, **kwargs):
            print("(1): ",self)      # (1)
            print("(2): ",self.func) # (2)
            print("(3): ",args)      # (3)
            return self.func(*args, **kwargs)
    
    class cls:
        @decorator
        def method(self, x, y):
            return x + y
    
    c = cls()
    print("(4): ",c.method)      # (4)
    print(c.method(3, 4))
    

    输出结果:

    (4):  <__main__.decorator object at 0x03261630>
    (1):  <__main__.decorator object at 0x03261630>
    (2):  <function cls.method at 0x032C2738>
    (3):  (3, 4)
    Traceback (most recent call last):  File "g:/pycode/scope.py", line 21, in <module>
        print(c.method(3, 4))
      File "g:/pycode/scope.py", line 12, in __call__    return self.func(*args, **kwargs)
    TypeError: method() missing 1 required positionalargument: 'y'
    

    注意观察上面__call__中输出的几个对象:

    • self对应的是decorator的实例对象method,而非cls的实例对象c,看输出结果的前两行即可知
    • self.func指向的是原始方法method,它是类变量,是类方法(函数),是装饰器赋予它作为函数的。也就是说,self.func指向的不是对象方法,而是类方法,类方法不会自动传递实例对象
    • args中保存的参数列表是(3, 4),但是cls.method中多了一个self位置参数,使得3赋值给了self,4被赋值给了x,y成了多余的,所以最后报错需要位置参数y。

    如果将上面的method()的定义修改一下,把self去掉,将会正确执行:

    class cls:
        @decorator
        def method(x, y):
            return x + y
    

    执行结果:

    (4):  <__main__.decorator object at 0x03151630>
    (1):  <__main__.decorator object at 0x03151630>
    (2):  <function cls.method at 0x031B2738>
    (3):  (3, 4)
    7
    

    因此参数问题必须解决。解决方案是进行判断:如果是通过实例对象触发的方法调用(即c.method()),就将外部函数通过types.MethodType()链接到这个实例对象中,否则就返回原始self(因为它指向被装饰的原始对象)。

    这需要借助描述符来实现,关于这一段的解释,我觉得直接看代码自行脑部更佳。

    class decorator:
        def __init__(self,func):
            wraps(func)(self)
            self.func = func
        def __call__(self, *args, **kwargs):
            return self.func(*args, **kwargs)
        def __get__(self, instance, owner):
            # 如果不是通过对象来调用的
            if instance is None:
                return self
            else:
                return types.MethodType(self, instance)
    
    class cls:
        @decorator
        def method(self, x, y):
            return x + y
    
    c = cls()
    print(c.method(3, 4))  # 调用__get__后调用__call__
    

    对于__wrapped__也一样可行:

    class decorator():
        def __init__(self, func):
            wraps(func)(self)
        def __call__(self, *args, **kwargs):
            return self.__wrapped__(*args, **kwargs)
        def __get__(self, instance, owner):
            if instance is None:
                return self
            else:
                return types.MethodType(self, instance)
    

    装饰时是否带参数

    如果要让作为装饰器的类在装饰时带参数,就像函数装饰器带参一样decorator(x,y,z)(func),可以将参数定义在__init__上进行处理,然后在__call__中封装一层。

    class Decorator:
        def __init__(self, *args, **kwargs):
            ... do something with args ...
        def __call__(self, func):
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)
            return wrapper
    

    和函数装饰器一样,如果想要达到下面这种既能无参装饰,又能带参装饰:

    @Decorator         # 无参装饰
    @Decorator(x,y,z)  # 带参装饰
    @Decorator()       # 带参装饰,只不过没给参数
    

    可以直接在__init__上进行参数有无的判断:

    import types
    from functools import wraps, partial
    
    class Decorator:
        def __init__(self, func=None, arg1=1, arg2=2, arg3=3):
            # 带参装饰器
            if func is None:
                self.func = partial(Decorator, arg1=arg1, arg2=arg2, arg3=arg3)
            else:       # 无参装饰器
                wraps(func)(self)
                self.func = func
    
        def __call__(self, *args, **kwargs):
            return self.func(*args, **kwargs)
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
            else:
                return types.MethodType(self, instance)
    

    这样的限制是装饰器如果带参数时,必须使用keyword方式指定参数。例如:

    # 带参装饰普通函数,使用keywords参数方式
    @Decorator(arg1=1, arg2=3, arg3=5)
    def func(x, y):
        return x + y
    
    print(func(11, 22))
    
    print('-' * 30)
    
    # 无参装饰普通函数
    @Decorator
    def func1(x, y):
        return x + y
    
    print(func1(111, 22))
    
    print('-' * 30)
    
    # 无参装饰方法
    class cls:
        @Decorator
        def method(self, x, y):
            return x + y
    
    c = cls()
    print(c.method(3, 4))
    
    print('-' * 30)
    
    # 带参装饰方法
    class cls1:
        @Decorator(arg1=1, arg2=3, arg3=5)
        def method(self, x, y):
            return x + y
    
    cc = cls1()
    print(cc.method(3, 4))
    

    总结:类作为装饰器的通用格式

    如果不考虑装饰时是否带参数的问题,根据上面的一大堆分析,类作为装饰器时的通用代码格式如下:

    import types
    from functools import wraps
    
    class Decorator:
        def __init__(self, func):
            wraps(func)(self)
            # self.func = func
    
        def __call__(self, *args, **kwargs):
            # return self.func(*args, **kwargs)
            # return self.__wrapped__(*args, **kwargs)
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
            else:
                return types.MethodType(self, instance)
    

    至于选择self.func的方式,还是self.__wrapped__的方式,随意。

    如果需要考虑装饰时带参数问题,那么参考上一小节内容。

    选择类谁作为装饰器?

    函数可以作为装饰器,类也可以作为装饰器。它们也都能处理处理各种需求,但是类作为装饰器的时候解释了好大一堆,非常麻烦。所以,如果可以的话,选择函数作为装饰器一般会是最佳方案。

  • 相关阅读:
    “error LNK1169: 找到一个或多个多重定义的符号”的解决方法(转载)
    std::ostringstream 转std::string
    【转载】红外感应模块+蜂鸣器实现简易报警
    分割字符串使用空格
    优秀程序员的 18 大法则【转载】
    解决win10 iot VS编程出现的无法引用错误
    [转载] 程序员如何成功追到女神?
    win8/win10/win2012r2 存储池 冗余分析
    [转载][NAS] 使用win8的“存储池”功能~
    mySQL中删除unique key的语法 (删除某个字段的唯一性)
  • 原文地址:https://www.cnblogs.com/f-ck-need-u/p/10204617.html
Copyright © 2020-2023  润新知