Python自定义装饰器使用详解及其原理深度剖析
注意:先行知识 python,本篇文章所有代码均为实际运行,为原理和逻辑讲解
一、装饰器
装饰器是 python
中的一种语法糖,虽然我不想用语法糖这个词来表达,但这句话写在了开头,我也不到用别的更准确的词来形容他了。
如果你刚接触编程不久,不理解语法糖,也没关系;在这里我说语法糖并不会影响到你接下来的理解,我只是用它在赘述,作为了一种形容词。
首先我们要搞懂装饰器是什么东西,其次搞懂装饰器的基础知识点,最后逐渐加深即可非常熟练的使用装饰器。
其实,你可以将装饰器当作是 python 自带的函数功能增效器,使函数可以额外的增加功能,但并不影响被增加功能函数本身(虽然有办法影响,但一般不会这样去做)。就像是一个插座,本身这个插座只允许两个头的插孔电器,但这个插座有一个功能,有一个连通器可以额外增加不同的插座接口,就这样把不同功能的插座进行连接,这样就实现了额外的功能,但这个插座本身却不会发生改变。
二、简单的装饰器
咱们的装饰器学习一步步进行,先来看一个最简单的装饰器:
@app.route("/")
def home():
return "Hello World!"
@app.route("/")
这是一个在 flask
中的路由装饰器,在这里与 home
函数进行绑定,在检测到访问当前站点的根目录时,即会执行 home
函数。
而在这里,装饰器的作用则是给了当前的 home
函数一个额外的功能,使其能够响应 web
请求。
那么咱们开始编写一个自定义装饰器:
def simple_decorator(func):
def wrapper():
print("Before function call")
result = func()
print("After function call")
return result
return wrapper
以上是一个简单的自定义装饰器的实现,咱们可以得知,一个简单的嵌套函数形式。咱们解构一下装饰器的语法,得到以下结果:
def 装饰器名称(func): # 接收一个函数(比如 home)
def wrapper(): # 包装原函数
# 添加自定义的新功能(比如权限检查)
func() # 执行原函数
return wrapper
我们从以上的语法中得知,装饰器最外层的函数,为一个接收一个函数作为参数的函数,其内部用一个 wrapper 为名函数包装原函数 func(),并且在此处 func() 执行原函数;最后返回整个函数 wrapper。
你可以理解为一个函数使用了装饰器如下:
@decorator
def func():
pass
随后会转变成:
func = decorator(func)
你再这里可能有疑问,在这里一定要使用 wrapper 来包裹(包装)原函数 func() 吗?当然不,在这里 wrapper 只是一个约定俗成的名字,但推荐用 wrapper 为名的函数来包裹 func() 原函数的执行,因为可能某些第三方的工具、库 之类会采用这个约定俗称的方式作为依赖,说不定会影响到某些功能的实现,并且 func 也是约定俗称代表着原函数。
其实通过以上的简单了解,此时应该知晓,基础的自定义装饰器其实就是一个二次包裹的一个函数,外层函数接收原函数为参数,二层函数则包裹这个原函数进行执行,最后返回包裹原函数的函数即可。
三、装饰器详解
通过第二点我们可以发现,例子中没有对应的传递参数给装饰器的功能,那当我们需要对装饰器进行传参时该怎么做呢?
其实很简单,咱们只需要在原本的两层包裹的函数外再加上一层函数接收参数即可:
def out_decorator(param):
def in_decorator(func): # 第二层:接收函数
def wrapper(): # 第三层:包装函数
print(param)
func()
return wrapper
return in_decorator
@out_decorator("装饰器传参") # 传递参数
def home():
print("原本函数内容")
home() # 输出:装饰器传参 → 原本函数内容
再上面的函数中,out_decorator 是一个新增接收参数的装饰器,当然也可以接收多个,其内部的两层包裹的函数与之前第二次所阐述的简单装饰器一样,使用 in_decorator 接收了 func 原函数后,在 in_decorator 函数内部再使用 wrapper 对原函数进行包裹即可,最后返回包裹函数 wrapper。
四、原函数的传参
既然说到了装饰器传参,那原函数的传参我们该怎么做呢?
在此我们使用一种无需维护的传参方式,为了使一些可能对python 接触不深的同学阅读,在此回对这种方式进行一下解释。
4.1 位置参数
在正式介绍传参前,需要了解一下函数参数的两种参数写法,首先是位置参数。
咱们在对函数进行传参时可能会这样 func(1, 2, 3)
;此时我们将 func 的参数 1 会说成传给 func 的第一个参数为 1,那么第二个参数为2,第三个参数为 3… 这样以此类推,我们使用位置对这些参数进行语言上的引导与定位,这种传参方式的参数即为位置参数。
4.2 关键字参数
在传参时不仅可以使用 func(1, 2, 3)
这样的位置参数,还可以使用 func(name="John", age=20)
这样的传参方式。
func(name="John", age=20)
的传参方式指定了参数所对应的“名”,就例如给与了一个键值对的方式,这种方式不再使用位置信息作为引导指定,我们会说给与 func 函数的参数有 2 个,一个是name 另一个是 age,name 的值为 john,age 的值为 20;这种传参方式的参数即为关键字参数。
4.3 *args 与 **kwargs 的打包
在python 中,有两个特殊的符号表示不同参数,其中 *args 表示位置参数,**kwargs 表示 关键字参数。
在这里,若读者没有深究,可能觉得 *args 与 **kwargs 就是关键字的存在,其实,在这里真正起作用的是 * 和 **,而 args 、kwargs 则是约定俗称的名称,args 表示位置参数、kwargs 表示关键字参数(先这样理解,但在不同情况下有其他解释,这只是当说命名上。)
在 python 中 * 与 ** 为python中的可变参数,主要用于对数据进行打包、解包。
我们先演示打包吧,以下是一个例子:
def func(a, *args, **kwargs):
print("a:", a)
print("args:", args) # 额外位置参数 → 元组
print("kwargs:", kwargs) # 关键字参数 → 字典
当我们传入参数 func(1, 2, 3, name=“John”, age=20) 时,位置参数1则是个与到 func 中的参数 a,2与3则直接被 *args进行了打包,成为一个元组 (2, 3) 给与到变量 args,而 name=“John”, age=20 关键字变量,则被 **kwargs 打包给与到 kwargs。
其结果输出为:
# 输出:
# a: 1
# args: (2, 3)
# kwargs: {'name': 'John', 'age': 20}
其中参数 a, *args, **kwargs :
不要认为这是指针,这是 python 中的可变参数的语法*args 和 **kwargs 只是参数处理方式,不涉及内存地址操作。python是高级语言,所有变量都是对对象的引用(类似指针的抽象概念),但用户无法直接操作内存地址。
总结一下:
4.4 *args 与 **kwargs 的解包
以下是一个演示 * 和 ** 的解包代码:
def add(a, b):
return a + b
numbers = (1, 2)
add(*numbers) # 等价于 add(1, 2) → 3
params = {"a": 3, "b": 4}
add(**params) # 等价于 add(a=3, b=4) → 7
以上代码在对元组 numbers = (1, 2) 进行解包后 add(*numbers) 传递参数,此时的元组参数变成了单独的数据进行传参,此时参数为位置参数。
以上代码在对字典 params = {“a”: 3, “b”: 4} 进行解包后 add(**params) 传递参数,此时的元组参数变成了单独的key、val 数据进行传参,此时参数为关键字参数参数。
总结一下:
4.5 原函数传参实现
通过以上对 *args 与 kwargs 后,已经知晓如何接收所有的位置参数与关键字参数,那么此时我们只需要在包装原函数的 wrapper 函数写上 *args, kwargs,再原样给与到 func 函数参数即可:
def decorator(func):
def wrapper(*args, **kwargs): # 接收所有参数
print("装饰器逻辑")
return func(*args, **kwargs) # 原样传递参数给原函数
return wrapper
可能有些同学会问,不给 wrapper 写 *args, **kwargs 不可以吗?其实装饰器的核心是替换原函数,当调用被装饰的函数时,实际上调用的是wrapper函数。
因此,wrapper需要能够接收所有传递给原函数的参数,无论是位置参数还是关键字参数。如果wrapper不接受任何参数,当被装饰的函数被调用时,传递的参数就会丢失,导致错误。
若你的装饰器也需要接收参数,那么再嵌套一层函数即可:
def out_decorator(param):
def in_decorator(func):
def wrapper(*args, **kwargs): # 接收所有参数
print("装饰器逻辑")
return func(*args, **kwargs) # 原样传递参数给原函数
return wrapper
return in_decorator
4.6 保留原函数元数据
若有一个函数使用了装饰器:
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def home():
print("Hello")
print(home.__name__) # 输出:wrapper
此时当你调用当前 home 函数的元数据函数名时,会直接输出 wrapper 这个包裹原函数的函数名。
在上一节说过装饰器的核心是替换原函数,当调用被装饰的函数时,实际上调用的是wrapper函数。
那么如何才能让装饰器不干扰原函数,使其假装起到“装饰”作用,假装不更改其内部元数据呢?此时只需要添加 @wraps(func) 即可。
注意 @wraps(func) 需要引入 from functools import wraps。
修改代码为:
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def home():
print("Hello")
print(home.__name__) # 输出:wrapper
如果是装饰器需要接收参数,也同理添加即可在 wrapper 之前即可:
def out_decorator(param):
def in_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs): # 接收所有参数
print("装饰器逻辑")
return func(*args, **kwargs) # 原样传递参数给原函数
return wrapper
return in_decorator
在这里建议,除非你有特殊需求,其他情况建议都加上 @wraps(func)。
五、类装饰器
装饰器不止可以使用函数实现,还可以使用类,代码如下:
class ClassDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("装饰器逻辑前")
result = self.func(*args, **kwargs)
print("装饰器逻辑后")
return result
@ClassDecorator
def my_function():
print("原函数逻辑")
my_function()
# 输出:
# 装饰器逻辑前
# 原函数逻辑
# 装饰器逻辑后
__call__
方法承担了类似 wrapper 的角色,在 Python 中,__call__
是一个特殊方法,它允许类的实例像函数一样被调用,从而执行 __call__
中的代码,这与之前所说的在调用被装饰的函数实际上是执行了 wrapper 一样,则当类装饰器中使用 __call__
时,它的核心作用是让装饰后的函数调用逻辑被包装在类的方法中。
作者:1_bit