Python装饰器详解
一、基础语法
1.1 函数装饰器
Python装饰器(Decorator)是一种特殊的函数,其可以装饰其他的函数或类。所谓装饰,其实就是将其他的函数或类作为参数传入,在原有基础上添加一些新逻辑或者属性后,将这个新的函数或类返回的过程。
我们首先来看函数装饰器,这里给出一个例子。
# 一个简易的示例装饰器
def myDecorator(func):
def wrapper(*args, **kwargs):
print("这里是添加的装饰器逻辑")
return func(*args, **kwargs)
return wrapper
# 被装饰函数
def example():
print("这里是被装饰的函数")
# 装饰过程
example = mydecorator(example)
# 调用被装饰后的函数
example()
# 调用结果
# >> 这里是添加的装饰器逻辑
# >> 这里是被装饰的函数
上面这段代码中,我们首先定义了一个装饰器函数,该函数以其他函数为形参,并返回一个新的函数对象。(请注意,在Python中函数本身就是一等对象,即可以被当作形参传递、当作返回值返回,也可以被存储在数据结构中)
在装饰过程中,example
这一变量被装饰器返回的新函数对象所替代。 因此在之后的调用中,结果里显示了装饰器导入的打印逻辑。这就是函数装饰器的原理。
1.2 @Wrapper语法糖
上面的代码和大家之前了解的装饰器画风可能不太一致。
事实上,装饰器本身只是函数的一种用法,而Python为了方便大家应用这种技巧,提供了一种语法糖,来快速应用装饰器函数。
# 使用@wrapper装饰器语法来快速应用装饰器函数
@myDecorator
def example():
print("这里是被装饰的函数")
这段代码与1.1中的装饰过程是等价的,程序员使用@mydecorator
语法时,等价于使用example = mydecorator(example)
对example
函数进行装饰。也就是说,被装饰的函数对象会作为参数传入装饰器函数,因此,请确保你的装饰器函数拥有合适数量的形参以接收被装饰函数对象。
从语法上讲,@
后面可以连接任何赋值表达式,最简单的情况就是我们直接给出装饰器函数名。如果是其他类型的表达式,也只需确保表达式的返回值是一个可调用的函数对象即可(这一点为后面的含参装饰器提供了支持)。
1.3 类装饰器
之前讲到,类与函数都可以通过装饰器进行装饰。装饰函数实际就是返回一个新函数对象,对于类而言道理相同,就是返回一个新的类对象而已。请注意这里的类对象不是指类的实例,而是指类类型对象(对,这个类对象的类型就是type
)。可以简单的理解为重新给出了被装饰类的定义。对一个类的装饰包括但不限于添加新的方法与属性,当然也可以覆盖或重写原类型的方法与属性。下面给一个简单的示例:
# 定义类装饰器
def myDecorator(cls):
class Wrapper:
def __init__(self, *args, **kwargs):
self.instance = cls(*args, **kwargs)
self.newAttr = "这是装饰器添加的新属性"
def __getattr__(self, name):
print("__getattr__方法经过了调用")
return getattr(self.instance, name)
def newfunc(self):
print("这是装饰器添加的新方法")
return Wrapper
# 使用上述装饰器对一个类进行装饰
@myDecorator
class Example:
def oldfunc(self):
print("这里是oldfunc")
# 调用原类型方法
(e := Example()).oldfunc()
# 调用结果
# >> __getattr__方法经过了调用
# >> 这里是oldfunc
可以看到,这个装饰后的类在实例化时,会创建原类型的实例作为属性,只有通过原类型实例我们能够访问原类型的属性与方法。这个过程用到了__getattr__
魔术方法,其作用是当无法直接获取到对象方法或属性时,会采用该方法对目标属性或方法进行查找。
正常来说,在对Example
进行装饰后,Example
实际上是被Wrapper
类型所替代,如果直接采用(e := Example()).oldfunc()
去调用装饰前Example
的oldfunc
方法是不会成功的,因为Wrapper
类中并不包含Example
类的属性与方法。
想要获取原类型的属性与方法可以通过getattr()
方法完成(作用是通过一个类型的实例获取其属性与方法,而区别于之前提到的魔术方法__getattr__
)。
然而,装饰器的精髓就在于装饰二字,无论我们怎么修饰原类型,它都应该具备自己原本的属性与方法,修饰后类型的外观与原类型不能有显著差别。因此可以借助__getattr__
方法包装getattr(instance, name)
方法,这样我们就可以通过装饰后类型的实例,对原类型属性方法进行直接访问,使用体验也几乎无差别。
二、进阶用法
2.1 含参装饰器
如果一个装饰器从它被定义的那刻起,它的装饰行为就不可更改,那么会在很多场景上给开发者带来困扰。因为这意味着,如果开发者只想对装饰器的装饰行为进行微调,却要重新定义一个新的装饰器。幸运的是,我们可以通过一些技巧来为装饰器指定特定参数,以此来改变装饰器的行为。下面是一个示例:
# 一个简单的含参装饰器
def myDecorator(name): # 套壳,接受参数
def realDecorator(func): # 这一层才是真正的装饰器函数
def wrapper(*args, **kwargs):
print(f"{name}定制装饰器")
return func(*args, **kwargs)
return wrapper
return realDecorator
# 装饰过程
@myDecorator(name="China")
def example1():
print("示例函数1")
@myDecorator(name="WuHan")
def example2():
print("示例函数2")
# 调用装饰后函数
example1()
# 调用结果
>> China定制装饰器
>> 示例函数1
>> WuHan定制装饰器
>> 示例函数2
之前提过,@
装饰器语法糖中,@
不光可以跟简单的函数名,也可以跟赋值表达式,只要确保这个表达式的返回值是一个合法装饰器函数即可。含参装饰器便是运用了这一技巧,@myDecorator(name="WuHan")
实际上是对myDecorator
函数的带参调用,而myDecorator
返回的正是真正的装饰器函数realDecorator
,外层这一套壳起到了处理自定义参数的效果。
2.2 装饰器堆叠
装饰器可以堆叠使用,一个函数可以被无数个装饰器装饰。下图是一个被两个装饰器进行装饰的函数,请思考一下两个装饰器作用的顺序。
# 装饰器1
def deco1(func):
def wrapper(*args, **kwargs):
print("deco1开始")
result = func(*args, **kwargs)
print("deco1结束")
return result
return wrapper
# 装饰器2
def deco2(func):
def wrapper(*args, **kwargs):
print("deco2开始")
result = func(*args, **kwargs)
print("deco2结束")
return result
return wrapper
@deco1
@deco2
def example():
print("被装饰函数!")
# 调用
example()
# 调用结果
# >>deco1开始
# >>deco2开始
# >>被装饰函数!
# >>deco2结束
# >>deco1结束
不难看出,离目标函数近的装饰器会先进行装饰。这个其实很好理解,只需要想一下装饰器的原理,以及上述装饰器装饰过程的等价形式即可:
# 等价形式,deco2先作用于example,deco1作用域装饰后的example
example = deco1(deco2(example))
2.3 装饰器类
看到这里,需要对之前的一个概念作出纠正。装饰器只能是函数吗?其实不然。只要是可调用对象即可。而这个可调用的特性来自于Python对象的魔术方法__call__()
,当我们调用一个可调用对象时,实际上就是执行的__call__
。对于函数对象而言,__call__
中的内容就是函数体代码。而对于自定义类来说,我们也可以通过定义它的__call__
方法来使其成为一个可调用对象。
class MyDecorator:
def __call__(self, func):
def wrapper(*args, **kwargs):
print("装饰器逻辑")
return func(*args, **kwargs)
return wrapper
@MyDecorator()
def example():
print("被装饰函数")
请注意,__call__
方法使得一个类的实例可以被调用,而并非类型本身被调用,因此这里@
后跟的应该是MyDecorator
的一个实例,即MyDecorator()
。
换个角度,不光装饰器本身可以是可调用类,里面的wrapper函数你要是愿意,也可以换成可调用类,用可调用类来替换被装饰的函数类型,这完全可行。
def myDecorator():
class Wrapper:
def __init__(self, func):
self.func = func
def __call__(self, *agrs, **kwargs):
print("装饰器逻辑")
return self.func(*agrs, **kwargs)
return Wrapper
@myDecorator()
def example():
print("被装饰函数")
请注意这种写法下@
后面需要调用myDecorator函数,以返回Wrapper类。这里的装饰过程实际上就是Wrapper类的实例化过程,以func作为构造函数参数,得到的实例将用于替换原来的example函数。
2.4 保留被装饰对象的元数据
最后来聊聊装饰这种行为的副作用
。在最开始讲的函数装饰器场景下,无论你是用的哪种装饰方法,你的被装饰函数都不再是原来的那个对象了。这产生的后果就是,原函数的元数据会全部丢失。函数对象常用的元数据这里列了一些。
元数据属性名 | 描述 |
---|---|
__name__ | 函数名称 |
__doc__ | 函数文档字符串,也就是大家在函数下面写的以一对三个双引号包裹起来的注释字符串,会被Pycharm等IDE读作函数文档 |
__module__ | 函数所在模块名 |
__annotations__ | 函数形参、返回值的类型注解 |
__code__ | 函数的字节码对象 |
__defaults__ | 函数默认参数信息 |
请仔细看,下面给出一个未被装饰的函数。
def example(num: int = 2) -> int:
"""
return the number + 1
:param num: an int number
:return: num + 1
"""
return num + 1
我们打印一下它的元数据信息。
print(f"__name__: {example.__name__}")
print(f"__doc__: {example.__doc__}")
print(f"__annotations__: {example.__annotations__}")
print(f"__code__: {example.__code__}")
print(f"__defaults__: {example.__defaults__}")
现在我们将该函数用下面的装饰器函数进行装饰一下。
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
再打印一下元数据。
可以看到“一切都变了”,因为现在虽然你使用的是example
这个变量,但是这个变量存储的并不是原函数的引用,而是一个装饰器中wrapper函数对象的引用。这种特性与装饰的理念并不相符,因为这种装饰行为没有很好地保持被装饰对象的外观。元数据最大的作用就是提高代码的可读性,其中的默认参数、类型注解、函数文档等信息对于程序员的开发十分有帮助。理想状态下,被装饰函数的关键元数据不应该发生改变。
要做到这一点,简单粗暴地,可以在返回wrapper
对象之前将wrapper
的元数据修改为原函数的版本。但是更优雅的做法是使用Python内置包functools
中的wraps
方法,来对装饰器函数进行额外装饰,下面是一个示例。
from functools import wraps
# 可以保存原函数元数据的装饰器
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
通过wraps
对装饰器返回的函数对象类型进行装饰,可以将原函数的元数据进行完整载入,以避免装饰行为对目标函数元数据的影响。对原理感兴趣的可以去看看文档,这里不展开描述。
以上就是本篇文章的全部内容,希望对你能有帮助。
在下水平有限,如有不当,烦请不吝赐教。
参考文档
1. Python函数定义Reference文档
2. Python装饰器文档
3. Python类定义文档
4. Python内置方法 functools.wraps文档
更多推荐
所有评论(0)