Python 进阶:函数装饰器

一、前言

本小节主要梳理函数装饰的用法,循序渐进,逐层增加条件,加大复杂度和难度。

环境说明:Python 3.6、windows11 64位

二、函数装饰器

装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。它不修改原来的函数,还给函数增加新的功能,而是使得调用原函数的时候附加一些功能。

【简单应用】

装饰器一般是一个闭包函数,只是函数接收的参数是一个函数。如下代码,My_Decorator()就是一个装饰器,直接调用即可。
以下装饰器是一个比较简单的应用,装饰器 My_Decorator() 给被装饰函数Introduction()加上了一个新的功能【print('print My_Decorator.inner')】,同时又不影响Introduction()原有功能。

# 定义一个装饰器
def My_Decorator(func): # 传入被装饰的函数
    def inner():
        print('print My_Decorator.inner') 
        func()          # 调用func() 函数,保证不修改被修饰函数,且正常执行
    return inner        # 返回inner 函数

def Introduction():
    name = 'Xindata'
    print('My name is %s!'%name) # 在装饰器函数中调用的时候才执行

# 调用装饰器函数
aa = My_Decorator(Introduction)  # 将inner 赋值变量给aa
print(type(aa)) # 结果为:,此时aa 是一个函数变量
print(aa)       # 结果为:.inner at 0x000001F6DBBAB790>
aa()            # 调用aa() 相当于调用inner() 函数,执行结果有2个,一个是【print My_Decorator.inner】是print('print My_Decorator.inner')的结果;一个是【My name is XIndata!】,调用func(),即调用Introduction() 函数的结果。

【带返回值】

上文的被装饰函数Introduction()没有返回值,所以可以通过上文代码实现,但是如果有返回值的怎么办呢,方法也很简单,在inner()函数中调用func()前加上return即可获取到被修饰函数的返回值,具体如下:

# 定义一个装饰器
def My_Decorator(func): 
    def inner():
        print('print My_Decorator.inner') 
        return func()   # 加上return,将调用func() 函数得到的结果进行返回
    return inner        

def Introduction():
    name = 'Xindata'
    print('My name is %s!'%name) 
    return name         # 将名字返回

aa = My_Decorator(Introduction)  
print(aa)       # 结果为:.inner at 0x000001F6DAEA33A0>
my_name = aa()  # 调用aa() 时,同样会打印两个结果:【print My_Decorator.inner】和【My name is XIndata!】
                # 同时,调用func()(即Introduction())返回的变量name会作为返回值赋值给my_name
print(my_name)  # 结果为:Xindata

【带参数】

以上代码是被装饰函数Introduction()没有参数的情况下适配的情况,当Introduction()有参数时怎么办呢?下面来看看这个升级版本。
这个代码的参数值'Xindata'的传递路径比较长,先是通过inner()name参数,然后传递到Introduction()name参数,然后通过Introduction()return 进行返回,再由inner()return 进行返回,最后再赋值给my_name
Python 进阶:函数装饰器_第1张图片

# 定义一个装饰器
def My_Decorator(func): 
    def inner(name):
        print('print My_Decorator.inner') 
        return func(name)   # 加上return,将调用func() 函数得到的结果进行返回
    return inner        

def Introduction(name):
#     name = 'Xindata'
    print('My name is %s!'%name) 
    return name

aa = My_Decorator(Introduction)
print(aa)       # 结果为:.inner at 0x000001F6DB00FDC0>
my_name = aa('Xindata') # 调用aa() 即(inner())时,给name参数传递参数值'Xindata'。调用aa() 函数打印的两个结果同上一个代码。
                        # 同时,name的参数值'Xindata'值传递给func()(即Introduction())的参数name,并通过Introduction()的return 返回,然后再由inner() 的return 返回,再赋值给my_name
print(my_name)

【兼容各种参数】

以上代码,支持了单一参数的传递,但是作为装饰器,由于其特性,经常会给不同的被修饰函数进行装饰,添加新的功能,而目前的这个装饰器,仅支持单一参数的情况,不能支持多参数,甚至是不同类型的参数,如关键字参数等,为此,需要进一步升级,只需要做一个简单的修改即可,下面看看新的装饰器版本。

# 定义一个装饰器
def My_Decorator(func): 
    def inner(*args,**kw):      # 将name参数改为*args,**kw,便可支持各种类型的参数
        print('print My_Decorator.inner') 
        return func(*args,**kw) # 将name参数改为*args,**kw,便可支持各种类型的参数
    return inner        

def Introduction(name):
#     name = 'Xindata'
    print('My name is %s!'%name) 
    return name

aa = My_Decorator(Introduction)
print(aa)
my_name = aa('Xindata')
print(my_name)

# 如果不需要一些过程,也可以进行链式调用,一步到位
my_name = My_Decorator(Introduction)('Xindata')
print(my_name)

@语法糖】

其实上面讲的都是装饰器比较早期的调用方式,后来官方支持了一个更加时髦的写法:@语法糖。把Python之禅的优雅简洁表现得淋漓尽致。下面一起来看看。

# 定义一个装饰器
def My_Decorator(func): 
    def inner(*args,**kw):
        print('print My_Decorator.inner') 
        return func(*args,**kw)
    return inner        

# 在Introduction()函数定义前,用@My_Decorator 声明用装饰器My_Decorator()装饰Introduction()函数
@My_Decorator
def Introduction(name):
#     name = 'Xindata'
    print('My name is %s!'%name) 
    return name

my_name = Introduction('Xindata') # 调用方式也发生了一些变化,直接调用被修饰的函数即可
print(my_name)

【装饰器带参】

如果需要对不同的场景使用不同的 Introduction(),可以对装饰器再加一层函数,然后在@声明时传递参数,如果是早期的调用方式,则可采用三层链式调用。具体代码如下。

# 定义一个装饰器
def Occasion(params):
    def My_Decorator(func): 
        def inner(*args,**kw):
            print('【%s】print My_Decorator.inner'% params) 
            return func(*args,**kw)
        return inner        
    return My_Decorator

@Occasion('Daily')       # 传递参数给params
def Introduction(name):
    print('My name is %s!'%name) 
    return name

my_name = Introduction('Xindata')
print(my_name)

# 早期调用方式,如下
# 定义一个装饰器
def Occasion(params):
    def My_Decorator(func): 
        def inner(*args,**kw):
            print('【%s】print My_Decorator.inner'% params) 
            return func(*args,**kw)
        return inner        
    return My_Decorator

def Introduction(name):
    print('My name is %s!'%name) 
    return name

my_name = Occasion('Daily')(Introduction)('Xindata') # 三层调用
print(my_name)

# 两串代码结果都是
# 【daily】print My_Decorator.inner
# My name is Xindata!
# Xindata

【多个装饰器】

单个装饰器,基本讲完了,那多个装饰器呢?
假设有d1d1两个装饰器,同时装饰f1函数,早期的方法则是val = d1(d2(f));使用@则是把@d1@d2两个装饰器按顺序应用到f函数上。抽象代码如下:

@d1
@d2
def f():
	print('f')
val = f()
    
# 等同于
def f():
	print('f')
val = d1(d2(f))

那上面的例子,再加一个装饰看看效果:

# 定义一个装饰器
def Occasion(params):
    def My_Decorator(func): 
        def inner(*args,**kw):
            print('【%s】print My_Decorator.inner'% params) 
            return func(*args,**kw)
        return inner        
    return My_Decorator

def Current_Decorator(func): 
    def inner(*args,**kw):
        print('print Current_Decorator.inner') 
        return func(*args,**kw)
    return inner        

@Occasion('Daily')         
@Current_Decorator         # 新增装饰器
def Introduction(name):
    print('My name is %s!'%name) 
    return name

my_name = Introduction('Xindata')
print(my_name)


## 早期调用方式
# 定义一个装饰器
def Occasion(params):
    def My_Decorator(func): 
        def inner(*args,**kw):
            print('【%s】print My_Decorator.inner'% params) 
            return func(*args,**kw)
        return inner        
    return My_Decorator

def Current_Decorator(func): 
    def inner(*args,**kw):
        print('print Current_Decorator.inner') 
        return func(*args,**kw)
    return inner   

def Introduction(name):
    print('My name is %s!'%name) 
    return name

my_name = Occasion('Daily')(Current_Decorator(Introduction))('Xindata') # 加一层调用
print(my_name)

# 两串代码结果都是
# 【Daily】print My_Decorator.inner
# print Current_Decorator.inner
# My name is Xindata!
# Xindata

看完上面这两个代码,你可能犯迷糊了,看打印结果前两行是先执行Occasion('Daily')装饰器,再执行Current_Decorator装饰器。但是看早期调用方式,又不太像,因为Occasion('Daily')执行完这一层,就跑到 Current_Decorator(Introduction)这一层,最后再传递 'Xindata'调用Introduction()。没错!事实就是第一个装饰器执行一半就跑到第二个装饰器执行,如果有第三个装饰器,还会跑到第三个装饰器,执行完第三个装饰器之后,再执行第二个装饰器,再执行第一个装饰器,从外到里再到外,这有点像递归的思想。
为了更好看这个效果,下面抽象一个代码在看看这个效果:

def Activity(func):
    def wrapper1(*args,**kw):
        print('活动开始了!  内层函数第1个执行,func参数值:%s'% func.__name__)
        func(*args,**kw)
        print('活动结束了!')
    print('外层函数第3个执行:Activity,返回wrapper1,开始执行内层函数,从wrapper1开始')
    return wrapper1

def Timing(func): 
    def wrapper2(*args,**kw):
        print('介绍计时开始!内层函数第2个执行,func参数值:%s'% func.__name__)
        func(*args,**kw)
        print('介绍计时结束!')
    print('外层函数第2个执行:Timing,返回wrapper2 给 @Activity')
    return wrapper2     

def Speech(func):
    def wrapper3(*args,**kw):
        print('开始演讲。    内层函数第3个执行,func参数值:%s'% func.__name__)
        func(*args,**kw)
        print('结束演讲。')
    print('外层函数第1个执行:Speech,返回wrapper3 给 @Timing')
    return wrapper3

@Activity
@Timing
@Speech
def Introduction(name):
    print('My name is %s!'%name) 

Introduction('Xindata')

# 运行结果为:
# 外层函数第1个执行:Speech,返回wrapper3 给 @Timing
# 外层函数第2个执行:Timing,返回wrapper2 给 @Activity
# 外层函数第3个执行:Activity,返回wrapper1,开始执行内层函数,从wrapper1开始
# 活动开始了!  内层函数第1个执行,func参数值:wrapper2
# 介绍计时开始!内层函数第2个执行,func参数值:wrapper3
# 开始演讲。    内层函数第3个执行,func参数值:Introduction
# My name is Xindata!
# 结束演讲。
# 介绍计时结束!
# 活动结束了!

执行顺序如下:
Python 进阶:函数装饰器_第2张图片
外层函数起装饰效果,内层函数执行新功能。函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。也就是说,外层函数即使不调用函数**Introduction()**也会打印出来。(可以注释掉Introduction('Xindata')运行看看效果。)
所以越靠近被装饰函数的装饰器先装饰后执行,而离得远的相反。可以记为就近原则装饰,执行则相反(类似递归思想)

二、变量作用域

【加上变量】

装饰函数一般都是一个闭包函数,这里单独拿闭包函数来做说明,重新找一个比较简单的例子辅助理解。
当在闭包函数的外层函数定义一个变量时,内层函数可以直接调用。

def func():
    x = 0
    def inner():
        # 调用x
        return x
    return inner

f = func()
print(f()) # 结果为0

如果在内层函数要对外层的变量进行修改,这时便会报错,这个和普通函数调用全局变量又要修改它的场景差不多,只不过在那个场景下,可以通过在函数内部对变量使用global声明为全局变量解决该问题。
那闭包函数是不是也有相关的关键字来声明将x作为闭包函数的“全局变量”呢?还真有,它就是nonlocal
Python 进阶:函数装饰器_第3张图片
使用 nonlocal x声明x不是当前内层函数的局部变量。
注意:这里的nonlocal x并不是声明x为全局变量。

def func():
    x = 0
    def inner():
        nonlocal x
        x += 1
        return x
    return inner

f = func()
print(f()) # 结果为1

【多次调用】

当多次调用f()的时候,我们会发现结果是递增的,但是打印x时则报错变量未定义。那么x只是闭包函数内的局部变量,并不是全局变量,但是它是怎么把值进行传递的呢?因为这个x是一个特殊的变量,有一个技术术语,叫自由变量(free variable),指未在本地作用域中绑定的变量。闭包函数会保留自由变量的绑定,使得调用函数时可以继续使用只有变量。

def func():
    x = 0
    def inner():
        nonlocal x
        x += 1
        return x
    return inner

f = func()
print(f()) # 结果为1
print(f()) # 结果为2
print(f()) # 结果为3
print(x)   # 报错:NameError: name 'x' is not defined

可以通过一些属性查看相关的变量和值
f.__closure__中的各个元素对应于f.__code__.co_freevars中的一个名称。这些元素是 cell 对象,有个 cell_contents 属性,保存着真正的值。

f.__code__.co_varnames  # 查看当前函数的局部变量
f.__code__.co_freevars  # 查看当前函数的自由变量
f.__closure__[0].cell_contents # 查看自由变量的值


- End -

你可能感兴趣的:(python,开发语言,装饰器)