说起Python的装饰器(Decorator),其实是一个特别有用且有趣的功能,可以让原本的Python函数(不只是函数)拥有之前没有的功能,使函数的功能更完美、健壮,并且不需要修改该函数原来的代码,用起来十分简单易懂。
不过呢,在了解装饰器之前,我们需要先了解一下闭包的概念。
闭包
其实装饰器就是一个闭包(closure)。闭包简单来讲就是一个函数定义中,引用了函数外定义的变量,并且该函数可以在其定义环境外被执行,可以延申函数的作用域。
"""案例一"""
# outer是外函数
def outer():
msg = "I am closure"
# inner是内函数
def inner():
print(msg)
return inner
closure = outer()
# 输出 I am closure
closure()
由上述代码可见,闭包其实就是外函数中定义内函数(套娃),内函数还可以使用外函数的变量。
闭包所实现的功能,有些类似于将函数作为参数传参,可参考下列代码理解。
"""案例二"""
def outer(func):
print(func())
def inner():
msg = "I am parameter"
return msg
# 输出 I am parameter
outer(inner)
通过上述两种代码再看闭包的概念,其实也挺容易理解的。
一个简单的装饰器
理解了闭包,那么就可以更友好的理解装饰器。前文说过,装饰器其实就是一个闭包,装饰器也是通过函数嵌套的方式实现。
"""案例三"""
# 定义装饰器tips
def tips(func):
def wrapper(*args, **kwargs):
print("计算完成:")
return func(*args, **kwargs)
return wrapper
# 调用装饰器tips
@tips
def add(a,b):
print(f"{a}+{b}={a+b}")
# 计算完成:
# 1+2=3
add(1, 2)
分析一下上述代码。当add函数被装饰时,add函数就成为了装饰器的参数func,当执行过装饰器内的wrapper函数后,最后再返回func函数,即执行add函数。
由此可见,装饰器可以让我们自定义的给函数新增功能,可以在其调用之前,亦可在其调用之后。
@functools.wraps(func)
笔者之前做的一个flask项目中,曾经遇见过一个问题,那就是装饰器的重名问题,困扰了笔者很长时间,也只能怪当时学艺不精。
以上述代码为例。说到底,装饰器不过都是执行的wrapper函数罢了。既然如此,add函数的属性要怎么访问呢?
@tips
def add(a, b):
print(f"当前函数:{add.__name__}")
print(f"{a}+{b}={a+b}")
上述代码对案例三中的add函数进行了小修改,添加了一条输出语句,通过魔法方法name让其输出当前函数名称。然而,结果却出人意料。
# 调用add函数
add(1, 2)
# 计算完成:
# 当前函数:wrapper
# 1+2=3
可见,装饰器使add本身的属性消失了。上文中笔者便提到,装饰器执行的都是内部的包装函数(调用闭包),也就是当前装饰器中的wrapper函数。
由此可见,一旦函数增加了装饰器,那么函数本身的属性会被装饰器内部的包装函数替换。@functools.wraps(func)便是解决该问题的关键。
引入wraps后,对案例三的tips装饰器进行修改。
"""案例四"""
from functools import wraps
# 装饰器tips
def tips(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("计算完成:")
return func(*args, **kwargs)
return wrapper
@wraps是python提供的装饰器。它能把原函数的元信息拷贝到装饰器里面的func函数中,这个函数将自动为包装器函数添加一个 __ wrapped __ 属性,以此为关联被包装的函数,让我们在装饰器里面访问在装饰之前的函数的属性。
此时,再调用add函数。
# 调用add函数
add(1, 2)
# 计算完成:
# 当前函数:add
# 1+2=3
带有参数的装饰器
一个携带了参数的装饰器将有三层函数(套娃),第一层函数用来接收装饰器传递的参数。请参考下列代码。
"""案例五"""
from functools import wraps
# 装饰器tips
def tips(parameter):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(parameter)
return func(*args, **kwargs)
return wrapper
return decorator
# 调用装饰器tips
@tips('计算完成:')
def add(a, b):
print(f"当前函数:{add.__name__}")
print(f"{a}+{b}={a+b}")
# 调用add函数
add(1, 2)
# 输出:
# 计算完成:
# 当前函数:add
# 1+2=3
装饰器重叠
当一个被装饰的对象同时叠加多个装饰器时,装饰器的加载顺序是由下而上,装饰器的执行顺序是由上而下。只要了解这个顺序,便可以对装饰器进行各种各样的重叠使用了。
"""案例六"""
from functools import wraps
# 第一个装饰器
def first_decorator(func):
print("i am first")
@wraps(func)
def wrapper(*args, **kwargs):
print("i am no.1")
return func(*args, **kwargs)
return wrapper
# 第二个装饰器
def second_decorator(func):
print("i am second")
@wraps(func)
def wrapper(*args, **kwargs):
print("i am no.2")
return func(*args, **kwargs)
return wrapper
# 为text函数增加两个装饰器
@first_decorator
@second_decorator
def text():
print("i am text")
# 调用text函数
text()
上述代码的执行结果为:
# i am second
# i am first
# i am no.1
# i am no.2
# i am text
由上述可见,所谓的加载程序即装饰器第一层的函数的执行过程,会按照由下往上进行加载。
执行程序即调用装饰器内的wrapper函数,会按照由上往下执行。
大家可以尝试使用一些开发工具的debug模式逐步观察运行顺序,以便加深理解。
类装饰器
首先,需要先说明一下,装饰器不仅仅只能装饰函数,还可以装饰类。
"""案例七"""
from functools import wraps
# 定义装饰器laugh
def laugh(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("ha ha ha")
return func(*args, **kwargs)
return wrapper
# 调用装饰器laugh
@laugh
class Animal:
def __init__(self, a_type=''):
self.a_type = a_type
# 重写魔法方法str,便于友好展示
def __str__(self):
if self.a_type:
return f"i am {self.a_type}"
else:
return "i am animal"
dog = Animal("dog")
print(dog)
print(Animal())
# 输出:
# ha ha ha
# i am dog
# ha ha ha
# i am animal
我们也可以通过重写类的魔法方法call,使类具有装饰器的效果(类装饰器)。
"""案例八"""
# 定义Motion类
class Motion:
def __init__(self, func):
self.func = func
# 重写魔法方法call
def __call__(self, *args, **kwargs):
print("i am decorator")
return self.func(*args, **kwargs)
# 调用类装饰器Motion
@Motion
def run():
print("i am run")
# 调用run函数
run()
# 输出
# i am decorator
# i am run
以上便是笔者对Python装饰器的简单理解及一些简单案例。笔者不由得感叹python语言的简洁精悍。