Python:说明白装饰器

1,002 阅读10分钟

摘要

Python 在语言特性层面提供 Decorator 模式实现:不改变现有对象的情况下,动态地扩展其额外功能,还为此定义了 @ 语法糖。灵活运用装饰器可以带来很多便利;但是 Python 的装饰器语法虽然简单,想要不被各路框架代码秀一脸,还需深入了解 Python 的闭包特性和对象模型,以及各型装饰器的实现原理和局限。

前提

本文所有资料与实验均基于 python 3.11.6

❯ python --version
Python 3.11.6

使用类作为装饰器的部分,可能会涉及 Python 元类(MetaClass)并需要了解 Python 对象创建的相关知识,建议先行了解基本知识后再行阅读。

正文

一切都是对象

Python 语义中,类、函数、对象实例等,一切都是对象;函数是可以调用(Callable)的对象,类是可以创建对象的对象。以 bar = Foo() 的创建为例:


class FooMeta(type):
  def __call__(self, *args: Any, **kwds: Any) -> Any:
    print("__call__")
    ret_object = super().__call__()
    print(f"FooMeta.__call__ created: {str(type(ret_object))}")
    return ret_object

class Foo(object, metaclass=FooMeta):
  def __init__(self) -> None:
    print("__init__")

  def __new__(cls) -> Self:
    print("__new__")
    return super().__new__(cls)

print("---")
bar = Foo()

# 运行结果:
# > ---
# > __call__
# > __new__
# > __init__
# > FooMeta.__call__ created: <__main__.Foo object at 0x7fde63c04050>

这个例子隐藏了非常多的细节,但是展示了 Python 对象最基本的创建模型:

元类被 call (这个例子中元类并没有生成它自己的实例

元类创建了一个类(这一步隐藏在元类的 super().__call__() 中)

类通过 new 产生一个新的对象

对象被 init

基础装饰器实现

下面的的例子实现了一个最基本的装饰器,它不接受参数,直接用一个函数替代了另一个函数: 装饰器函数接受原函数作为参数,返回一个包裹着原函数调用的新函数。

虽然这个装饰器函数被命名为 'decorator', 但是它的名称实际上并没有特别的要求

def decorator(func: Callable) -> Callable:
    def wrapper(*args: Any, **kwargs) -> Any:
        print("Doing something magic in the wrapper.")
        return_val = func(*args, **kwargs)
        print("Clean up after real function calls.")

        return return_val
    return wrapper

def log(text: str):
    print(text)

print("--- Raw:")
log("Calling the original function.")
print("--- Decorator:")

@decorator
def log(text: str):
    print(text)

# Can be replaced with:
# log = decorator(log)

log("Called through the decorator.")


# 这个最简单的装饰器运行结果如下:
# > --- Raw:
# > Calling the original function.
# > --- Decorator:
# > Doing something magic in the wrapper.
# > Called through the decorator.
# > Clean up after real function calls.

注意以上代码中注释掉的语句 log = decorator(log),揭示了 Python 装饰器的本质:装饰器返回的包裹函数被赋值给原变量,而这句话可以达到 @ 语法相同的效果。

探究本质的话, @decorator 语句无非是解释器提供的语法糖,优雅的替代 'decorator()' 函数调用而已。单步调试,会发现这句被分为两部分执行:上半部分会执行到被装饰函数定义完成,而下半部分会立即执行装饰器函数,将 'wrapper' 函数返回至装饰器外。

这个简单的例子应用了多个 Python 高级特性:函数式编程和闭包。函数不仅被作为参数和返回值;而且接收目标函数的实参定义在装饰器函数中,却在 'wrapper' 函数中被使用。这种把函数外部的变量与函数进行绑定的特性,被称为闭包。

由于使用了闭包特性,所以自然也应当遵循闭包的限制,请参阅相关资料。

进阶:为装饰器加上参数

如果希望在运行过程中改变装饰器的行为,很容易联想到:既然装饰器是一个函数,是否可以向它传入参数用来控制运行方式呢?答案是行不通的,因为 @ 语法糖的接口协议已使用被装饰函数作为装饰器的唯一参数。

那么带参装饰器如何实现的呢,解决的方式仍然是闭包。手法也是直接粗暴: 再增加一层包装,让简单装饰器本身也被顶层函数返回

def decorator_builder(type: str) -> Callable:
    def decorator(func: Callable) -> Callable:
        def wrapper(*args: Any, **kwargs) -> Any:

            print(f"Doing something {type} in the wrapper.")
            return_val = func(*args, **kwargs)
            print("Clean up after real function calls.")

            return return_val
        return wrapper
    return decorator

def log(text: str):
    print(text)

print("--- Raw:")
log("Calling the original function.")
print("--- Decorator:")

@decorator_builder("manipulation")
def log(text: str):
    print(text)

# Can be replaced with:
# log = decorator(log)

log("Called through the decorator.")


# 带参的装饰器运行结果如下:
# > --- Raw:
# > Calling the original function.
# > --- Decorator:
# > Doing something manipulation in the wrapper.
# > Called through the decorator.
# > Clean up after real function calls.

这个例子中,直接定义的函数从 'decorator' 变成了 'decorator_builder' 函数,装饰器从 直接定义 变作 被外层函数返回,并且装饰后的函数再次使用闭包特性,直接使用了在 'builder' 中定义的实参。

理解带参装饰器的关键是:@ 运算符会在装饰前目标函数前,首先求解 'decorator_builder()' 的值。 所以这个例子中,'decorator' 装饰器被 'decorator_builder' 返回;装饰前使用的参数,包装在 'decorator_builder' 函数体的闭包对象中。

高阶:装饰类的装饰器

正如前言,Python 的函数是对象、类也是对象;那么可不可以使用装饰器来装饰类?答案是肯定的,就如前面函数装饰的实现一样,只不过这一次不再返回函数,而是返回一个类。

实现的关键是:装饰器返回一个继承自被装饰类的包裹类,该类 'overload' 被装饰类的方法 。类似后代使用 __super()__ 方法调用祖先方法。

"重载" 不是 python 语境中的术语,但这个例子中包裹类继承了被装饰类,并实现了方法的覆盖,有助于准确理解这个方案。


### 静态方版本
def decorator(cls) -> object:
    class wrapper(cls):
        def __init__(self) -> None:
            pass

        @staticmethod
        def log(*args: Any, **kwargs) -> Any:
            print(f"Doing something magic in the wrapper.")
            return_val = cls.log(*args, **kwargs)
            print("Clean up after real function calls.")
            
            return return_val
    return wrapper

@decorator
class logger():
    @staticmethod
    def log(text: str):
        print(text)

logger.log("Called through the decorator.")


### 对象方法的版本
def decorator(cls) -> object:
  class wrapper(cls):
    def __init__(self) -> None:
      pass

    def log(self, *args: Any, **kwargs) -> Any:
      print(f"Doing something magic in the wrapper.")
      return_val = super(wrapper, self).log(*args, **kwargs)
      print("Clean up after real function calls.")
      return return_val

  return wrapper

@decorator
class logger():
  def log(self, text: str):
    print(text)

logger().log("Called through the decorator.")


# 运行结果如下:
# > Doing something magic in the wrapper.
# > Called through the decorator.
# > Clean up after real function calls.

新需求:使用类作为装饰器

至此函数作为装饰器已经介绍完毕。但是不难看出以上方案存在局限。

考虑这样的场景:装饰器本身是是有状态的,在运行期间会改变行为模式,比如添加方法,动态地改变日志格式。仅通过函数装饰器已经很难满足业务需求,使用类作为装饰器似乎是更好的选择。

明白了Python 的 @ 语法糖是对函数调用的简写,不难得出结论:凡是 Callable 对象都可以作为装饰器使用。考虑对象的创建过程 object = class(*args),把 'args' 替换为 'func/class' 即可使用类作为装饰器。

这个例子其实不太准确,因为类被调用的方法是 __init__ 而不是函数对象的 __call__,但是调用形式非常接近。如果把装饰器类换做装饰器对象,被 @ 调用的方法就会是 __call__

class decorator():
    def log(self, *args: Any, **kwargs) -> Any:
        print(f"Doing something magic in the wrapper")
        return_val = self.cls.log(*args, **kwargs)
        print("Clean up after real function calls")
        return return_val

    def __init__(self, cls):
        self.cls = cls

@decorator
class logger():
    @staticmethod
    def log(text: str):
        print(text)

logger.log("Called through the decorator.")

# 这个以类作为装饰器的例子运行结果如下:
# > Doing something magic in the decorator
# > Called through the decorator.
# > Clean up after real function calls

又是一个极度简化的示例,实际上并没有解决我们的问题:把装饰器的实现嵌入业务逻辑中。因为被装饰类已经被隐藏,无法实例化。只到这个程度的话,与函数装饰器相比不仅没有任何优势,反而引入了额外的复杂度。

但是这个例子确实演示了使用类作为装饰器关键: @SomeClass 形式的装饰器可以利用 'SomeClass' 类的初始化函数包装被装饰的类

变种:使用类作为装饰器装饰一个函数

上面的例子被装饰的对象是一个类,但是被装饰的也可以是一个函数。

class decorator():
    def __call__(self, *args: Any, **kwargs) -> Any:
        print(f"Doing something magic in the decorator")
        return_val = self.func(*args, **kwargs)
        print("Clean up after real function calls")

        return return_val

    def __init__(self, cls):
        self.func = cls


@decorator
def log(text: str):
    print(text)

log("Called through the decorator.")

# 这个以类作为装饰器的例子运行结果如下:
# > Doing something magic in the decorator
# > Called through the decorator.
# > Clean up after real function calls

和上面的例子相比,装饰器的 log() 方法变成了 __call__() 魔法方法,使装饰器类实例化以后的对象变成了 Callable 对象。所以对被装饰函数的调用,变成了对类实例对象的调用。从这个角度,无非手搓一个函数对象的轮子罢了。

实用化:使用类装饰装饰类并接受参数

(一)回顾使用带参数的函数装饰器的正确打开方式,可以发现主要过程分两步:

  1. 生成 Callable 对象,接受参数并返回一个装饰器;
  2. @ 语法将被装饰 Callable 对象作为参数传入上一步得到的装饰器,得到一个包裹着实被装饰对象的 Callable 对象;
  3. 使用时,引用包裹对象

(二)使用类作为装饰器,将函数对象概念用类对象概念进行替代

  1. 类的 __init__() 方法接受一个参数,用于传入被装饰对象;当解释器解析 @ 语句时,隐式调用了类的构造函数,返回了装饰器类对象作为包裹对象。
  2. 对被装饰的类的引用变成了装饰器类对象,两者显然并不一样,主要是对函数调用的处理:
    • 装饰器类的对象不具备类的特性,无法进一步实例化;
    • 装饰器对象没有 __call__() 方法时,装饰器对象仅可以使用被装饰类的静态方法和属性;
    • 装饰器对象包含 __call__() 方法时,可用于包裹一个函数;

显然,希望装饰器完全模拟被装饰的类时,还需要解决的主要问题有两个:

  1. 由于 __init__() 方法需要被用于传入参数,需要另一个方法接收 @ 语法传入的被装饰对象;
  2. @ 语法调用对象的方法返回一个类对象

class ClassDecorator(object):
    def __init__(self, tag: str) -> None:
        print("ClassDecorator.__init__")
        self._tag = tag

    def __call__(self, cls) -> Self:
        print("ClassDecorator.__call__")

        class WrapperMeta(type):
            def __call__(meta_cls) -> Any:
                obj = super().__call__(self._tag)
                print(f"WrapperMeta.__call__ creates {str(type(obj))}")
                return obj

        class Wrapper(cls, metaclass=WrapperMeta):
            def __init__(self, tag):
                self._tag = tag

            def log(self, *args: Any, **kwargs):
                print(f"Doing something magic in the wrapper with '{self._tag}'.")
                return_val = super().log(*args, **kwargs)
                print("Clean up after real function calls.")
                return return_val

        return Wrapper


print("... DECORATING ...")
@ClassDecorator("Class Decorator Demonstration")
class Logger():
    def log(self, text: str):
        print(text)
 

print("... USING ...")
Logger().log("Called through the decorator.")


# > ... STAGE 1 ...
# > ClassDecorator.__init__
# > ClassDecorator.__call__
# > ... STAGE 2 ...
# > WrapperMeta.__call__ created: <__main__.ClassDecorator.__call__.<locals>.Wrapper object at 0x7fde63aefe10>
# > Doing something magic in the wrapper with 'Class Decorator Demonstration'.
# > Called through the decorator.
# > Clean up after real function calls.

这个例子解决了以上三个问题,装饰以后的类可以被实例化,因为装饰器返回的也是一个类,并且继承了被装饰类,默认行为保持一致; 'call()' 方法的添加不再受到包裹类的限制。

最终的形态尽可能的回避了 python 的各种黑魔法,也避免了过度改装类对象,形式上相对简洁不少,也可以在 Wrapper 类中添加 'call()' 方法,灵活适配装饰函数和装饰类的转换,实现有状态的业务逻辑的控制。

参考资料:

Python “黑魔法” 之 Meta Classes