本小节主要梳理函数装饰的用法,循序渐进,逐层增加条件,加大复杂度和难度。
环境说明: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
。
# 定义一个装饰器
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
单个装饰器,基本讲完了,那多个装饰器呢?
假设有d1
和d1
两个装饰器,同时装饰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!
# 结束演讲。
# 介绍计时结束!
# 活动结束了!
执行顺序如下:
外层函数起装饰效果,内层函数执行新功能。函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。也就是说,外层函数即使不调用函数**Introduction()**
也会打印出来。(可以注释掉Introduction('Xindata')
运行看看效果。)
所以越靠近被装饰函数的装饰器先装饰后执行,而离得远的相反。可以记为就近原则装饰,执行则相反(类似递归思想)。
装饰函数一般都是一个闭包函数,这里单独拿闭包函数来做说明,重新找一个比较简单的例子辅助理解。
当在闭包函数的外层函数定义一个变量时,内层函数可以直接调用。
def func():
x = 0
def inner():
# 调用x
return x
return inner
f = func()
print(f()) # 结果为0
如果在内层函数要对外层的变量进行修改,这时便会报错,这个和普通函数调用全局变量又要修改它的场景差不多,只不过在那个场景下,可以通过在函数内部对变量使用global
声明为全局变量解决该问题。
那闭包函数是不是也有相关的关键字来声明将x
作为闭包函数的“全局变量”呢?还真有,它就是nonlocal
。
使用 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 -