浅谈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的表达更加的美观,优雅~~~

以上~~~

posted @ 2018-03-27 09:36  木lin木  阅读(155)  评论(0编辑  收藏  举报