关于Python的装饰器使用方法(下)

讨论 stubbron
Lv3 见习炼丹师
发布在 Python编程   1555   0
讨论 stubbron   1555   0

    将装饰器定义为类的一部分

    在类中定义一个装饰器是很直接的,但是首先我们需要理清装饰器将以什么方式来应用。具体来说就是以实例方法还是以类方法的形式应用。

    from functools import wraps
    
    
    class FunctionEmbellish(object):
    
        def function_one(self, function):
            """This is the instance decorator for FunctionEmbellish"""
    
            @wraps(function)
            def wrapper(*args, **kwargs):
                print("Tis is FunctionEmbellish instance decorator")
                return function(*args, **kwargs)
    
            return wrapper
    
        @classmethod
        def function_two(cls, function):
            """This is the static method"""
    
            @wraps(function)
            def wrapper(*args, **kwargs):
                print("Tis is FunctionEmbellish static method")
                return function(*args, **kwargs)
    
            return wrapper
    
    
    # 应用展示,你应该要知道,有一个装饰器是来自实例化方法
    embellish = FunctionEmbellish()
    
    
    @embellish.function_one
    def spam():
        ...
    
    
    @FunctionEmbellish.function_two
    def grok():
        ...

    在类中定义装饰器乍看起来可能有些古怪,但是在标准库中也可以找到这样的例子。尤其是,内建的装饰器@property实际上是一个拥有getter()、setter()和deleter()方法的类, 每个方法都可作为一个装饰器。

    在类中定义装饰器有个难理解的地方就是对于额外参数 self 或 cls 的正确使用。 尽管最外层的装饰器函数比如 function_one() 或 function_two() 需要提供一个 self 或 cls 参数, 但是在两个装饰器内部被创建的 wrapper() 函数并不需要包含这个 self 参数。 你唯一需要这个参数是在你确实要访问包装器中这个实例的某些部分的时候。其他情况下都不用去管它。

    另外额外注意!!就是在涉及到继承的时候。 例如,假设你想让在A中定义的装饰器作用在子类B中。你需要像下面这样写:

    class B(A):
        @A.decorator2
        def bar(self):
            pass

    也就是说,装饰器要被定义成类方法并且你必须显式的使用父类名去调用它。 你不能使用 @B.decorator2 ,因为在方法定义时,这个类B还没有被创建。

    将装饰器定义为类

    我们想用装饰器来包装函数,但是希望得到的结果是一个可调用的实例。我们需要装饰器既能在类中工作,也可以在类外部使用。

    要把装饰器定义成类实例,需要确保在类中实现__call__()__get__()方法。例如,下面的代码定义了一个类,可以在另一个函数上添加一个简单的性能分析层:

    import types
    from functools import wraps
    
    class Profiled:
        def __init__(self, func):
            wraps(func)(self)
            self.ncalls = 0
    
        def __call__(self, *args, **kwargs):
            self.ncalls += 1
            return self.__wrapped__(*args, **kwargs)
    
        def __get__(self, instance, cls):
            if instance is None:
                return self
            else:
                return types.MethodType(self, instance)
    
    
    # 应用展示
    @Profiled
    def add(x, y):
        return x + y
    
    
    class Span:
        @Profiled
        def bar(self, x):
            print(self, x)
    
    
    print("--" * 5)
    print(add(2, 3))
    print(add(2, 3))
    print(add.ncalls) # add 调用两次 -> return 2
    
    print("--" * 5)
    s = Span()
    s.bar(2)
    print(s.bar.ncalls)  # bar 调用一次 -> return 1

    把装饰器定义成类通常是简单明了的。 但是,这里有一些相当微妙的细节值得做进一步的解释,尤其是计划将装饰器应用在实例的方法上时。 首先,这里对functools.wraps()函数的使用和在普通装饰器中的目的一样——意在从被包装的函数中拷贝重要的元数据到可调用实例中。 其次,解决方案中所展示的__get__()方法常常容易被忽视。如果省略掉__get__()并保留其他所有的代码,会发现当尝试调用被装饰的实例方法时会出现怪异的行为。 例如:

    >>> s = Span()
    >>> s.bar(3)
    Traceback (most recent call last):
    ...
    TypeError: spam() missing 1 required positional argument: 'x'

    出错的原因在于每当函数实现的方法需要在类中进行查询时,作为描述符协议(descriptor protocol)的一部分,它们的__get__()方法都会被调用。在这种情况下,__get__()的目的是用来创建一个绑定方法对象(最终会给方法提供self参数)。 下面的例子说明了其中的机理:

    >>> s = Span()
    >>> def grok(self, x):
    ...     pass
    ...
    >>> grok.__get__(s, Span)
    <bound method Span.grok of <__main__.Span object at 0x100671e90>>
    >>>

    __get__()方法在这里确保了绑定方法对象会恰当地创建出来。type. MethodType()手动创建了一个绑定方法在这里使用。绑定方法只会在使用到实例的时候才创建。如果在类中访问该方法,__get__()instance参数就设为None,直接返回Profiled实例本身。这样就使得获取实例的ncalls属性成为可能。 如果想在某些方面避免这种混乱,可以考虑装饰器的替代方案,即(可自定义属性的装饰器)中描述过的利用闭包和nonlocal变量。示例如下

    def profiled(func):
        ncalls = 0
        @wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal ncalls
            ncalls += 1
            return func(*args, **kwargs)
        wrapper.ncalls = lambda: ncalls
        return wrapper
    
    # Example
    @profiled
    def add(x, y):
        return x + y

    为类和静态方法提供装饰器

    将装饰器作用到类和静态方法上是简单而直接的,但是要保证装饰器在应用的时候需要放在@classmethod@staticmethod之前。

    import time
    from functools import wraps
    
    def timethis(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time.time()
            r = func(*args, **kwargs)
            end = time.time()
            print(end - start)
            return r
    
        return wrapper
    
    class Spam:
        @timethis
        def instance_method(self, n):
            print(self, n)
            while n > 0:
                n -= 1
    
        @classmethod
        @timethis
        def class_method(cls, n):
            print(cls, n)
            while n > 0:
                n -= 1
    
        @staticmethod
        @timethis
        def static_method(n):
            print(n)
            while n > 0:
                n -= 1

    如果装饰器的顺序搞错了,那么将得到错误提示。问题在于@classmethod@staticmethod并不会实际创建可直接调用的对象。相反,它们创建的是特殊的描述符对象。因此,如果尝试在另一个装饰器中像函数那样使用它们,装饰器就会崩溃。确保这些装饰器出现在@classmethod@staticmethod之前就能解决这个问题。

    装饰器为被包装函数增加参数

    inspect.signature 这个函数允许我们从一个可调用对象中提取出参数签名信息。同时这个模块可以给我们创建函数的签名信息。示例如下:

    from inspect import Signature, Parameter
    
    parms = [
        Parameter("x",Parameter.POSITIONAL_OR_KEYWORD),
        Parameter("y",Parameter.POSITIONAL_OR_KEYWORD, default=5),
    ]
    s = Signature(parms)
    print(s)
    from functools import wraps
    import inspect
    
    def optional_debug(func):
        """"""
        # 检查fun参数,是否带debug参数
        arg = inspect.signature(func).parameters.get("debug", None)
        if arg is not None :
            raise TypeError("debug argument already defined")
    
        @wraps(func)
        def wrapper(*args, debug=False, **kwargs):
            if debug: print("Calling", func.__name__)
            return func(*args, **kwargs)
    
        # 管理函数签名
        sig = inspect.signature(func)
        params = list(sig.parameters.values())
        params.append(inspect.Parameter(
            "debug",
            inspect.Parameter.KEYWORD_ONLY,
            default=False
        ))
        # 把原有的函数签名,附加到wrapper上
        wrapper.__signature__ = sig.replace(parameters=params)
        return wrapper
    
    
    @optional_debug
    def spam(a, b, c):
        return a+b+c
    
    print(inspect.signature(spam))
    print(spam(1, 2, 3))
    print(spam(1, 2, 3, debug=True))

    用装饰器给类定义打补丁

    你想通过反省或者重写类定义的某部分来修改它的行为,但是你又不希望使用继承或元类的方式。

    这种情况可能是类装饰器最好的使用场景了。例如,下面是一个重写了特殊方法 __getattribute__ 的类装饰器, 可以打印日志:

    # -*- coding: utf-8 -*-
    # !/usr/bin/python3
    
    def log_getattribute(cls):
        orig_getattribute = cls.__getattribute__
    
        # Make a new definition
        def new_getattribute(self, name):
            print(f"getting: {name}")
            return orig_getattribute(self, name)
    
        # Attach to the class and return
        cls.__getattribute__ = new_getattribute
        return cls
    
    # Example use
    @log_getattribute
    class test:
        def __init__(self, x):
            self.x = x
    
        def spam(self):
            pass
    
    T = test(3)
    print(T.x)
    T.spam()

    或者你也可以使用继承,但是为了去理解它,你就必须知道方法调用顺序、super() 以及其它的继承知识。 某种程度上来讲,类装饰器方案就显得更加直观,并且它不会引入新的继承体系。它的运行速度也更快一些, 因为他并不依赖 super() 函数。

    如果你系想在一个类上面使用多个类装饰器,那么就需要注意下顺序问题。 例如,一个装饰器A会将其装饰的方法完整替换成另一种实现, 而另一个装饰器B只是简单的在其装饰的方法中添加点额外逻辑。 那么这时候装饰器A就需要放在装饰器B的前面。

    版权声明:作者保留权利,不代表意本站立场。如需转载请联系本站以及作者。

    参与讨论

    回复《 关于Python的装饰器使用方法(下)

    EditorJs 编辑器

    沙发,很寂寞~
    反馈
    to-top--btn