摘要
在 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
对象。所以对被装饰函数的调用,变成了对类实例对象的调用。从这个角度,无非手搓一个函数对象的轮子罢了。
实用化:使用类装饰装饰类并接受参数
(一)回顾使用带参数的函数装饰器的正确打开方式,可以发现主要过程分两步:
- 生成函数(Callable 对象) 接受参数并返回一个装饰器;
@
语法将被装饰函数(也是 Callable 对象)传入上一步得到的装饰器,得到一个包裹函数; 使用时实际上使用的是包裹对象;
(二)使用类作为装饰器,将函数对象概念用类对象概念进行替代
- 类的
__init__()
方法接受一个参数,用于传入被装饰对象;当解释器解析@
语句时,隐式调用了类的构造函数,返回了装饰器类本身作为包裹对象。由于__init__()
方法被调用,说明产生的是类装饰器实例化的一个普通对象;而被装饰的是一个类对象,两者显然并不一样。变化出现在此后的步骤中:- 装饰器对象没有
__call__()
方法时,装饰器对象仅可以使用被装饰类的静态方法和属性; - 装饰器对象包含
__call__()
方法时,可用于包裹一个函数;
- 装饰器对象没有
显然,希望装饰器完全模拟被装饰的类时,还需要解决的主要问题有两个:
- 由于
__init__()
方法需要被用于传入参数,需要另一个方法接收@
语法传入的被装饰对象; - 让
@
语法调用对象的方法返回一个类对象,__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 的各种黑魔法,也避免了过度改装类对象,形式上简洁不少。但是所用到的技巧仍然是最简单函数装饰所用的那一套,反倒引入了元类用魔改装饰器类,实在算不得真优雅。本例只是为了展示类装饰器的实现思路,实际应用中没有特别的需求时应回避无谓的技巧。