装饰器是Python的一大重点和难点,也是后续学习对类进行装饰以及元类的基础,其允许Python实现装饰器设计模式,可以在不改变某函数结构和调用方式基础之上,为其增加新的功能,并且最大化复用新的功能
装饰器在面向切面编程的场景中很有用,比如为函数增加日志记录、登录校验、权限校验等,我们可以将这些功能写成一个装饰器,然后直接应用到相应需要改功能的函数中即可,可以保证对原代码和函数零侵入。
下面会详细展开讲解装饰器写法、实际应用即实现原理。
限于篇幅,本篇文章只会讲述对函数的装饰器写法和原理,至于对类的装饰器,会在后续文章中,与元类一起讲解(因为元类本质也符合装饰器特点)
装饰器本质是一个闭包函数,所以在讲解装饰器之前,需要先理解Python闭包函数的概念,闭包函数有以下几个特点:
比如下面的闭包函数,实现了计数counter功能
#外层函数
def outter_func():
#定义外层函数的局部变量
a=0
#定义一个内层函数
def inner_func():
#声明下在内层函数内,a变量指向到外层函数的a
nonlocal a
a+=1
print(a)
#返回内层函数
return inner_func
counter=outter_func()
counter() #输出为1
counter() #输出为2
闭包函数之所以可以实现让外层函数内的变量常驻内存,关键就是其定义了个内层函数,并通过内层函数访问外层函数的变量,并最后由外层函数将内层函数返回出去并赋值给另外一个变量。
此时因为内层函数被赋值给一个变量,其内存空间不会被释放,而内层函数又在其函数体内引用了外层函数的变量,导致该变量的内存也不会被回收。
一般情况下,当一个函数运行完毕后,其内存空间即被回收释放,下次再调用该函数的时候,会重新完整运行一次被调用函数,但闭包函数主要是利用Python的内存回收机制,实现了闭包的效果。
装饰器自身是一个返回可调用对象的可调用对象,装饰器和闭包整体代码结构相对比较相似,装饰器的特点主要如下:
比如下面的是一个初级版本的装饰器
def mydec(func):
def dec(*args):
"""
your decorator code
"""
return func(*args)
return dec
#下面是一个被装饰函数
def myfunc(*args):
pass
#最后,写下面的代码,相当于调用装饰器函数,传入被装饰函数
#然后把内部具体实现装饰器功能的函数return并再次赋值给该被装饰器函数
myfunc=mydec(myfunc)
上面的装饰器,虽然功能层面可以正常工作,但是有以下问题:
所以,最终装饰器的解决方案为如下:
from functools import wraps
def mydec(func):
#该行代码主要作用是将func的元信息,复制给dec函数
@wraps(func)
def dec(*args):
"""
your decorator code
"""
return func(*args)
return dec
#下面是一个被装饰函数,然后使用上面的装饰器进行装饰
@mydec
def myfunc(*args):
pass
以上,主要做了以下调整:
此时,myfunc的函数元信息也没发生任何变化,比如myfunc.__name__还是myfunc,而不是dec,至此,完美
至于wraps函数具体实现原理,下面会讲
此时,可以将装饰器和闭包进行对比,可以发现
在以上代码中,可以看到@wraps(func),可以理解为,这个本质也是一个装饰器,该装饰器的作用就是将func函数的元信息复制给被装饰的函数,元信息包括__name__,__doc__等信息,以下代码模拟了该装饰器的功能实现,主要是帮助大家深入理解装饰器的语法
#以下定义自己的wraps装饰器,亲测有效,本质就是一个带参的装饰器,核心功能是复制函数元信息
def mywraps(fwrap):
def out_dec(func):
def in_dec(**args):
return func(**args)
meta_info=['__module__', '__name__', '__qualname__', '__doc__', '__annotations__']
#以下代码,主要是逐个获取fwrap函数的以上元信息的值,并复制给dec函数,这样在最终使用该装饰器装饰函数的时候,看到的函数元信息不再是返回的dec的,而是fwrap的
for meta in meta_info:
setattr(in_dec,meta,getattr(fwrap,meta))
#逐个获取fwrap函数的元信息,并复制到dec函数上
return in_dec
return out_dec
下面,会对日常可能用到的装饰器,具体实现和语法结构进行展开介绍,因为在实际场景中,可能会存在以下场景:
以下,会针对以上场景,分别展开如何满足对应装饰器需求
下面是不带参装饰器写法,本质就是两层函数嵌套
from functools import
def deco(func):
@wraps(func):
def in_dec(*args):
'''
your decorator code
'''
return func(*args)
return in_dec
@deco
def myfunc():
pass
带参数装饰器,即可以向装饰器传参,以为装饰器赋予个性化定制的特点,根据传入参数不同,装饰器表现行为不同等等,此时,需要再加一层函数嵌套,最外层函数主要实现传参的功能,然后返回第二层函数,此时就又退化成了两层嵌套,即不带参装饰器
from functools import wraps
def dec_with_args(*args):
def dec(func):
@wraps(func)
def in_dec(*args):
"""
your decorator code
"""
return func(*args)
return in_dec
return dec
@dec_with_args((*args)
#此处,可以认为先调用了一次外层函数,返回了dec函数,然后再将myfunc函数传给dec函数
#1、dec_with_args(*args)返回dec函数
#2、此时,变为@dec,等同于myfunc=dec(myfunc),又回到了不带参的装饰器
def myfunc():
pass
被装饰函数带参情况最简单,其实本质就是装饰器内层函数,只要能接受同样个数的参数即可,如下:
from functools import wraps
def dec(func):
@wraps(func)
def in_dec(a,b):
"""
your decorator code
"""
return func(a,b)
return in_dec
@dec
def myfunc(a,b):
pass
装饰器还可以通过类来实现,其实主要是利用类的以下特点来变相实现函数装饰器功能:
下面举例,用类实现带参装饰器,可以观察下不同
from functools import wraps
#定义一个装饰器名称的类
class with_para_decorator:
#在类的__init__函数内接受装饰器参数,并赋值给类的实例参数,这样可以让其他函数随时使用
#当然,如果装饰器没有参数,此处不转a,b即可,相当于类无参实例化
def __init__(self,a,b):
self.a=a
self.b=b
#在类的__call__函数内接受被装饰函数,并具体定义装饰器
def __call__(self,func):
@wraps(func)
def wrap_function(arg1,arg2):
print('装饰带参数的函数,函数传的参数为:{0}, {1}'.format(arg1,arg2))
print('带参数的装饰器,装饰器传的参数为:{0}, {1}'.format(self.a,self.b))
return func(arg1,arg2)
return wrap_function
#使用装饰器
@with_para_decorator(1,2)
def need_decorate(a,b):
pass
need_decorate(4,5)
以上代码具体原理解析如下:
可以对某个被装饰函数应用多个装饰器,如下所示
@dec1
@dec2
@dec3
def myfunc():
pass
此时:
实际应用中,可能希望将代码运行现场数据和情况,记录到日志文件内,并且一般希望记录的内容也基本相同,此时,可以实现一个记录日志的装饰器,然后将该装饰器应用到希望增加日志的函数上即可。
下面实现一个可以指定记录日志文件地址的装饰器
from functools import wraps
import datetime
#定义一个可以记录函数调用时间、传入参数的装饰器
def dec(log_file):
#接受log_file参数,供具体实现装饰器功能的函数使用
def dec_print_info(func):
@wraps(func)
def print_info(a,b):
#该函数,因为本身没有定义log_file变量,python此时会逐层往上找寻,找到了最外层传入的log_file变量,然后使用
with open (log_file,'a+') as f:
hour=datetime.datetime.now().hour
minute=datetime.datetime.now().minute
second=datetime.datetime.now().second
f.write('调用时间:{}点{}分{}秒,传入的参数为:{}和{}\n'.format(hour,minute,second,a,b))
return func(a,b)
return print_info
return dec_print_info
#加装饰器时,传入日志文件地址
@dec('/users/yanweichao/downloads/log2.txt')
def myfunc(a,b):
return a+b
myfunc(12,59)
from functools import wraps
import requests as req
def log_auth(func):
@wraps(func)
def logged(*args,**kwargs):
response=req.post('API',headers=headers,payloads=payloads)
#如果没有登录,则调用登录流程
if not response:
login()
return func(*args,**kwargs)
return logged
@log_auth
def purchase(sku_lists,uid):
'''
your purchase code here
'''
return
当然,装饰器还有其他很多的应用,基本原则是:
此时,均可考虑用装饰器实现。