这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
说起来装饰器,其实算是Python中比较难理解的知识点之一了,在Python开发的面试过程中,这是一个高频问题,甚至说必问问题都不为过,至少我经历的面试是这样。装饰器也是Python开发中经常要使用到的功能,熟练掌握装饰器会让你的代码更加简洁,思路也更加广阔。 好了,废话不多说,直接开锤。
什么是装饰器
所谓的装饰器,其实就是通过装饰器函数,来修改原函数的一些功能,使得原函数不需要修改。理解起来好像就是装饰器本身可以增强其他函数的功能,一个函数平平无奇,没有关系,有我装饰器,定能让你变的闪闪发光。 这么说可能也有点绕,举个栗子,比如你是一个男程序员,然后你带上假发,穿上女装,再画个妆,那你可能会成为一个女装大佬,那么假发、女装、化妆就是装饰器,你还是你,但是因为有了这些装饰器,你成为了一个女装大佬。enmmmm...,大概就是这样。
下面看一个世界上最简单的函数,放宽心,绝对不是Hello World!
def hello():
print("你好,装饰器!")
哈哈,我没骗人吧,不信的小伙伴可以私下里运行一下哈哈哈。
那如果我想让hello()
函数再实现个其他的功能,比如打印个执行函数时的时间。
有人说可以在hello()
函数里加上打印时间的代码不就好了。
import datetime
def hello():
print("当前时间:", datetime.datetime.now())
print("你好,装饰器!")
那么问题来了,如果还有hello1()
、hello2()
、hello3()
等等大量的函数,那也一个一个的加上吗,那就会造成大量的重复代码,为了避免重复代码,我们可以重新定义一个函数,专门用来打印当前时间,打印完时间再执行真正的业务代码。
def print_time(func):
print("当前时间:", datetime.datetime.now())
func()
def hello():
print("你好,装饰器!")
print_time(hello)
这样逻辑上虽然没多大毛病,但是这样的话,我们每次都要传递一个函数给print_time()
函数,这种做法破坏了原有的代码逻辑结构,原来执行业务逻辑,我们只需要调用hello()
函数,而现在我们必须要调用print_time()
函数才行,那有没有更好的方式,当然,那就是装饰器。
装饰器的实现
下面就是装饰器方式的实现:
import datetime
def my_decorator(func):
def wrapper():
print("当前时间:", datetime.datetime.now())
func()
return wrapper
@my_decorator
def hello():
print("你好,装饰器!")
hello()
运行结果:
当前时间: 2021-07-31 11:31:46.630720
你好,装饰器!
Process finished with exit code 0
注:@符号是装饰器的语法糖,在定义函数的时候使用,可以避免再一次赋值操作。
此处@my_decorator
,相当于
hello = my_decorator(hello)
hello()
很显然,这样不但可以实现需要的功能,代码也更加简洁。
解释一下,当运行最后的hello()
函数时,调用过程是这样的:
- 先是执行
hello = my_decorator(hello)
,此时变量hello
指向的是my_decorator()
。 my_decorator(func)
中传参是hello
,返回的wrapper
,而wrapper
又会调用到原函数hello()
。- 所以,先执行
wrapper()
函数里的代码,然后才执行hello()
函数里的。
上述代码里的my_decorator()
就是一个装饰器,它“增强”了hello()
的功能,但是并没有去真正的改变hello()
函数的内部实现。
带参数的装饰器
上面的只是一个非常简单的装饰器,但是实际场景中,很多函数都是要带有参数的,比如hello(name)
等,那要怎么实现呢,其实也很简单,直接在对应装饰器的wrapper()
上,加上对应的参数就可以了。
def my_decorator(func):
def wrapper(*args, **kwargs):
print("当前时间:", datetime.datetime.now())
func(*args, **kwargs)
return wrapper
@my_decorator
def hello(name):
print("你好,{}".format(name))
hello("tigeriaf")
输出:
当前时间: 2021-07-31 13:43:59.192468
你好,tigeriaf
Process finished with exit code 0
注:在python里,*args
和**kwargs
表示接受任意数量和类型的参数,想要了解的可以看我之前的文章python中的*args和**kwargs用法解读
自定义参数
上面的装饰器都是接收外来的参数,其实装饰器有自己的参数。 比如,加个参数来控制下业务函数执行的次数:
def my_decorator_outer(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
print("当前时间:", datetime.datetime.now())
for i in range(num):
func(*args, **kwargs)
return wrapper
return my_decorator
@my_decorator_outer(3)
def hello(name):
print("你好,{}".format(name))
hello("tigeriaf")
注意:这里是三层嵌套的函数,最外层my_decorator_outer
函数的参数num控制的是次数。
运行后会输出以下信息:
当前时间: 2021-07-31 14:37:28.190077
你好,tigeriaf
你好,tigeriaf
你好,tigeriaf
Process finished with exit code 0
内置装饰器@functools.wrap
现在我们多做一步探索,我们来打印一下下面例子中的hello()函数的元信息:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("当前时间:", datetime.datetime.now())
func(*args, **kwargs)
return wrapper
@my_decorator
def hello(name):
print("你好,{}".format(name))
# hello("tigeriaf")
print(hello.__name__) # 打印hello函数的元信息
结果为:
wrapper
Process finished with exit code 0
这说明,它不再是以前的那个hello()
函数,而是被wrapper()
函数取代了,函数名等函数属性会发生改变,当然这对结果不会产生什么影响,但是如果我们需要用到元函数信息,那要怎么保留它呢?哎,这时候可以用内置装饰器@functools.wrap
来实现。
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("当前时间:", datetime.datetime.now())
func(*args, **kwargs)
return wrapper
@my_decorator
def hello(name):
print("你好,{}".format(name))
# hello("tigeriaf")
print(hello.__name__)
运行下看看:
hello
Process finished with exit code 0
显然,我们在写一个装饰器的时候,最好加上@functools.wrap
,它能保留原有函数的名称和函数属性。
类装饰器
刚刚我们接触的装饰器是函数来完成,由于Python的灵活性,我们也可以使用类来实现一个装饰器。
类能实现装饰器的功能,是由于当我们调用一个类的实例时,就调用到它的__call__()
方法。
那接下来我们把之前的例子改成类装饰器来实现:
class MyDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("当前时间:", datetime.datetime.now())
return self.func(*args, **kwargs)
@MyDecorator
def hello(name):
print("你好,{}".format(name))
hello("tigeriaf")
运行结果:
当前时间: 2021-07-31 15:06:15.400093
你好,tigeriaf
Process finished with exit code 0
多层装饰器的执行顺序
既然装饰器可以增强函数的功能,那如果有多个装饰器,我都想要怎么办?
其实,只要把需要用的装饰器都加上去就好了。
@decorator1
@decorator2
@decorator3
def hello():
...
但是要注意的是,这里的执行顺序,会从上到下依次执行,可以来看下:
def my_decorator1(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("装饰器1,当前时间:", datetime.datetime.now())
func(*args, **kwargs)
return wrapper
def my_decorator2(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("装饰器2,当前时间:", datetime.datetime.now())
func(*args, **kwargs)
return wrapper
def my_decorator3(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("装饰器3,当前时间:", datetime.datetime.now())
func(*args, **kwargs)
return wrapper
@my_decorator1
@my_decorator2
@my_decorator2
def hello(name):
print("你好,{}".format(name))
hello("tigeriaf")
运行结果:
装饰器1,当前时间: 2021-07-31 15:13:23.824342
装饰器2,当前时间: 2021-07-31 15:13:23.824342
装饰器2,当前时间: 2021-07-31 15:13:23.824342
你好,tigeriaf
Process finished with exit code 0
总结
好了,关于装饰器就先说道这里,其实装饰器经常用于有切面需求的场景,比如:性能测试、添加日志、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与业务函数功能本身无关的重复代码,这样可以使整体更简洁。何乐而不为呢。
最后,感谢女朋友在工作和生活中的包容、理解与支持 !