浅谈Python装饰器
对于Python装饰器,之前一直理解比较模糊,很多博客上讲的东西看似很清楚,但总觉得有点照搬解释,没有自己的体会和理解(感觉更像是很多作者强行吸收了这些概念,而不是基于自身的理解)。例如以下是一个在网上搜到的例子。
def log(func):
def wrapper(*args, **kw):
print 'call %s():' % func.__name__
return func(*args, **kw)
return wrapper
@log
def now():
print '2013-12-25'
然后执行 now()
的结果如下:
>>> now()
call now():
2013-12-25
客观的说,这已经是解释的比较清楚的例子了,但还是让人不解,例如
- 到底什么是“装饰器”?是log函数,@log这一行代码,还是别的什么?
- “装饰器”有啥要求?
- 是不是log一定要包含子函数?子函数的名字是不是一定叫wrapper?参数有什么限制?
- “装饰器”在调用的时候是什么顺序?
为了理清这些疑问,我决定从最简单的开始,一点一点往上加,看会如何。过程比较繁琐,可以快速看下去。
最简单的代码
如下是我写的第一段代码。
def log():
print "function log"
def hello():
print "hello"
hello()
这段代码相信稍微接触过python语言的都能懂,输出就是
hello
直接加入装饰器
然后直接在hello()前面加入@log。也就是hello函数变成
@log
def hello():
print "hello"
这是运行报错,如下:
Traceback (most recent call last):
File "C:\Users\Administrator\Desktop\zsq.py", line 8, in <module>
@log
TypeError: log() takes no arguments (1 given)
这段错误是说log有一个参数传入,但log的定义中没有参数。参考文章开头的函数,可以明白,装饰器函数至少需要传入一个参数,而且这个参数也就是函数名(如本例子中的hello)。
修改装饰器函数
因为log需要传入函数名hello
,所以修改装饰器函数log,如下:
def log(func):
print "function log"
运行之后还是报错,如下:
function log
Traceback (most recent call last):
File "C:\Users\Administrator\Desktop\zsq.py", line 13, in <module>
hello()
TypeError: 'NoneType' object is not callable
首先 fucntion log
被打印出来了,说明log函数被执行到了,但接下来hello
没有打印,所以执行出错。错误说 NoneType 不能被调用,说明程序尝试去执行这样的代码 NoneType()
, 导致出错,对照文章开头的函数,可以发现是因为log没有返回值,所以返回值为 NoneType ,在尝试调用 hello()
时才会出现这个错误。
log添加返回值
根据上面的错误,我们为 log 添加一个返回值,让log函数返回值为 hello 函数。这里再把完整的代码写一遍:
def log(func):
print "function log"
return func
@log
def hello():
print "hello"
hello()
这是执行没有报错,结果如下:
function log
hello
换句话说,上面这段代码就是装饰器的 “最小系统”,必须包括的有:
- 装饰器的修饰符(如 “@log”), 放在函数前指定某个函数(如hello)使用装饰器函数(如log);
- 装饰器函数(log())在定义时至少有一个参数,这个参数就是函数名(如hello)
- 装饰器函数需要返回一个函数,这个函数会被重新赋值为原函数名(如hello)
前两点好理解,但第三点其实还是有点绕。所以我们考虑不使用装饰器修饰符(“@log”),而有替换的方法达到同样的效果。由于我们知道实际上是 hello 作为 log 的参数,并调用了 log 。因此等效的代码如下:
def log(func):
print "function log"
return func
## @log #这里的装饰器修饰符注释掉
def hello():
print "hello"
new_hello = log(hello)
new_hello()
这样的代码输出的结果与用装饰器一样。所以也就是说,之所以log需要返回一个函数,就是为了将这个函数赋值为 hello 。这里为了区分用了new_hello, 但实际上如果用装饰器的话,是直接将log返回的函数直接又赋值给了 hello,即
hello=log(hello)
hello()
到这里基本上我们理解了装饰器实际上做了什么,简单的说就是:
把(被装饰函数的)函数名当做参数传入另一个函数(即装饰器函数),再把装饰器函数的 返回值 赋值给 函数名。
通过这样一个操作,虽然函数名没变,但函数名对应的函数却变了,也就是说 "hello" 这个函数名经过装饰后,本质上已经变成了 “new_hello” 了,只是名字没有变,还叫 “hello” 而已。
那么下一个问题是,为什么要在装饰器函数里面再套一个函数呢?
再执行一遍?
考虑如下一段代码:
def log(func):
print "function log"
return func
@log
def hello():
print "hello"
hello()
hello()
这段代码输出如下:
function log
hello
hello
不知道是否有人觉得奇怪,为什么只有一个 function log
打印出来,难道不是应该有两个? 只有一个 function log
是因为替换的操作并非是在函数调用时执行的,而是在加载hello的实现时执行的。可以理解为紧跟在hello的定义之后加入了替换,即实际的替换代码是:
def log(func):
print "function log"
return func
def hello():
print "hello"
new_hello = log(hello)
new_hello()
new_hello()
可以把 new_hello = log(hello)
和 hello 的定义理解为一个整体,这里为了区分依旧用了 new_hello,实际上“new_hello” 的函数名是 “hello”。 上述代码的输出跟使用装饰器完全一致。
log函数再嵌套一个函数
这里就可以发现,如果 log 返回的函数跟原来的函数一模一样的话,那么相当于这个装饰器log什么都没做,只是在hello函数加载时执行了一遍log函数而已,后续再次调用 new_hello 时,依旧调用的是 hello 函数。无法达到每次调用hello,都执行一段特定代码的目的。 为了达到上述目的,就有了在log里面再定义一个函数,然后在这个函数里面执行特定的代码。例如:
def log(func):
def _log():
print "go into %s"%func.__name__
func()
print "go outof %s"%func.__name__
return _log
@log
def hello():
print "hello"
hello()
hello()
上述代码中,log函数里面嵌套了一个 _log 函数,并且返回不再是 func, 而是 _log,这就意味着new_hello 不再是 hello, 而是 _log。从而做到以后每次调用 hello, 实际上调用的是 _log。因此上述代码的输出是:
go into hello
hello
go outof hello
go into hello
hello
go outof hello
如果把代码替换成不带装饰器的,应该如下:
def log(func):
def _log():
print "go into %s"%func.__name__
func()
print "go outof %s"%func.__name__
return _log
def hello():
print "hello"
new_hello = log(hello)
new_hello()
new_hello()
上述代码与使用装饰器的代码效果一致,这也就说明了装饰器的语法(如@log)实际上不是重点,重点在于把 函数名 作为参数,再返回,即使不用@, 用赋值的方式 new_hello = log(hello)
也能达到同样的效果。所以有些博客上说,装饰器只是一个“语法糖”。只是让python的表达更加的美观,优雅~~~
以上~~~