Python中的装饰器用法详解

  1. 云栖社区>
  2. 博客>
  3. 正文

Python中的装饰器用法详解

shadowcat 2016-04-07 23:07:00 浏览1130
展开阅读全文

来源:http://www.jb51.net/article/59867.htm

来源:http://blog.csdn.net/mdl13412/article/details/22608283


这篇文章主要介绍了Python中的装饰器用法,以实例形式详细的分析了Python中的装饰器的使用技巧及相关注意事项,具有一定参考借鉴价值,需要的朋友可以参考下

本文实例讲述了Python中的装饰器用法。分享给大家供大家参考。具体分析如下:

这里还是先由stackoverflow上面的一个问题引起吧,如果使用如下的代码:

@makebold
@makeitalic
def say():
   return "Hello"

打印出如下的输出:

<b><i>Hello<i></b>

你会怎么做?最后给出的答案是:

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped
 
def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped
 
@makebold
@makeitalic
def hello():
    return "hello world"
 
print hello() ## 返回 <b><i>hello world</i></b>

现在我们来看看如何从一些最基础的方式来理解Python的装饰器。

装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

1.1. 需求是怎么来的?

装饰器的定义很是抽象,我们来看一个小例子。

def foo():
    print 'in foo()'
foo()

这是一个很无聊的函数没错。但是突然有一个更无聊的人,我们称呼他为B君,说我想看看执行这个函数用了多长时间,好吧,那么我们可以这样做:
import time
def foo():
    start = time.clock()
    print 'in foo()'
    end = time.clock()
    print 'used:', end - start
 
foo()

很好,功能看起来无懈可击。可是蛋疼的B君此刻突然不想看这个函数了,他对另一个叫foo2的函数产生了更浓厚的兴趣。

怎么办呢?如果把以上新增加的代码复制到foo2里,这就犯了大忌了~复制什么的难道不是最讨厌了么!而且,如果B君继续看了其他的函数呢?

1.2. 以不变应万变,是变也

还记得吗,函数在Python中是一等公民,那么我们可以考虑重新定义一个函数timeit,将foo的引用传递给他,然后在timeit中调用foo并进行计时,这样,我们就达到了不改动foo定义的目的,而且,不论B君看了多少个函数,我们都不用去修改函数定义了!

import time
 def foo():
    print 'in foo()'
 
def timeit(func):
    start = time.clock()
    func()
    end =time.clock()
    print 'used:', end - start
 
timeit(foo)

看起来逻辑上并没有问题,一切都很美好并且运作正常!……等等,我们似乎修改了调用部分的代码。原本我们是这样调用的:foo(),修改以后变成了:timeit(foo)。这样的话,如果foo在N处都被调用了,你就不得不去修改这N处的代码。或者更极端的,考虑其中某处调用的代码无法修改这个情况,比如:这个函数是你交给别人使用的。

1.3. 最大限度地少改动!

既然如此,我们就来想想办法不修改调用的代码;如果不修改调用代码,也就意味着调用foo()需要产生调用timeit(foo)的效果。我们可以想到将timeit赋值给foo,但是timeit似乎带有一个参数……想办法把参数统一吧!如果timeit(foo)不是直接产生调用效果,而是返回一个与foo参数列表一致的函数的话……就很好办了,将timeit(foo)的返回值赋值给foo,然后,调用foo()的代码完全不用修改!

#-*- coding: UTF-8 -*-
import time
 
def foo():
    print 'in foo()'
 
# 定义一个计时器,传入一个,并返回另一个附加了计时功能的方法
def timeit(func):
    
    # 定义一个内嵌的包装函数,给传入的函数加上计时功能的包装
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
    
    # 将包装后的函数返回
    return wrapper
 
foo = timeit(foo)
foo()

这样,一个简易的计时器就做好了!我们只需要在定义foo以后调用foo之前,加上foo = timeit(foo),就可以达到计时的目的,这也就是装饰器的概念,看起来像是foo被timeit装饰了。在在这个例子中,函数进入和退出时需要计时,这被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。与传统编程习惯的从上往下执行方式相比较而言,像是在函数执行的流程中横向地插入了一段逻辑。在特定的业务领域里,能减少大量重复代码。面向切面编程还有相当多的术语,这里就不多做介绍,感兴趣的话可以去找找相关的资料。

这个例子仅用于演示,并没有考虑foo带有参数和有返回值的情况,完善它的重任就交给你了 :)

上面这段代码看起来似乎已经不能再精简了,Python于是提供了一个语法糖来降低字符输入量。

import time
 
def timeit(func):
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
    return wrapper
 
@timeit
def foo():
    print 'in foo()'
 
foo()

重点关注第11行的@timeit,在定义上加上这一行与另外写foo = timeit(foo)完全等价千万不要以为@有另外的魔力。除了字符输入少了一些,还有一个额外的好处:这样看上去更有装饰器的感觉。

要理解python的装饰器,我们首先必须明白在Python中函数也是被视为对象(在python中一切都是对象)。在python中一切都是对象这一点很重要。先看一个例子:

def shout(word="yes") :
    return word.capitalize()+" !"
 
print shout()
# 输出 : 'Yes !'
 
# 作为一个对象,你可以把函数赋给任何其他对象变量
 
scream = shout
 
# 注意我们没有使用圆括号,因为我们不是在调用函数
# 我们把函数shout赋给scream,也就是说你可以通过scream调用shout
 
print scream()
# 输出 : 'Yes !'
 
# 还有,你可以删除旧的名字shout,但是你仍然可以通过scream来访问该函数
 
del shout
try :
    print shout()
except NameError, e :
    print e
    #输出 : "name 'shout' is not defined"
 
print scream()
# 输出 : 'Yes !'

我们暂且把这个话题放旁边,我们先看看python另外一个很有意思的属性:可以在函数中定义函数:
def talk() :
 
    # 你可以在talk中定义另外一个函数
    def whisper(word="yes") :
        return word.lower()+"...";
 
    # ... 并且立马使用它
 
    print whisper()
 
# 你每次调用'talk',定义在talk里面的whisper同样也会被调用
talk()
# 输出 :
# yes...
 
# 但是"whisper" 不会单独存在:
 
try :
    print whisper()
except NameError, e :
    print e

    #输出 : "name 'whisper' is not defined"*

函数引用

从以上两个例子我们可以得出,函数既然作为一个对象,因此:

1. 其可以被赋给其他变量

2. 其可以被定义在另外一个函数内

这也就是说,函数可以返回一个函数,看下面的例子:

def getTalk(type="shout") :
 
    # 我们定义另外一个函数
    def shout(word="yes") :
        return word.capitalize()+" !"
 
    def whisper(word="yes") :
        return word.lower()+"...";
 
    # 然后我们返回其中一个
    if type == "shout" :
        # 我们没有使用(),因为我们不是在调用该函数
        # 我们是在返回该函数
        return shout
    else :
        return whisper
 
# 然后怎么使用呢 ?
 
# 把该函数赋予某个变量
talk = getTalk()    
 
# 这里你可以看到talk其实是一个函数对象:
print talk
#输出 : <function shout at 0xb7ea817c>
 
# 该对象由函数返回的其中一个对象:
print talk()
 
# 或者你可以直接如下调用 :
print getTalk("whisper")()
#输出 : yes...

还有,既然可以返回一个函数,我们可以把它作为参数传递给函数:
def doSomethingBefore(func) :
    print "I do something before then I call the function you gave me"
    print func()
 
doSomethingBefore(scream)
#输出 :
#I do something before then I call the function you gave me
#Yes !

这里你已经足够能理解装饰器了,其他它可被视为封装器。也就是说,它能够让你在装饰前后执行代码而无须改变函数本身内容。

手工装饰

那么如何进行手动装饰呢?

# 装饰器是一个函数,而其参数为另外一个函数
def my_shiny_new_decorator(a_function_to_decorate) :
 
    # 在内部定义了另外一个函数:一个封装器。
    # 这个函数将原始函数进行封装,所以你可以在它之前或者之后执行一些代码
    def the_wrapper_around_the_original_function() :
 
        # 放一些你希望在真正函数执行前的一些代码
        print "Before the function runs"
 
        # 执行原始函数
        a_function_to_decorate()
 
        # 放一些你希望在原始函数执行后的一些代码
        print "After the function runs"
 
    #在此刻,"a_function_to_decrorate"还没有被执行,我们返回了创建的封装函数
    #封装器包含了函数以及其前后执行的代码,其已经准备完毕
    return the_wrapper_around_the_original_function
 
# 现在想象下,你创建了一个你永远也不远再次接触的函数
def a_stand_alone_function() :
    print "I am a stand alone function, don't you dare modify me"
 
a_stand_alone_function()
#输出: I am a stand alone function, don't you dare modify me
 
# 好了,你可以封装它实现行为的扩展。可以简单的把它丢给装饰器
# 装饰器将动态地把它和你要的代码封装起来,并且返回一个新的可用的函数。
a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
#输出 :
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs

现在你也许要求当每次调用a_stand_alone_function时,实际调用却是a_stand_alone_function_decorated。实现也很简单,可以用my_shiny_new_decorator来给a_stand_alone_function重新赋值。
复制代码 代码如下:
a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function()
#输出 :
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs
 
# And guess what, that's EXACTLY what decorators do !

装饰器揭秘

前面的例子,我们可以使用装饰器的语法:

复制代码 代码如下:
@my_shiny_new_decorator
def another_stand_alone_function() :
    print "Leave me alone"
 
another_stand_alone_function()
#输出 :
#Before the function runs
#Leave me alone
#After the function runs

当然你也可以累积装饰:
复制代码 代码如下:
def bread(func) :
    def wrapper() :
        print "</''''''\>"
        func()
        print "<\______/>"
    return wrapper
 
def ingredients(func) :
    def wrapper() :
        print "#tomatoes#"
        func()
        print "~salad~"
    return wrapper
 
def sandwich(food="--ham--") :
    print food
 
sandwich()
#输出 : --ham--
sandwich = bread(ingredients(sandwich))
sandwich()
#outputs :
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

使用python装饰器语法:
复制代码 代码如下:
@bread
@ingredients
def sandwich(food="--ham--") :
    print food
 
sandwich()
#输出 :
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

装饰器的顺序很重要,需要注意
复制代码 代码如下:
@ingredients
@bread
def strange_sandwich(food="--ham--") :
    print food
 
strange_sandwich()
#输出 :
##tomatoes#
#</''''''\>
# --ham--
#<\______/>
# ~salad~

最后回答前面提到的问题:
复制代码 代码如下:
# 装饰器makebold用于转换为粗体
def makebold(fn):
    # 结果返回该函数
    def wrapper():
        # 插入一些执行前后的代码
        return "<b>" + fn() + "</b>"
    return wrapper
 
# 装饰器makeitalic用于转换为斜体
def makeitalic(fn):
    # 结果返回该函数
    def wrapper():
        # 插入一些执行前后的代码
        return "<i>" + fn() + "</i>"
    return wrapper
 
@makebold
@makeitalic
def say():
    return "hello"
 
print say()
#输出: <b><i>hello</i></b>
 
# 等同于
def say():
    return "hello"
say = makebold(makeitalic(say))
 
print say()
#输出: <b><i>hello</i></b>

内置的装饰器

内置的装饰器有三个,分别是staticmethod、classmethod和property,作用分别是把类中定义的实例方法变成静态方法、类方法和类属性。由于模块里可以定义函数,所以静态方法和类方法的用处并不是太多,除非你想要完全的面向对象编程。而属性也不是不可或缺的,Java没有属性也一样活得很滋润。从我个人的Python经验来看,我没有使用过property,使用staticmethod和classmethod的频率也非常低。

复制代码 代码如下:
class Rabbit(object):
    
    def __init__(self, name):
        self._name = name
    
    @staticmethod
    def newRabbit(name):
        return Rabbit(name)
    
    @classmethod
    def newRabbit2(cls):
        return Rabbit('')
    
    @property
    def name(self):
        return self._name

这里定义的属性是一个只读属性,如果需要可写,则需要再定义一个setter:
复制代码 代码如下:
@name.setter
def name(self, name):
    self._name = name

functools模块

functools模块提供了两个装饰器。这个模块是Python 2.5后新增的,一般来说大家用的应该都高于这个版本。但我平时的工作环境是2.4 T-T

2.3.1. wraps(wrapped[, assigned][, updated]):
这是一个很有用的装饰器。看过前一篇反射的朋友应该知道,函数是有几个特殊属性比如函数名,在被装饰后,上例中的函数名foo会变成包装函数的名字wrapper,如果你希望使用反射,可能会导致意外的结果。这个装饰器可以解决这个问题,它能将装饰过的函数的特殊属性保留。

复制代码 代码如下:
import time
import functools
 
def timeit(func):
    @functools.wraps(func)
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
    return wrapper
 
@timeit
def foo():
    print 'in foo()'
 
foo()
print foo.__name__

首先注意第5行,如果注释这一行,foo.__name__将是'wrapper'。另外相信你也注意到了,这个装饰器竟然带有一个参数。实际上,他还有另外两个可选的参数,assigned中的属性名将使用赋值的方式替换,而updated中的属性名将使用update的方式合并,你可以通过查看functools的源代码获得它们的默认值。对于这个装饰器,相当于wrapper = functools.wraps(func)(wrapper)。

2.3.2. total_ordering(cls):
这个装饰器在特定的场合有一定用处,但是它是在Python 2.7后新增的。它的作用是为实现了至少__lt__、__le__、__gt__、__ge__其中一个的类加上其他的比较方法,这是一个类装饰器。如果觉得不好理解,不妨仔细看看这个装饰器的源代码:

复制代码 代码如下:
def total_ordering(cls):
      """Class decorator that fills in missing ordering methods"""
      convert = {
          '__lt__': [('__gt__', lambda self, other: other < self),
                     ('__le__', lambda self, other: not other < self),
                     ('__ge__', lambda self, other: not self < other)],
          '__le__': [('__ge__', lambda self, other: other <= self),
                     ('__lt__', lambda self, other: not other <= self),
                     ('__gt__', lambda self, other: not self <= other)],
          '__gt__': [('__lt__', lambda self, other: other > self),
                     ('__ge__', lambda self, other: not other > self),
                     ('__le__', lambda self, other: not self > other)],
          '__ge__': [('__le__', lambda self, other: other >= self),
                     ('__gt__', lambda self, other: not other >= self),
                     ('__lt__', lambda self, other: not self >= other)]
      }
      roots = set(dir(cls)) & set(convert)
      if not roots:
          raise ValueError('must define at least one ordering operation: < > <= >=')
      root = max(roots)       # prefer __lt__ to __le__ to __gt__ to __ge__
      for opname, opfunc in convert[root]:
          if opname not in roots:
              opfunc.__name__ = opname
              opfunc.__doc__ = getattr(int, opname).__doc__
              setattr(cls, opname, opfunc)
      return cls


目录(?)[+]

浅谈Python装饰器

By 马冬亮(凝霜  Loki)

一个人的战争(http://blog.csdn.net/MDL13412)

前置知识

一级对象

Python将一切视为 objec t的子类,即一切都是对象,因此函数可以像变量一样被指向和传递,我们来看下面的例子:

foo.def foo():
    pass
    
print issubclass(foo.__class__, object)__class__, object)  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. True  
上述代码说明了Python 中的函数是 object 的子类,下面让我们看函数被当作参数传递时的效果:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def foo(func):  
  2.     func()  
  3.   
  4. def bar():  
  5.     print "bar"  
  6.   
  7. foo(bar)  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. bar  

Python中的namespace

Python中通过提供 namespace 来实现重名函数/方法、变量等信息的识别,其一共有三种 namespace,分别为:

  • local namespace: 作用范围为当前函数或者类方法
  • global namespace: 作用范围为当前模块
  • build-in namespace: 作用范围为所有模块

当函数/方法、变量等信息发生重名时,Python会按照 local namespace -> global namespace -> build-in namespace的顺序搜索用户所需元素,并且以第一个找到此元素的 namespace 为准。
下面以系统的 build-in 函数 str 为例进行说明:

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def str(s):  
  2.     print "global str()"  
  3.   
  4. def foo():  
  5.     def str(s):  
  6.         print "closure str()"  
  7.     str("dummy")  
  8.   
  9. def bar():  
  10.     str("dummy")  
  11.   
  12. foo()  
  13. bar()  
首先定义三个 global namespace 的函数 strfoo 和bar,然后在 foo 函数中定义一个内嵌的 local namespace 的函数str,然后在函数 foo 和bar 中分别调用 str("dummy"),其运行结果如下所示:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. closure str()  
  2. global str()  
通过编码实验,我们可以看到:
  • foo 中调用 str 函数时,首先搜索 local namespace,并且成功找到了所需的函数,停止搜索,使用此namespace 中的定义
  • bar 中调用 str 函数时,首先搜索 local namespace,但是没有找到str 方法的定义,因此继续搜索 global namespace,并成功找到了str 的定义,停止搜索,并使用此定义
下面我们使用Python内置的 `ocals() 和 globals() 函数查看不同 namespace 中的元素定义:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var = "var in global"  
  2.   
  3. def fun():  
  4.     var = "var in fun"  
  5.     print "fun: " + str(locals())  
  6.   
  7. print "globals: " + str(globals())  
  8. fun()  
运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. globals: {'__builtins__': <module '__builtin__' (built-in)>, '__file__''a.py''__package__'None'fun': <function fun at 0x7f2ca74f66e0>, 'var''var in global''__name__''__main__''__doc__'None}  
  2. fun: {'var''var in fun'}  
通过运行结果,我们看到了 fun 定义了 local namespace 的变量var,在 global namespace 有一个全局的 var 变量,那么当在global namespace 中直接访问 var 变量的时候,将会得到 var = "var in global" 的定义,而在fun 函数的 local namespace 中访问 var 变量,则会得到fun 私有的 var = "var in fun" 定义。

*args and **kwargs

  • *args: 把所有的参数按出现顺序打包成一个 list
  • **kwargs:把所有 key-value 形式的参数打包成一个 dict

下面给出一个 *args 的例子:

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. params_list = (12)  
  2. params_tupple = (12)  
  3.   
  4. def add(x, y):  
  5.     print x + y  
  6.   
  7. add(*params_list)  
  8. add(*params_tupple)  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. 3  
  2. 3  
**kwargs 的例子:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. params = {  
  2.     'x'1,  
  3.     'y'2  
  4. }  
  5.   
  6. def add(x, y):  
  7.     print x + y  
  8.   
  9. add(**params)  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. 3  

闭包

[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. <a target="_blank" href="http://zh.wikipedia.org/zh-cn/%E9%97%AD%E5%8C%85_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)">闭包在维基百科上的定义</a>如下: 在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。  
下面给出一个使用闭包实现的logger factory的例子:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def logger_facroty(prefix="", with_prefix=True):  
  2.     if with_prefix:  
  3.         def logger(msg):  
  4.             print prefix + msg  
  5.         return logger  
  6.     else:  
  7.         def logger(msg):  
  8.             print msg  
  9.         return logger  
  10.   
  11. logger_with_prefix = logger_facroty("Prefix: ")  
  12. logger_without_prefix = logger_facroty(with_prefix=False)  
  13. logger_with_prefix("msg")  
  14. logger_without_prefix("msg")  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. Prefix: msg  
  2. msg  
在上面这个闭包的例子中,prefix 变量时所谓的自由变量,其在 return logger 执行完毕后,便脱离了创建它的环境logger_factory,但因为其被 logger_factory 中定义的 logger 函数所引用,其生命周期将至少和 logger 函数相同。这样,在 logger 中就可以引用到logger_factory 作用域内的变量 prefix
将闭包与 namespace 结合起来:

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var = "var in global"  
  2.   
  3. def fun_outer():  
  4.     var = "var in fun_outer"  
  5.     unused_var = "this var is not used in fun_inner"  
  6.     print "fun_outer: " + var  
  7.     print "fun_outer: " + str(locals())  
  8.     print "fun_outer: " + str(id(var))  
  9.   
  10.     def fun_inner():  
  11.         print "fun_inner: " + var  
  12.         print "fun_inner: " + str(locals())  
  13.         print "fun_inner: " + str(id(var))  
  14.   
  15.     return fun_inner  
  16.   
  17. fun_outer()()  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. fun_outer: var in fun_outer  
  2. fun_outer: {'var''var in fun_outer''unused_var''this var is not used in fun_inner'}  
  3. fun_outer: 140228141915584  
  4. fun_inner: var in fun_outer  
  5. fun_inner: {'var''var in fun_outer'}  
  6. fun_inner: 140228141915584  
在这个例子中,当 fun_outer 被定义时,其内部的定义的 fun_inner 函数对 print "fun_inner: " + var中所引用的 var 变量进行搜索,发现第一个被搜索到的 var 定义在 fun_outer 的 local namespace 中,因此使用此定义,通过 print "fun_outer: " + str(id(var)) 和 print "fun_inner: " + str(id(var)),当var 超出 fun_outer 的作用域后,依然存活,而 fun_outer 中的unused_var 变量由于没有被 fun_inner 所引用,因此会被GC

探索装饰器

定义

[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. <a target="_blank" href="http://en.wikipedia.org/wiki/Python_syntax_and_semantics#Decorators">点击打开装饰器在维基百科上的定义链接</a>如下: A decorator is any callable Python object that is used to modify a function, method or class definition.   

基本语法

语法糖

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. @bar  
  2. def foo():  
  3.     print "foo"  
其等价于:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def foo():  
  2.     print "foo"  
  3. foo = bar(foo)  

无参数装饰器

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def foo(func):  
  2.     print 'decorator foo'  
  3.     return func  
  4.  
  5. @foo  
  6. def bar():  
  7.     print 'bar'  
  8.   
  9. bar()  
foo 函数被用作装饰器,其本身接收一个函数对象作为参数,然后做一些工作后,返回接收的参数,供外界调用。

注意: 时刻牢记 @foo 只是一个语法糖,其本质是 foo = bar(foo)

带参数装饰器

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. import time  
  2.   
  3. def function_performance_statistics(trace_this=True):  
  4.     if trace_this:  
  5.        def performace_statistics_delegate(func):  
  6.             def counter(*args, **kwargs):  
  7.                 start = time.clock()  
  8.                 func(*args, **kwargs)  
  9.                 end =time.clock()  
  10.                 print 'used time: %d' % (end - start, )  
  11.             return counter  
  12.     else:  
  13.        def performace_statistics_delegate(func):  
  14.             return func  
  15.     return performace_statistics_delegate  
  16.  
  17. @function_performance_statistics(True)  
  18. def add(x, y):  
  19.     time.sleep(3)  
  20.     print 'add result: %d' % (x + y,)  
  21.  
  22. @function_performance_statistics(False)  
  23. def mul(x, y=1):  
  24.     print 'mul result: %d' % (x * y,)  
  25.   
  26. add(11)  
  27. mul(10)  
上述代码想要实现一个性能分析器,并接收一个参数,来控制性能分析器是否生效,其运行效果如下所示:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. add result: 2  
  2. used time: 0  
  3. mul result: 10  
上述代码中装饰器的调用等价于:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. add = function_performance_statistics(True)(add(11))  
  2. mul = function_performance_statistics(False)(mul(10))  

类的装饰器

类的装饰器不常用,因此只简单介绍。

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def bar(dummy):  
  2.     print 'bar'  
  3.   
  4. def inject(cls):  
  5.     cls.bar = bar  
  6.     return cls  
  7.  
  8. @inject  
  9. class Foo(object):  
  10.     pass  
  11.   
  12. foo = Foo()  
  13. foo.bar()  
上述代码的 inject 装饰器为类动态的添加一个 bar 方法,因为类在调用非静态方法的时候会传进一个self 指针,因此 bar 的第一个参数我们简单的忽略即可,其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. bar  

类装饰器

类装饰器相比函数装饰器,具有灵活度大,高内聚、封装性等优点。其实现起来主要是靠类内部的 __call__ 方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法,下面时一个实例:

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. class Foo(object):  
  2.     def __init__(self, func):  
  3.         super(Foo, self).__init__()  
  4.         self._func = func  
  5.   
  6.     def __call__(self):  
  7.         print 'class decorator'  
  8.         self._func()  
  9.  
  10. @Foo  
  11. def bar():  
  12.     print 'bar'  
  13.   
  14. bar()  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. class decorator  
  2. bar  

内置装饰器

Python中内置的装饰器有三个: staticmethodclassmethod 和property

staticmethod 是类静态方法,其跟成员方法的区别是没有 self 指针,并且可以在类不进行实例化的情况下调用,下面是一个实例,对比静态方法和成员方法

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. class Foo(object):  
  2.     @staticmethod  
  3.     def statc_method(msg):  
  4.         print msg  
  5.   
  6.     def member_method(self, msg):  
  7.         print msg  
  8.   
  9. foo = Foo()  
  10. foo.member_method('some msg')  
  11. foo.statc_method('some msg')  
  12. Foo.statc_method('some msg')  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. some msg  
  2. some msg  
  3. some msg  
classmethod 与成员方法的区别在于所接收的第一个参数不是 self 类实例的指针,而是当前类的具体类型,下面是一个实例:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. class Foo(object):  
  2.     @classmethod  
  3.     def class_method(cls):  
  4.         print repr(cls)  
  5.   
  6.     def member_method(self):  
  7.         print repr(self)  
  8.   
  9. foo = Foo()  
  10. foo.class_method()  
  11. foo.member_method()  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. <class '__main__.Foo'>  
  2. <__main__.Foo object at 0x10a611c50>  
property 是属性的意思,即可以通过通过类实例直接访问的信息,下面是具体的例子:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. class Foo(object):  
  2.     def __init__(self, var):  
  3.         super(Foo, self).__init__()  
  4.         self._var = var  
  5.  
  6.     @property  
  7.     def var(self):  
  8.         return self._var  
  9.  
  10.     @var.setter  
  11.     def var(self, var):  
  12.         self._var = var  
  13.   
  14. foo = Foo('var 1')  
  15. print foo.var  
  16. foo.var = 'var 2'  
  17. print foo.var  
注意: 如果将上面的 @var.setter 装饰器所装饰的成员函数去掉,则Foo.var 属性为只读属性,使用 foo.var = 'var 2' 进行赋值时会抛出异常,其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var 1  
  2. var 2  
注意: 如果使用老式的Python类定义,所声明的属性不是 read only的,下面代码说明了这种情况:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. class Foo:  
  2.     def __init__(self, var):  
  3.         self._var = var  
  4.  
  5.     @property  
  6.     def var(self):  
  7.         return self._var  
  8.   
  9. foo = Foo('var 1')  
  10. print foo.var  
  11. foo.var = 'var 2'  
  12. print foo.var  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var 1  
  2. var 2  

调用顺序

装饰器的调用顺序与使用 @ 语法糖声明的顺序相反,如下所示:

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def decorator_a(func):  
  2.     print "decorator_a"  
  3.     return func  
  4.   
  5. def decorator_b(func):  
  6.     print "decorator_b"  
  7.     return func  
  8.  
  9. @decorator_a  
  10. @decorator_b  
  11. def foo():  
  12.     print "foo"  
  13.       
  14. foo()  
其等价于:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def decorator_a(func):  
  2.     print "decorator_a"  
  3.     return func  
  4.   
  5. def decorator_b(func):  
  6.     print "decorator_b"  
  7.     return func  
  8.   
  9. def foo():  
  10.     print "foo"  
  11.   
  12. foo = decorator_a(decorator_b(foo))  
  13. foo()  
通过等价的调用形式我们可以看到,按照python的函数求值序列,decorator_b(fun) 会首先被求值,然后将其结果作为输入,传递给decorator_a,因此其调用顺序与声明顺序相反。其运行结果如下所示:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. decorator_b  
  2. decorator_a  
  3. foo  

调用时机

装饰器很好用,那么它什么时候被调用?性能开销怎么样?会不会有副作用?接下来我们就以几个实例来验证我们的猜想。
首先我们验证一下装饰器的性能开销,代码如下所示:

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def decorator_a(func):  
  2.     print "decorator_a"  
  3.     print 'func id: ' + str(id(func))  
  4.     return func  
  5.   
  6. def decorator_b(func):  
  7.     print "decorator_b"  
  8.     print 'func id: ' + str(id(func))  
  9.     return func  
  10.   
  11. print 'Begin declare foo with decorators'  
  12. @decorator_a  
  13. @decorator_b  
  14. def foo():  
  15.     print "foo"  
  16. print 'End declare foo with decorators'  
  17.   
  18. print 'First call foo'  
  19. foo()  
  20. print 'Second call foo'  
  21. foo()  
  22. print 'Function infos'  
  23. print 'decorator_a id: ' + str(id(decorator_a))  
  24. print 'decorator_b id: ' + str(id(decorator_b))  
  25. print 'fooid : ' + str(id(foo))  
其运行结果如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. Begin declare foo with decorators  
  2. decorator_b  
  3. func id: 140124961990488  
  4. decorator_a  
  5. func id: 140124961990488  
  6. End declare foo with decorators  
  7. First call foo  
  8. foo  
  9. Second call foo  
  10. foo  
  11. Function infos  
  12. decorator_a id: 140124961954464  
  13. decorator_b id: 140124961988808  
  14. fooid : 140124961990488  
在运行结果中的:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. Begin declare foo with decorators  
  2. decorator_b  
  3. func id: 140124961990488  
  4. decorator_a  
  5. func id: 140124961990488  
  6. End declare foo with decorators  
证实了装饰器的调用时机为: 被装饰对象定义时
而运行结果中的:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. First call foo  
  2. foo  
  3. Second call foo  
  4. foo  
证实了在相同 .py 文件中,装饰器对所装饰的函数只进行一次装饰,不会每次调用相应函数时都重新装饰,这个很容易理解,因为其本质等价于下面的函数签名重新绑定:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. foo = decorator_a(decorator_b(foo))  
对于跨模块的调用,我们编写如下结构的测试代码:
[ruby] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. .  
  2. ├── common  
  3. │   ├── decorator.py  
  4. │   ├── __init__.py  
  5. │   ├── mod_a  
  6. │   │   ├── fun_a.py  
  7. │   │   └── __init__.py  
  8. │   └── mod_b  
  9. │       ├── fun_b.py  
  10. │       └── __init__.py  
  11. └── test.py  
上述所有模块中的 __init__.py 文件均为: # -*- coding: utf-8 -*-
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. # -*- coding: utf-8 -*-  
  2. # common/mod_a/fun_a.py  
  3.   
  4.   
  5. from common.decorator import foo  
  6.   
  7.   
  8. def fun_a():  
  9.     print 'in common.mod_a.fun_a.fun_a call foo'  
  10.     foo()  

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. # -*- coding: utf-8 -*-  
  2. # common/mod_b/fun_b.py  
  3.   
  4. from common.decorator import foo  
  5.   
  6. def fun_b():  
  7.     print 'in common.mod_b.fun_b.fun_b call foo'  
  8.     foo()  
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. # -*- coding: utf-8 -*-  
  2. # common/decorator.py  
  3.   
  4. def decorator_a(func):  
  5.     print 'init decorator_a'  
  6.     return func  
  7.  
  8. @decorator_a  
  9. def foo():  
  10.     print 'function foo'  
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. # -*- coding: utf-8 -*-  
  2. # test.py  
  3.   
  4. from common.mod_a.fun_a import fun_a  
  5. from common.mod_b.fun_b import fun_b  
  6.   
  7. fun_a()  
  8. fun_b()  
上述代码通过创建 common.mod_a 和 common.mod_b 两个子模块,并调用common.decorator 中的 foo 函数,来测试跨模块时装饰器的工作情况,运行 test.py 的结果如下所示:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. init decorator_a  
  2. in common.mod_a.fun_a.fun_a call foo  
  3. function foo  
  4. in common.mod_b.fun_b.fun_b call foo  
  5. function foo  
经过上面的验证,可以看出,对于跨模块的调用,装饰器也只会初始化一次,不过这要归功于 *.pyc,这与本文主题无关,故不详述。
关于装饰器副作用的话题比较大,这不仅仅是装饰器本身的问题,更多的时候是我们设计上的问题,下面给出一个初学装饰器时大家都会遇到的一个问题——丢失函数元信息:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def decorator_a(func):  
  2.     def inner(*args, **kwargs):  
  3.         res = func(*args, **kwargs)  
  4.         return res  
  5.     return inner  
  6.  
  7. @decorator_a  
  8. def foo():  
  9.     '''''foo doc'''  
  10.     return 'foo result'  
  11.   
  12. print 'foo.__module__: ' + str(foo.__module__)  
  13. print 'foo.__name__: ' + str(foo.__name__)  
  14. print 'foo.__doc__: ' + str(foo.__doc__)  
  15. print foo()  
其运行结果如下所示:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. foo.__module__: __main__  
  2. foo.__name__: inner  
  3. foo.__doc__: None  
  4. foo result  
我们可以看到,在使用 decorator_a 对 foo 函数进行装饰后,foo 的元信息会丢失,解决方案参见: functools.wraps

多个装饰器运行期行为

前面已经讲解过装饰器的调用顺序和调用时机,但是被多个装饰器装饰的函数,其运行期行为还是有一些细节需要说明的,而且很可能其行为会让你感到惊讶,下面时一个实例:

[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def tracer(msg):  
  2.     print "[TRACE] %s" % msg  
  3.   
  4. def logger(func):  
  5.     tracer("logger")  
  6.     def inner(username, password):  
  7.         tracer("inner")  
  8.         print "call %s" % func.__name__  
  9.         return func(username, password)  
  10.     return inner  
  11.   
  12. def login_debug_helper(show_debug_info=False):  
  13.     tracer("login_debug_helper")  
  14.     def proxy_fun(func):  
  15.         tracer("proxy_fun")  
  16.         def delegate_fun(username, password):  
  17.             tracer("delegate_fun")  
  18.             if show_debug_info:  
  19.                 print "username: %s\npassword: %s" % (username, password)  
  20.             return func(username, password)  
  21.         return delegate_fun  
  22.     return proxy_fun  
  23.   
  24. print 'Declaring login_a'  
  25.  
  26. @logger  
  27. @login_debug_helper(show_debug_info=True)  
  28. def login_a(username, password):  
  29.     tracer("login_a")  
  30.     print "do some login authentication"  
  31.     return True  
  32.   
  33. print 'Call login_a'  
  34. login_a("mdl""pwd")  
大家先来看一下运行结果,看看是不是跟自己想象中的一致:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. Declaring login_a  
  2. [TRACE] login_debug_helper  
  3. [TRACE] proxy_fun  
  4. [TRACE] logger  
  5. Call login_a  
  6. [TRACE] inner  
  7. call delegate_fun  
  8. [TRACE] delegate_fun  
  9. username: mdl  
  10. password: pwd  
  11. [TRACE] login_a  
  12. do some login authentication  
首先,装饰器初始化时的调用顺序与我们前面讲解的一致,如下:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. Declaring login_a  
  2. [TRACE] login_debug_helper  
  3. [TRACE] proxy_fun  
  4. [TRACE] logger  
然而,接下来,来自 logger 装饰器中的 inner 函数首先被执行,然后才是login_debug_helper 返回的 proxy_fun 中的 delegate_fun 函数。各位读者发现了吗,运行期执行login_a 函数的时候,装饰器中返回的函数的执行顺序是相反的,难道是我们前面讲解的例子有错误吗?其实,如果大家的认为运行期调用顺序应该与装饰器初始化阶段的顺序一致的话,那说明大家没有看透这段代码的调用流程,下面我来为大家分析一下。
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. def login_debug_helper(show_debug_info=False):  
  2.     tracer("login_debug_helper")  
  3.     def proxy_fun(func):  
  4.         tracer("proxy_fun")  
  5.         def delegate_fun(username, password):  
  6.             tracer("delegate_fun")  
  7.             if show_debug_info:  
  8.                 print "username: %s\npassword: %s" % (username, password)  
  9.             return func(username, password)  
  10.         return delegate_fun  
  11.     return proxy_fun  
当装饰器 login_debug_helper 被调用时,其等价于:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. login_debug_helper(show_debug_info=True)(login_a)('mdl''pwd')  
对于只有 login_debug_helper 的情况,现在就应该是执行玩login_a输出结果的时刻了,但是如果现在在加上logger 装饰器的话,那么这个login_debug_helper(show_debug_info=True)(login_a)('mdl', 'pwd')就被延迟执行,而将login_debug_helper(show_debug_info=True)(login_a) 作为参数传递给 logger,我们令 login_tmp = login_debug_helper(show_debug_info=True)(login_a),则调用过程等价于:
[python] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. login_tmp = login_debug_helper(show_debug_info=True)(login_a)  
  2. login_a = logger(login_tmp)  
  3. login_a('mdl''pwd')  
相信大家看过上面的等价变换后,已经明白问题出在哪里了,如果你还没有明白,我强烈建议你把这个例子自己敲一遍,并尝试用自己的方式进行化简,逐步得出结论。


网友评论

登录后评论
0/500
评论
shadowcat
+ 关注