在看装饰器之前,首先我们必须简单了解一下闭包的。闭包可以简单的描述为:内层函数在运行中需要调用外层函数中的变量,并且外层函数的返回值为内层函数。例如:
def outer():
a = 12
def inner():
print(a, inner.__closure__[1].cell_contents)
return inner
outer()()
inner函数调用了outer函数中的a变量(还有inner函数),并且outer函数的返回值为inner函数(注意,此处只返回函数名,而不要加括号,那表示运行这个函数,函数名表示这个函数的引用),而outer()()则表示调用了outer()函数后,由于返回值是inner函数的引用,再加一个括号表示调用inner函数。__closure__为内层函数调用了外层函数的哪些变量或者内层函数,__closure__[1].cell_contents表示调用的第二个变量(或函数)的值。
闭包我们就简单了解到这里,下面看一下装饰器。装饰器的本质就是一个闭包函数,首先我们先用非装饰器的方式对一个函数进行封装,例如:
def outer(func_name):
print('1')
def inner():
print('3')
func_name()
print('4')
print('2')
return inner
def textfunc():
print("我被执行了")
outer(textfunc)()
我们简单分析一下这段代码。outer(textfunc)将调用outer函数,然后打印1,打印2,返回inner函数的引用,接着第二个括号,即outer(textfunc)()将调用inner函数,inner函数打印3,调用func_name(即我们的textfunc函数),打印4,结束调用。而这其实就是装饰器的实质,上面的代码改用装饰器可以写为下面的样子:
def outer(func_name):
print('1')
def inner():
print('3')
func_name()
print('4')
print('2')
return inner
@outer #使用装饰器
def textfunc():
print("我被执行了")
textfunc() #调用函数
从代码中我们可以发现,装饰器的实质就是用一个闭包函数对一个函数进行“包装”,使我们再调用该函数时不需要将其作为某个函数的参数带入,在一定程度上提高了开发的效率。当然,我们还是要简单分析一下那句@outer的意思。在@outer后面定义一个函数,就表示,该函数在以后的调用中,都是作为outer函数的参数进行调用,我们的textfunc()与前面一段代码的outer(textfunc)()起到的作用是完全一样的。
到了这里我们可能会思考三个问题。1,如果被调用的函数(textfunc)有参数怎么办;2,如果想用多个闭包函数同时包装一个函数怎么办;3,如果装饰器(outer)有参数怎么办。我们进行逐一的解决。
首先是第一个问题,被装饰的函数有参数怎么办。这里我们只需要将textfunc和inner做一些修改就可以,例如:
def outer(func_name):
print('1')
def inner(a, b): ###############
print('3')
func_name(a, b) ###########
print('4')
print('2')
return inner
@outer
def textfunc(a, b): #################
print("我被执行了", a, b)
textfunc(12, 34) ###################
后面跟######的地方为与上一段代码不同的地方。说到这个地方,必须先强调一件事。鄙人刚刚学习python一周,对python的解释器以及底层的运行原理没有进行过研究,后面所述均为自己的理解和猜想,如果是高手以及想研究python底层的人士,请恕我能力不足。如果是有钻研精神的新手,也可以再搜索一下其他的博文,剩下的小伙伴就听我意淫一下吧。
理解或者看懂这段代码不难,但是问题在于,为什么参数要放在inner中而不是outer中。我的感觉是,放在outer中不方便管理,容易出错,或者降低运行效率。如果我们将参数放到outer中,将装饰器变成函数调用的形式就变成outer(textfunc, a, b)(),这看上去没有什么问题,但是python不是c++这种将每一个参数的类型都固定的,如果我们这里犯贱将outer的调用变为outer(a, b, textfunc),或者我的outer函数设计为outer(a, b, func_name)而进行正常的调用会怎么样呢?答案是:出错,因为a并不是一个函数。如果解释器再费尽心思的判断每一个参数的类型,效率自然会降低,所以将参数放在outer中并不划算,但是如果变成outer(textfunc)(a, b)这种形式,则可以避免这种问题。虽然我们在调用被装饰的函数时感受不到这些,只需要像调用正常函数一样去调用就可以,但是我们在设计装饰器的时候要避免出现错误。
问题又来了,如果我们的装饰器需要传入参数,需要怎么处理呢?答案是,多一层包装。还是同样的道理,如果将参数直接放在outer中,我们的调用很可能会出错,所以我们必须将outer外面再加上一层包装,才能让参数传到装饰器中,具体的形式类似于下面的代码:
def func1(arg1):
print(arg1, " 1")
def func2(func):
print(arg1, " 2")
def func3(arg2):
func(arg2)
print(arg1, " 3")
print('2')
return func3
print('1')
return func2
@func1(100)
def func4(arg2):
print("i love c++", arg2)
这里的func2于func3可以分别对于前面代码中的outer于inner,我们在func2的外面再进行一次包装,就可以将参数传入装饰器。做一个简单的分析:我们先将装饰器变成函数调用的形式,fun1(arg1)(func4)(arg2)如果前面我写的内容都表达清楚了,那这句函数的调用应该也是可以比较透彻的分析的。首先为了防止函数和参数都在同一个参数列表中出现错误,且装饰器需要在调用函数前传入参数,所以在最外层,即func1中需要先传入相应的参数,接着将func4作为参数传给func2,接着func2将调用func3,并且为func3传入arg2,然后调用func4(即func3中的func),执行完毕,退出。这也是前面我们列出的三个问题中的第三个问题。当然,我们可以将arg1和arg2都变成*args以及**kwargs
下面就是第二个问题,如何进行多次包装。这个过程有点点啰嗦,我们先看一段代码:
def first(func):
print('i\'m first')
def inner():
print('i\'m first.inner')
func()
return inner
def second(func):
print('i\'m second')
def inner():
print('i\'m second.inner')
func()
return inner
@first
@second
def textfunc():
print("i'm func")
pass
textfunc()
这段代码的运行结果为:
i'm second
i'm first
i'm first.inner
i'm second.inner
i'm func
其实从这个例子我们也可以更好的去想象装饰器的底层实现。首先,second 对 textfunc 进行了包装,接着first 对 second 又进行了一次包装,当然我们还可以添加更多的装饰器。我们将这段代码变成函数调用的形式应该是这个样子:first(second(textfunc))() 。结构很诡异有没有。我们慢慢来拆分。首先我们要执行的是first(),这个是毫无疑问的,在执行first()这个函数之前,我们要将参数second(textfunc) 传入first。注意,此时的second函数由于传入了参数textfunc,所以已经执行了,此时输出i'm second 并且返回了second函数的inner函数的引用,所以我们可以将first(second(textfunc))变成 first(second.inner(textfunc))。这个时候first函数将开始执行,输出i'm first,然后返回first.inner,此时的first.inner中的代码完全展开应该为:
print('i\'m first.inner')
#func()#这个func实际上是first传入的参数second.inner(textfunc)
print('i\'m second.inner)
#func()#这里的func是second的参数textfunc
textfunc()
由这个顺序我们就可以比较清晰的了解两层装饰器究竟是怎样执行的。如果代码没有解释清楚,就继续往下看。我们的first的inner的函数体为输出 i'm first.inner,执行first传入的func函数。而我们first函数传入的这个函数,实际上是second(textfunc)执行后的结果,也就是second函数中的inner。所以我们把func展开,就变成了second.inner中的
print('i\'m second.inner) func()
而这里的func恰恰又是textfunc函数,所以我们就分析出了最后的运行结果。简而言之,就是first函数对 second(textfunc)进行了一次包装。如果我们有三个装饰器,则这三个装饰器的实际执行结果应该为first(second(third(textfunc)))()。具体的分析和前面的分析相同。
如果看到这里没有其他的疑问,我会感觉有点可惜,因为你缺乏了一些探索精神,需要培养一下。我会产生第四个问题,如果多层装饰器带有参数且被包装的函数也有参数,展开以后应该是什么样子的呢?首先,程序应该为这个样子的
def first(arg1):
print('i\'m first', arg1)
def outer(func):
print('i\'m first.outer')
def inner(arg3):
print('i\'m first.inner')
func(arg3)
return inner
return outer
def second(arg2):
print('i\'m second', arg2)
def outer(func):
print('i\'m second.outer')
def inner(arg3):
print('i\'m second.inner')
func(arg3)
return inner
return outer
#@first(1)
#@second(2)
def textfunc(arg3):
print("i'm func", arg3)
pass
first(1)(second(2)(textfunc))(3)
执行结果为:
i'm first 1
i'm second 2
i'm second.outer
i'm first.outer
i'm first.inner
i'm second.inner
i'm func 3
而根据前面的分析,它的函数调用形式应该为这样的:
first(1)(second(2)(textfunc))(3)
如果对前面的三个问题都可以理解,那这段分析应该也是不成问题的。
当然,如果有熟悉python底层实现或者了解装饰器官方解释的,还望多多指教~
。