python装饰器全解--包含实现原理及应用场景

装饰器是Python的一大重点和难点,也是后续学习对类进行装饰以及元类的基础,其允许Python实现装饰器设计模式,可以在不改变某函数结构和调用方式基础之上,为其增加新的功能,并且最大化复用新的功能

装饰器在面向切面编程的场景中很有用,比如为函数增加日志记录、登录校验、权限校验等,我们可以将这些功能写成一个装饰器,然后直接应用到相应需要改功能的函数中即可,可以保证对原代码和函数零侵入。

下面会详细展开讲解装饰器写法、实际应用即实现原理。

限于篇幅,本篇文章只会讲述对函数的装饰器写法和原理,至于对类的装饰器,会在后续文章中,与元类一起讲解(因为元类本质也符合装饰器特点)

一、装饰器实现原理

1.1 闭包函数讲解

装饰器本质是一个闭包函数,所以在讲解装饰器之前,需要先理解Python闭包函数的概念,闭包函数有以下几个特点:

  1. 闭包函数是函数的嵌套,函数内还有函数,即外层函数嵌套一个内层函数
  2. 在外层函数定义局部变量,在内层函数通过nonlocal引用,并实现指定功能,比如计数
  3. 最后外层函数return内层函数
  4. 主要作用:可以变相实现私有变量的功能,即用内层函数访问外层函数内的变量,并让外层函数内的变量常驻内存

比如下面的闭包函数,实现了计数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的内存回收机制,实现了闭包的效果。

1.2 装饰器特点和实现原理

装饰器自身是一个返回可调用对象的可调用对象,装饰器和闭包整体代码结构相对比较相似,装饰器的特点主要如下:

  1. 装饰器也是函数的嵌套结构,可能还会存在三层嵌套
  2. 外层函数就是装饰器函数,接受的参数是一个函数,一般是传入被装饰函数
  3. 内层函数实现具体的装饰器功能,比如日志记录、登录鉴权等
  4. 内层函数return一次传入的函数调用
  5. 外层函数return内层函数
  6. 如果是多层嵌套,最内层是实现具体装饰器功能的函数,并负责调用一次传入的函数,最外1层函数return第2层函数,依次类推,不过一般最多就是三层函数嵌套

比如下面的是一个初级版本的装饰器

def mydec(func):
    def dec(*args):
        """
        your decorator code
        """
        return func(*args)
    return dec

#下面是一个被装饰函数
def myfunc(*args):
    pass

#最后,写下面的代码,相当于调用装饰器函数,传入被装饰函数
#然后把内部具体实现装饰器功能的函数return并再次赋值给该被装饰器函数
myfunc=mydec(myfunc)

上面的装饰器,虽然功能层面可以正常工作,但是有以下问题:

  1. 最后一个myfunc=mydec(myfunc)语句,改变了myfunc的指向和元信息,因为本质是由调用myfunc变成了调用dec函数,此时,myfunc.__name__其实是dec
  2. 对现有代码略有侵入,且不美观,不那么python

所以,最终装饰器的解决方案为如下:

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

以上,主要做了以下调整:

  1. 引入wraps函数工具,对内层实现装饰器功能的函数进行装饰,主要是将传入的被装饰函数元信息复制给具体实现装饰器功能函数
  2. 对被装饰函数进行装饰时,直接在该被装饰函数上面添加 @mydec即可,该语句功能等同于  myfunc=mydec(myfunc),只是为了美观,进行了语法简化,让更具有python味儿

此时,myfunc的函数元信息也没发生任何变化,比如myfunc.__name__还是myfunc,而不是dec,至此,完美

至于wraps函数具体实现原理,下面会讲

1.3 装饰器和闭包对比

此时,可以将装饰器和闭包进行对比,可以发现

  1. 装饰器本质,或者从代码结构上说,可以认为就是一个闭包
  2. 都是利用了Python函数本身既可以嵌套、又可以接受函数参数、又可以返回一个函数的特点。
  3. 只是闭包偏向于利用内存常驻的特点,而装饰器偏向于利用函数返回函数的特点

1.4 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

二、装饰器写法

下面,会对日常可能用到的装饰器,具体实现和语法结构进行展开介绍,因为在实际场景中,可能会存在以下场景:

  1. 装饰器可能因为功能实现需要,还需要能接受参数
  2. 被装饰函数可能有参或无参

以下,会针对以上场景,分别展开如何满足对应装饰器需求

2.1 函数装饰器

2.1.1 不带参装饰器

下面是不带参装饰器写法,本质就是两层函数嵌套

  1. 外层函数是装饰器名称,接受一个函数入参(被装饰函数)
  2. 内层函数具体实现装饰器功能,同时return一下外层函数接受的函数参数调用(被装饰函数),同时使用@wraps装饰下该函数,实现函数元信息复制
  3. 外层函数最后return下内层函数
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

2.1.2 带参装饰器

带参数装饰器,即可以向装饰器传参,以为装饰器赋予个性化定制的特点,根据传入参数不同,装饰器表现行为不同等等,此时,需要再加一层函数嵌套,最外层函数主要实现传参的功能,然后返回第二层函数,此时就又退化成了两层嵌套,即不带参装饰器

  1. 有三层函数嵌套,最外层函数主要是接受装饰器的参数,实现闭包,常驻内存,供其内层函数使用,然后return 第二层函数
  2. 第二层函数与不带参情况下,基本一样
  3. 第三层函数还是最终实现装饰器功能
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

2.1.3 被装饰函数带参

被装饰函数带参情况最简单,其实本质就是装饰器内层函数,只要能接受同样个数的参数即可,如下:

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

2.2 类装饰器

装饰器还可以通过类来实现,其实主要是利用类的以下特点来变相实现函数装饰器功能:

  1. 函数调用语语法f()等同于类的实例化,即调用类的__init__函数创建对象
  2. 对象的调用obj()等同于运行对象的__call__魔法函数
  3. 通过类实现装饰器,可以避免函数装饰器超过2层的嵌套情况,因为如果有三层的话,最外层函数可以认为是在调用类的__init__函数,这样可以让代码更易读和维护
  4. 本质,只要实现类的__init__和__call__魔法函数,并在__init__函数内接受装饰器参数,在__call__函数内实现具体装饰器结构即可

下面举例,用类实现带参装饰器,可以观察下不同

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)		   

以上代码具体原理解析如下:

  1. @with_para_decorator(1,2),因为是类的名称,相当于使用(1,2)参数创建并返回该类的一个实例对象,比如是obj
  2. 此时,语法变为@obj,相当于need_decorate=obj(need_decorate),此时会调用obj.__call__魔法函数,而我们在该魔法函数具体实现了装饰器功能
  3. 可以看到,其本质的运行原理,和函数装饰器没区别,只是将三层函数嵌套,变成了一个__init__函数和__call__函数的两层嵌套
  4. 对比下来,可以看到,类装饰器,代码更加直观

2.3 装饰器嵌套

可以对某个被装饰函数应用多个装饰器,如下所示

@dec1
@dec2
@dec3
def myfunc():
    pass

此时:

  1. 可以对某个被装饰函数,增加多个功能
  2. 装饰器生效顺序,从上到下,即dec1>dec2>dec3

三、装饰器实际应用举例

3.1 日志记录

实际应用中,可能希望将代码运行现场数据和情况,记录到日志文件内,并且一般希望记录的内容也基本相同,此时,可以实现一个记录日志的装饰器,然后将该装饰器应用到希望增加日志的函数上即可。

下面实现一个可以指定记录日志文件地址的装饰器

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)

3.2 登录校验

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 

3.3 其他

当然,装饰器还有其他很多的应用,基本原则是:

  1. 希望为存量函数和代码增加新功能,同时又不希望对原有函数进行调整(包括代码和调用方式等)
  2. 较多其他函数均需要该新增功能,希望一处实现,多处复用,提升代码可读性和可维护性

此时,均可考虑用装饰器实现。

猜你喜欢

转载自blog.csdn.net/yifengchaoran/article/details/113854472