Python:说明白装饰器

摘要

在 Python 语言中使用装饰器可以轻易地实现 Decorator 模式:在不改变现有对象的情况下,动态地给该对象增加额外功能。同时 Python 还为装饰器提供了 @ 语法糖,灵活运用装饰器可以在应用开发过程中带来很多便利。虽然 Python 的装饰器基础用法简单,但若想要灵活应用,实现进阶用法,还得了解 Python 的闭包(Closure)、对象模型。

前提

本文所有资料与实验均基于 python 3,详细的 python 版本为 3.11.6。

bash 复制代码
❯ python --version
Python 3.11.6

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

类、函数、对象实例,一切都是对象

Python 语法中一切都是对象;函数是可以调用(Callable)的对象;类是可以创建对象的对象,如下 bar = Foo() 的执行过程:

python 复制代码
class CreateFoo(type):
    def __call__(self, *args: Any, **kwds: Any) -> Any:
        print("__call__")
        return super().__call__()

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

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

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

# 运行结果:
# > ---
# > __call__
# > __new__
# > __init__

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

  • 元类被 call (这个例子中元类并没有生成它自己的实例
  • 元类创建了一个 实例 (类)
  • 类对象通过 new 产生一个新的实例(对象)
  • 实例对象被 init

基础

先来一段最基础的装饰器实现:

python 复制代码
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.

这个例子实现一个最基本的装饰器:一个函数接受另一个函数(对象)作为参数,返回又一个函数。虽然这个函数被命名为 'decorator', 但是这个名称实际并没有讲究。

这揭示了 Python 装饰器的本质:用另一个函数取代目标函数,从而实现移花接木的效果;而 log = decorator(log) 显然可以可达到相同的效果。实际上 @decorator 本身无非是解释器提供的语法糖,优雅的替代 'decorator()' 函数调用而已。在 IDE 中单步调试以上代码,会发现 @decorator 这一句被分为两部分:上半部分会执行到被装饰函数定义完成,而下半部分会立即执行装饰器函数,将 'wrapper' 函数返回至装饰器外。

虽然简单,但该例应用了多个 Python 高级特性:函数式编程和闭包。函数不仅被作为参数和返回值;而且仔细观察的话,接收目标函数的实参其实定义在装饰器函数中,但却在 'wrapper' 函数中被使用。这种把函数外部的变量与函数进行绑定,被称为闭包。由于使用了闭包特性,所以自然也应当遵循闭包的限制,具体请参阅相关资料。

进阶:为装饰器加上参数

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

python 复制代码
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' (是的,你没有看错。这是一个对被装饰类的方法的覆盖,因为需要返回一个被装饰类的后代,包裹类的定义中继承了目标类)。

为了简单起见,下面的例子中将使用类的静态方法,对于对象的处理将在更后面一些探讨。

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.")

# 这个最基础的装饰器运行结果如下:
# > Doing something magic in the wrapper.
# > Called through the decorator.
# > Clean up after real function calls.

更进一步:使用类作为装饰器

至此基本的装饰器已经介绍完毕;但是,不出意外的话业务需求总有意外。考虑这样的场景:装饰器本身是业务需求的一部分,是有状态的,并且在运行期间会改变行为模式。这种情况下通过接受参数的函数装饰器已经很难满足业务需求了,必要时可考虑使用类作为装饰器。

明白了Python 的 @ 语法糖是对函数调用的简写,不难得出结论:凡是 Callable 对象都可以作为装饰器使用。再次考虑 object = class(*args)代码,把 'args' 替换为 'func'(此处举这个例子其实不太准确,因为类被调用的方法是 __init__ 而不是 __call__,但是形式非常接近。如果把装饰器类换做装饰器对象,被 @ 调用的方法就会是 __call__)。使用类作为装饰器:

python 复制代码
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' 类的初始化函数包装被装饰的类。

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

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

python 复制代码
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 对象)传入上一步得到的装饰器,得到一个包裹函数; 使用时实际上使用的是包裹对象;

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

  1. 类的 __init__() 方法接受一个参数,用于传入被装饰对象;当解释器解析 @ 语句时,隐式调用了类的构造函数,返回了装饰器类本身作为包裹对象。由于 __init__() 方法被调用,说明产生的是类装饰器实例化的一个普通对象;而被装饰的是一个类对象,两者显然并不一样。变化出现在此后的步骤中:
    • 装饰器对象没有 __call__() 方法时,装饰器对象仅可以使用被装饰类的静态方法和属性;
    • 装饰器对象包含 __call__() 方法时,可用于包裹一个函数;

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

  1. 由于 __init__() 方法需要被用于传入参数,需要另一个方法接收 @ 语法传入的被装饰对象;
  2. @ 语法调用对象的方法返回一个类对象, __call__() 方法就时关键
python 复制代码
class decorator():
    def __init__(self, tag: str):
        self.tag = tag

    def __call__(self, cls):
        return type(cls.__name__, (cls,), {"log": decorator.log, "tag": self.tag})

    def log(self, text: str):
        print(f"--- {self.tag} ---")
        print("Doing something magic in the decorator.")
        super(self.__class__, self).log(text)
        print("Clean up after real function calls.")

@decorator("demo")
class Logger():
    @staticmethod
    def log(text: str):
        print(text)

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

# 这个例子运行结果如下:
# > --- demo --
# > Doing something magic in the decorator.
# > Called through the decorator.
# > Clean up after real function calls.

在这个例子中,我们创造了一个装饰器类,这个类在实例化时接收参数。实例化以后的对象被当作 Callable 调用时,传入了另一个类。Python 的黑魔法在这里派上了用场:使用 type 类(这个类也是 Callable )直接创建了一个新的 类对象 。稍后我们使用这个经过装饰的类时,实际上使用的都是由这个类对象实例化的结果。

组合:并不优雅的最终优雅形态

虽然上一个例子中,装饰器类已经工作的非常像被装饰的类了,但是却并不优雅。如果执行 dir(Logger) ,你可能会得到这样的结果:

python 复制代码
# 省略上例中的代码 ...  
dir(Logger)

# > ['__class__', ..., 'log', 'tag']

以上结果显示,'log' 、 'tag' 是装饰器类对象成员,在类实例对象间共享,通常这样做有违设计范式。把此前提到过的内容组合到一起,

python 复制代码
class Decorator():
    def __init__(self, tag):
        self.tag = tag

    def __call__(self, cls) -> Any:
        def log(_o, text):
            print(f"--- {self.tag} ---")
            print(f"Doing something magic in the decorator")
            super(_o.__class__, _o).log(text)
            print("Clean up after real function calls")

        return type(cls.__name__, (cls, ), {"log": log})

@Decorator("DECORATOR TAG")
class Logger():
    def log(self, text):
        print(text)

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

# > --- DECORATOR TAG ---
# > Doing something magic in the decorator.
# > Called through the decorator.
# > Clean up after real function calls.

这个例子中,装饰器类首先被实例化,保存装饰器到自身属性中;其 __call__() 方法被 @ 调用时返回一个由元类创建的新包裹类,包裹类继承了被装饰类,覆盖了基类的部分方法,以完成业务逻辑。最终包裹类顶替了被装饰器的名字,参与到业务逻辑中。

最终的形态尽可能的回避了 python 的各种黑魔法,也避免了过度改装类对象,形式上简洁不少。但是所用到的技巧仍然是最简单函数装饰所用的那一套,反倒引入了元类用魔改装饰器类,实在算不得真优雅。本例只是为了展示类装饰器的实现思路,实际应用中没有特别的需求时应回避无谓的技巧。

参考资料:

Python "黑魔法" 之 Meta Classes

相关推荐
魔道不误砍柴功12 分钟前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
_.Switch37 分钟前
高级Python自动化运维:容器安全与网络策略的深度解析
运维·网络·python·安全·自动化·devops
测开小菜鸟2 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
萧鼎3 小时前
Python并发编程库:Asyncio的异步编程实战
开发语言·数据库·python·异步
学地理的小胖砸3 小时前
【一些关于Python的信息和帮助】
开发语言·python
疯一样的码农3 小时前
Python 继承、多态、封装、抽象
开发语言·python
Python大数据分析@4 小时前
python操作CSV和excel,如何来做?
开发语言·python·excel
黑叶白树4 小时前
简单的签到程序 python笔记
笔记·python
Shy9604184 小时前
Bert完形填空
python·深度学习·bert
上海_彭彭4 小时前
【提效工具开发】Python功能模块执行和 SQL 执行 需求整理
开发语言·python·sql·测试工具·element