深刻浅出 Python 装饰器:16 步轻松搞定 Python 装饰器

Python的装饰器的英文名叫Decorator,当你看到这个英文名的时候,你可能会把其跟Design Pattern里的Decorator搞混了,其实这是彻底不一样的两个东西。虽然好像,他们要干的事都很类似——都是想要对一个已有的模块作一些“修饰工做”,所谓修饰工做就是想给现有的模块加上一些小装饰(一些小功能,这些小功能可能好多模块都会用到),但又不让这个小装饰(小功能)侵入到原有的模块中的代码里去。可是OO的Decorator简直就是一场恶梦,不信你就去看看wikipedia上的词条(Decorator Pattern)里的UML图和那些代码,这就是我在《 从面向对象的设计模式看软件设计》“餐后甜点”一节中说的,OO鼓励了——“厚重地胶合和复杂层次”,也是《 如此理解面向对象编程》中所说的“OO的狂热者们很是惧怕处理数据”,Decorator Pattern搞出来的代码简直就是OO的反面教程。html

Python 的 Decorator在使用上和Java/C#的Annotation很类似,就是在方法名前面加一个@XXX注解来为这个方法装饰一些东西。可是,Java/C#的Annotation也很让人望而却步,太TMD的复杂了,你要玩它,你须要了解一堆Annotation的类库文档,让人感受就是在学另一门语言。python

而Python使用了一种相对于Decorator Pattern和Annotation来讲很是优雅的方法,这种方法不须要你去掌握什么复杂的OO模型或是Annotation的各类类库规定,彻底就是语言层面的玩法:一种函数式编程的技巧。若是你看过本站的《函数式编程》,你必定会为函数式编程的那种“描述你想干什么,而不是描述你要怎么去实现”的编程方式感到畅快。(若是你不了解函数式编程,那在读本文以前,还请你移步去看看《函数式编程》) 好了。mysql

做为一名教python的老师,我发现学生们基本上一开始很难搞定python的装饰器,也许由于装饰器确实很难懂。搞定装饰器须要你了解一些函数式编程的概念,固然还有理解在python中定义和调用函数相关语法的一些特色。git

我无法让装饰器变得简单,可是经过一步步的剖析,我也许可以让你在理解装饰器的时候更自信一点。由于装饰器很复杂,这篇文章将会很长(本身都说很长,还敢这么多废话blablabla…前戏就不继续翻译直接省略了)github

1. 函数

在python中,函数经过def关键字、函数名和可选的参数列表定义。经过return关键字返回值。咱们举例来讲明如何定义和调用一个简单的函数:web

def foo():
    return 1
foo()
1

方法体(固然多行也是同样的)是必须的,经过缩进来表示,在方法名的后面加上双括号()就可以调用函数算法

2. 做用域

在python中,函数会建立一个新的做用域。python开发者可能会说函数有本身的命名空间,差很少一个意思。这意味着在函数内部碰到一个变量的时候函数会优先在本身的命名空间里面去寻找。让咱们写一个简单的函数看一下 本地做用域 和 全局做用域有什么不一样:sql

a_string = "This is a global variable"
def foo():
    print locals()
print globals()
{..., 'a_string': 'This is a global variable'}
foo() # 2
{}

内置的函数globals返回一个包含全部python解释器知道的变量名称的字典(为了干净和洗的白白的,我省略了python自行建立的一些变量)。在#2我调用了函数 foo 把函数内部本地做用域里面的内容打印出来。咱们可以看到,函数foo有本身独立的命名空间,虽然暂时命名空间里面什么都尚未。shell

3. 变量解析规则

固然这并非说咱们在函数里面就不能访问外面的全局变量。在python的做用域规则里面,建立变量必定会必定会在当前做用域里建立一个变量,可是访问或者修改变量时会先在当前做用域查找变量,没有找到匹配变量的话会依次向上在闭合的做用域里面进行查看找。因此若是咱们修改函数foo的实现让它打印全局的做用域里的变量也是能够的:数据库

a_string = "This is a global variable"
def foo():
    print a_string # 1
foo()
This is a global variable

在#1处,python解释器会尝试查找变量a_string,固然在函数的本地做用域里面是找不到的,因此接着会去上层的做用域里面去查找。

可是另外一方面,假如咱们在函数内部给全局变量赋值,结果却和咱们想的不同:

a_string = "This is a global variable"
def foo():
    a_string = "test" # 1
    print locals()
foo()
{'a_string': 'test'}
a_string # 2
'This is a global variable'

咱们可以看到,全局变量可以被访问到(若是是可变数据类型(像list,dict这些)甚至可以被更改)可是赋值不行。在函数内部的#1处,咱们实际上新建立了一个局部变量,隐藏全局做用域中的同名变量。咱们能够经过打印出局部命名空间中的内容得出这个结论。咱们也能看到在#2处打印出来的变量a_string的值并无改变。

4. 变量生存周期

值得注意的一个点是,变量不只是生存在一个个的命名空间内,他们都有本身的生存周期,请看下面这个例子:

def foo():
    x = 1
foo()
print x # 1
#Traceback (most recent call last):
#NameError: name 'x' is not defined

#1处发生的错误不只仅是由于做用域规则致使的(尽管这是抛出了NameError的错误的缘由)它还和python以及其它不少编程语言中函数调用实现的机制有关。在这个地方这个执行时间点并无什么有效的语法让咱们可以获取变量x的值,由于它这个时候压根不存在!函数foo的命名空间随着函数调用开始而开始,结束而销毁。

5. 函数参数

python容许咱们向函数传递参数,参数会变成本地变量存在于函数内部。

def foo(x):
    print locals()
foo(1)
{'x': 1}

在Python里有不少的方式来定义和传递参数,完整版能够查看 python官方文档。咱们这里简略的说明一下:函数的参数能够是必须的位置参数或者是可选的命名,默认参数。

def foo(x, y=0): # 1
    return x - y
foo(3, 1) # 2
2
foo(3) # 3
3
foo() # 4
#Traceback (most recent call last):
#TypeError: foo() takes at least 1 argument (0 given)
foo(y=1, x=3) # 5
2

在#1处咱们定义了函数foo,它有一个位置参数x和一个命名参数y。在#2处咱们可以经过常规的方式来调用函数,尽管有一个命名参数,但参数依然能够经过位置传递给函数。在调用函数的时候,对于命名参数y咱们也能够彻底无论就像#3处所示的同样。若是命名参数没有接收到任何值的话,python会自动使用声明的默认值也就是0。须要注意的是咱们不能省略第一个位置参数x, 不然的话就会像#4处所示发生错误。

目前还算简洁清晰吧, 可是接下来可能会有点使人困惑。python支持函数调用时的命名参数(我的以为应该是命名实参)。看看#5处的函数调用,咱们传递的是两个命名实参,这个时候由于有名称标识,参数传递的顺序也就不用在乎了。

固然相反的状况也是正确的:函数的第二个形参是y,可是咱们经过位置的方式传递值给它。在#2处的函数调用foo(3,1),咱们把3传递给了第一个参数,把1传递给了第二个参数,尽管第二个参数是一个命名参数。

桑不起,感受用了好大一段才说清楚这么一个简单的概念:函数的参数能够有名称和位置。这意味着在函数的定义和调用的时候会稍稍在理解上有点儿不一样。咱们能够给只定义了位置参数的函数传递命名参数(实参),反之亦然!若是以为不够能够查看官方文档

6. 嵌套函数

Python容许建立嵌套函数。这意味着咱们能够在函数里面定义函数并且现有的做用域和变量生存周期依旧适用。

def outer():
    x = 1
    def inner():
        print x # 1
    return inner() # 2
outer()
1

这个例子有一点儿复杂,可是看起来也还行。想想在#1发生了什么:python解释器需找一个叫x的本地变量,查找失败以后会继续在上层的做用域里面寻找,这个上层的做用域定义在另一个函数里面。对函数outer来讲,变量x是一个本地变量,可是如先前提到的同样,函数inner能够访问封闭的做用域(至少能够读和修改)。在#2处,咱们调用函数inner,很是重要的一点是,inner也仅仅是一个遵循python变量解析规则的变量名,python解释器会优先在outer的做用域里面对变量名inner查找匹配的变量.

7. 函数是python世界里的一级类对象

显而易见,在python里函数和其余东西同样都是对象。(此处应该大声歌唱)啊!包含变量的函数,你也并非那么特殊!

issubclass(int, object) # all objects in Python inherit from a common baseclass
#True
def foo():
    pass
foo.__class__ # 1
#<type 'function'>
issubclass(foo.__class__, object)
#True

你也许从没有想过,你定义的函数竟然会有属性。没办法,函数在python里面就是对象,和其余的东西同样,也许这样描述会太学院派太官方了点:在python里,函数只是一些普通的值而已和其余的值一毛同样。这就是说你尅一把函数想参数同样传递给其余的函数或者说从函数了里面返回函数!若是你历来没有这么想过,那看看下面这个例子:

def add(x, y):
    return x + y
def sub(x, y):
    return x - y
def apply(func, x, y): # 1
    return func(x, y) # 2
apply(add, 2, 1) # 3
3
apply(sub, 2, 1)
1

这个例子对你来讲应该不会很奇怪。add和sub是很是普通的两个python函数,接受两个值,返回一个计算后的结果值。在#1处大家能看到准备接收一个函数的变量只是一个普通的变量而已,和其余变量同样。在#2处咱们调用传进来的函数:“()表明着调用的操做而且调用变量包含的值。在#3处,大家也能看到传递函数并无什么特殊的语法。” 函数的名称只是很其余变量同样的表标识符而已。

大家也许看到过这样的行为:“python把频繁要用的操做变成函数做为参数进行使用,像经过传递一个函数给内置排序函数的key参数从而来自定义排序规则。那把函数当作返回值回事这样的状况呢:

def outer():
    def inner():
        print "Inside inner"
    return inner # 1
foo = outer() #2
foo
#<function inner at 0x...>
foo()
#Inside inner

这个例子看起来也许会更加的奇怪。在#1处我把刚好是函数标识符的变量inner做为返回值返回出来。这并无什么特殊的语法:”把函数inner返回出来,不然它根本不可能会被调用到。“还记得变量的生存周期吗?每次函数outer被调用的时候,函数inner都会被从新定义,若是它不被当作变量返回的话,每次执行事后它将不复存在。

在#2处咱们捕获住返回值 – 函数inner,将它存在一个新的变量foo里。咱们可以看到,当对变量foo进行求值,它确实包含函数inner,并且咱们可以对他进行调用。初次看起来可能会以为有点奇怪,可是理解起来并不困难是吧。坚持住,由于奇怪的转折立刻就要来了

8. 闭包

咱们先不急着定义什么是闭包,先来看看一段代码,仅仅是把上一个例子简单的调整了一下:

def outer():
    x = 1
    def inner():
        print x # 1
    return inner
foo = outer()
foo.func_closure
#(<cell at 0x...: int object at 0x...>,)

在上一个例子中咱们了解到,inner做为一个函数被outer返回,保存在一个变量foo,而且咱们可以对它进行调用foo()。不过它会正常的运行吗?咱们先来看看做用域规则。

全部的东西都在python的做用域规则下进行工做:“x是函数outer里的一个局部变量。当函数inner在#1处打印x的时候,python解释器会在inner内部查找相应的变量,固然会找不到,因此接着会到封闭做用域里面查找,而且会找到匹配。

可是从变量的生存周期来看,该怎么理解呢?咱们的变量x是函数outer的一个本地变量,这意味着只有当函数outer正在运行的时候才会存在。根据咱们已知的python运行模式,咱们无法在函数outer返回以后继续调用函数inner,在函数inner被调用的时候,变量x早已不复存在,可能会发生一个运行时错误。

万万没想到,返回的函数inner竟然可以正常工做。Python支持一个叫作函数闭包的特性,用人话来说就是,嵌套定义在非全局做用域里面的函数可以记住它在被定义的时候它所处的封闭命名空间。这可以经过查看函数的func_closure属性得出结论,这个属性里面包含封闭做用域里面的值(只会包含被捕捉到的值,好比x,若是在outer里面还定义了其余的值,封闭做用域里面是不会有的)

记住,每次函数outer被调用的时候,函数inner都会被从新定义。如今变量x的值不会变化,因此每次返回的函数inner会是一样的逻辑,假如咱们稍微改动一下呢?

def outer(x):
    def inner():
        print x # 1
    return inner
print1 = outer(1)
print2 = outer(2)
print1()
1
print2()
2

从这个例子中你可以看到闭包 – 被函数记住的封闭做用域 – 可以被用来建立自定义的函数,本质上来讲是一个硬编码的参数。事实上咱们并非传递参数1或者2给函数inner,咱们其实是建立了可以打印各类数字的各类自定义版本。

闭包单独拿出来就是一个很是强大的功能, 在某些方面,你也许会把它当作一个相似于面向对象的技术:outer像是给inner服务的构造器,x像一个私有变量。使用闭包的方式也有不少:你若是熟悉python内置排序方法的参数key,你说不定已经写过一个lambda方法在排序一个列表的列表的时候基于第二个元素而不是第一个。如今你说不定也能够写一个itemgetter方法,接收一个索引值来返回一个完美的函数,传递给排序函数的参数key。

不过,咱们如今不会用闭包作这么low的事(⊙o⊙)…!相反,让咱们再爽一次,写一个高大上的装饰器!

9. 装饰器

装饰器其实就是一个闭包,把一个函数当作参数而后返回一个替代版函数。咱们一步步从简到繁来瞅瞅:

def outer(some_func):
    def inner():
        print "before some_func"
        ret = some_func() # 1
        return ret + 1
    return inner
def foo():
    return 1
decorated = outer(foo) # 2
decorated()
#before some_func
#2

仔细看看上面这个装饰器的例子。们定义了一个函数outer,它只有一个some_func的参数,在他里面咱们定义了一个嵌套的函数inner。inner会打印一串字符串,而后调用some_func,在#1处获得它的返回值。在outer每次调用的时候some_func的值可能会不同,可是无论some_func的之如何,咱们都会调用它。最后,inner返回some_func() + 1的值 – 咱们经过调用在#2处存储在变量decorated里面的函数可以看到被打印出来的字符串以及返回值2,而不是指望中调用函数foo获得的返回值1。

咱们能够认为变量decorated是函数foo的一个装饰版本,一个增强版本。事实上若是打算写一个有用的装饰器的话,咱们可能会想愿意用装饰版本彻底取代原先的函数foo,这样咱们老是会获得咱们的”增强版“foo。想要达到这个效果,彻底不须要学习新的语法,简单地赋值给变量foo就好了:

foo = outer(foo)
foo # doctest: +ELLIPSIS
#<function inner at 0x...>

如今,任何怎么调用都不会牵扯到原先的函数foo,都会获得新的装饰版本的foo。

假设有以下函数:

def now():
    print '2013-12-25'
f = now
f()
#2013-12-25

如今假设咱们要加强now()函数的功能,好比,在函数调用先后自动打印日志,但又不但愿修改now()函数的定义,这种在代码运行期间动态增长功能的方式,称之为“装饰器”(Decorator)。

本质上,decorator就是一个返回函数的高阶函数。因此,咱们要定义一个能打印日志的decorator,能够定义以下:

def log(func):
    def wrapper(*args, **kw):
        print 'call %s():' % func.__name__
        return func(*args, **kw)
    return wrapper

观察上面的log,由于它是一个decorator,因此接受一个函数做为参数,并返回一个函数。

10. 使用 @ 标识符将装饰器应用到函数

Python2.4支持使用标识符@将装饰器应用在函数上,只须要在函数的定义前加上@和装饰器的名称。在上一节的例子里咱们是将本来的方法用装饰后的方法代替:

add = wrapper(add)

这种方式可以在任什么时候候对任意方法进行包装。可是若是咱们自定义一个方法,咱们可使用@进行装饰:

@wrapper
def add(a, b):
    return Coordinate(a.x + b.x, a.y + b.y)

须要明白的是,这样的作法和先前简单的用包装方法替代原有方法是如出一辙的, python只是加了一些语法糖让装饰的行为更加的直接明确和优雅一点。

多个decorator

@decorator_one
@decorator_two
def func():
    pass

至关于:

func = decorator_one(decorator_two(func))

好比:带参数的decorator:

@decorator(arg1, arg2)
def func():
    pass

至关于:

func = decorator(arg1,arg2)(func)

这意味着decorator(arg1, arg2)这个函数须要返回一个“真正的decorator”。

11. *args and **kwargs

咱们已经完成了一个有用的装饰器,可是因为硬编码的缘由它只能应用在一类具体的方法上,这类方法接收两个参数,传递给闭包捕获的函数。若是咱们想实现一个可以应用在任何方法上的装饰器要怎么作呢?再好比,若是咱们要实现一个能应用在任何方法上的相似于计数器的装饰器,不须要改变原有方法的任何逻辑。这意味着装饰器可以接受拥有任何签名的函数做为本身的被装饰方法,同时可以用传递给它的参数对被装饰的方法进行调用。

很是巧合的是Python正好有支持这个特性的语法。能够阅读 Python Tutorial 获取更多的细节。当定义函数的时候使用了*,意味着那些经过位置传递的参数将会被放在带有*前缀的变量中, 因此:

def one(*args):
    print args # 1
one()
#()
one(1, 2, 3)
#(1, 2, 3)
def two(x, y, *args): # 2
    print x, y, args
two('a', 'b', 'c')
#a b ('c',)

第一个函数one只是简单地讲任何传递过来的位置参数所有打印出来而已,大家可以看到,在代码#1处咱们只是引用了函数内的变量args, *args仅仅只是用在函数定义的时候用来表示位置参数应该存储在变量args里面。Python容许咱们制定一些参数而且经过args捕获其余全部剩余的未被捕捉的位置参数,就像#2处所示的那样。
*操做符在函数被调用的时候也能使用。意义基本是同样的。当调用一个函数的时候,一个用*标志的变量意思是变量里面的内容须要被提取出来而后当作位置参数被使用。一样的,来看个例子:

def add(x, y):
    return x + y
lst = [1,2]
add(lst[0], lst[1]) # 1
3
add(*lst) # 2
3

#1处的代码和#2处的代码所作的事情实际上是同样的,在#2处,python为咱们所作的事其实也能够手动完成。这也不是什么坏事,*args要么是表示调用方法大的时候额外的参数能够从一个可迭代列表中取得,要么就是定义方法的时候标志这个方法可以接受任意的位置参数。
接下来提到的**会稍多更复杂一点,**表明着键值对的餐宿字典,和*所表明的意义相差无几,也很简单对不对:

def foo(**kwargs):
    print kwargs
foo()
#{}
foo(x=1, y=2)
#{'y': 2, 'x': 1}

当咱们定义一个函数的时候,咱们可以用**kwargs来代表,全部未被捕获的关键字参数都应该存储在kwargs的字典中。如前所诉,args、kwargs并非python语法的一部分,但在定义函数的时候,使用这样的变量名算是一个不成文的约定。和*同样,咱们一样能够在定义或者调用函数的时候使用**。

dct = {'x': 1, 'y': 2}
def bar(x, y):
    return x + y
bar(**dct)
#3

12. 更通用的装饰器

有了这招新的技能,咱们随随便便就能够写一个可以记录下传递给函数参数的装饰器了。先来个简单地把日志输出到界面的例子:

def logger(func):
    def inner(*args, **kwargs): #1
        print "Arguments were: %s, %s" % (args, kwargs)
        return func(*args, **kwargs) #2
    return inner

请注意咱们的函数inner,它可以接受任意数量和类型的参数并把它们传递给被包装的方法,这让咱们可以用这个装饰器来装饰任何方法。

@logger
def foo1(x, y=1):
    return x * y
@logger
def foo2():
    return 2
foo1(5, 4)
#Arguments were: (5, 4), {}
#20
foo1(1)
#Arguments were: (1,), {}
#1
foo2()
#Arguments were: (), {}
#2

随便调用咱们定义的哪一个方法,相应的日志也会打印到输出窗口,和咱们预期的同样。

13. 带参数的装饰器:

若是decorator自己须要传入参数,那就须要编写一个返回decorator的高阶函数,写出来会更复杂。好比,要自定义log的文本:

def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print '%s %s():' % (text, func.__name__)
            return func(*args, **kw)
        return wrapper
    return decorator

这个3层嵌套的decorator用法以下:

@log('execute')
def now():
    print '2013-12-25'

执行结果以下:

>>> now()
execute now():
2013-12-25

和两层嵌套的decorator相比,3层嵌套的效果是这样的:

now = log('execute')(now)

咱们来剖析上面的语句,首先执行log('execute'),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数。

14. 装饰器的反作用

以上两种decorator的定义都没有问题,但还差最后一步。由于咱们讲了函数也是对象,它有__name__等属性,但你去看通过decorator装饰以后的函数,它们的__name__已经从原来的'now'变成了'wrapper'

>>> now.__name__
'wrapper'

由于返回的那个wrapper()函数名字就是'wrapper',因此,须要把原始函数的__name__等属性复制到wrapper()函数中,不然,有些依赖函数签名的代码执行就会出错。

不须要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,因此,一个完整的decorator的写法以下:

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print 'call %s():' % func.__name__
        return func(*args, **kw)
    return wrapper

或者针对带参数的decorator:

import functools

def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print '%s %s():' % (text, func.__name__)
            return func(*args, **kw)
        return wrapper
    return decorator

import functools是导入functools模块。模块的概念稍候讲解。如今,只需记住在定义wrapper()的前面加上@functools.wraps(func)便可。

固然,即便是你用了functools的wraps,也不能彻底消除这样的反作用。你会发现,即便是你你用了functools的wraps,你在用getargspec时,参数也不见了。要修正这一问题,咱们还得用Python的反射来解决,固然,我相信大多数人的程序都不会去getargspec。因此,用functools的wraps应该够用了。

15. class式的 Decorator

首先,先得说一下,decorator的class方式,仍是看个示例:

class myDecorator(object):
 
    def __init__(self, fn):
        print "inside myDecorator.__init__()"
        self.fn = fn
 
    def __call__(self):
        self.fn()
        print "inside myDecorator.__call__()"
 
@myDecorator
def aFunction():
    print "inside aFunction()"
 
print "Finished decorating aFunction()"
 
aFunction()
 
# 输出:
# inside myDecorator.__init__()
# Finished decorating aFunction()
# inside aFunction()
# inside myDecorator.__call__()

1)一个是__init__(),这个方法是在咱们给某个函数decorator时被调用,因此,须要有一个fn的参数,也就是被decorator的函数。
2)一个是__call__(),这个方法是在咱们调用被decorator函数时被调用的。
上面输出能够看到整个程序的执行顺序。

这看上去要比“函数式”的方式更易读一些。

上面这段代码中,咱们须要注意这几点:

1)若是decorator有参数的话,__init__() 成员就不能传入fn了,而fn是在__call__的时候传入的。

16. 一些decorator的示例

好了,如今咱们来看一下各类decorator的例子:

16.1 给函数调用作缓存

这个例实在是太经典了,整个网上都用这个例子作decorator的经典范例,由于太经典了,因此,我这篇文章也不能免俗。

from functools import wraps
def memo(fn):
    cache = {}
    miss = object()
 
    @wraps(fn)
    def wrapper(*args):
        result = cache.get(args, miss)
        if result is miss:
            result = fn(*args)
            cache[args] = result
        return result
 
    return wrapper
 
@memo
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

上面这个例子中,是一个斐波拉契数例的递归算法。咱们知道,这个递归是至关没有效率的,由于会重复调用。好比:咱们要计算fib(5),因而其分解成fib(4) + fib(3),而fib(4)分解成fib(3)+fib(2),fib(3)又分解成fib(2)+fib(1)…… 你可看到,基本上来讲,fib(3), fib(2), fib(1)在整个递归过程当中被调用了两次。

而咱们用decorator,在调用函数前查询一下缓存,若是没有才调用了,有了就从缓存中返回值。一会儿,这个递归从二叉树式的递归成了线性的递归。

另一个常见的例子是爬虫里的 URL Cache:

最简单的缓存,一般这样实现:

def web_lookup(url, saved={}):
    if url in saved:
        return saved[url]
    page = urllib.urlopen(url).read()
    saved[url] = page
    return page

能够这样写:

@cache
def web_lookup(url):
    return urllib.urlopen(url).read()

def cache(func):
    saved = {}
    @wraps(func)
    def newfunc(*args):
        if args in saved:
            return saved[args]
        result = func(*args)
        saved[args] = result
        return result
    return newfunc

16.2 Profiler的例子

这个例子没什么高深的,就是实用一些。

import cProfile, pstats, StringIO
 
def profiler(func):
    def wrapper(*args, **kwargs):
        datafn = func.__name__ + ".profile" # Name the data file
        prof = cProfile.Profile()
        retval = prof.runcall(func, *args, **kwargs)
        #prof.dump_stats(datafn)
        s = StringIO.StringIO()
        sortby = 'cumulative'
        ps = pstats.Stats(prof, stream=s).sort_stats(sortby)
        ps.print_stats()
        print s.getvalue()
        return retval
 
    return wrapper

16.3 注册回调函数

下面这个示例展现了经过URL的路由来调用相关注册的函数示例:

class MyApp():
    def __init__(self):
        self.func_map = {}
 
    def register(self, name):
        def func_wrapper(func):
            self.func_map[name] = func
            return func
        return func_wrapper
 
    def call_method(self, name=None):
        func = self.func_map.get(name, None)
        if func is None:
            raise Exception("No function registered against - " + str(name))
        return func()
 
app = MyApp()
 
@app.register('/')
def main_page_func():
    return "This is the main page."
 
@app.register('/next_page')
def next_page_func():
    return "This is the next page."
 
print app.call_method('/')
print app.call_method('/next_page')

注意:
1)上面这个示例中,用类的实例来作decorator。
2)decorator类中没有__call__(),可是wrapper返回了原函数。因此,原函数没有发生任何变化。

16.4 给函数打日志

下面这个示例演示了一个logger的decorator,这个decorator输出了函数名,参数,返回值,和运行时间。

from functools import wraps
def logger(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        ts = time.time()
        result = fn(*args, **kwargs)
        te = time.time()
        print "function      = {0}".format(fn.__name__)
        print "    arguments = {0} {1}".format(args, kwargs)
        print "    return    = {0}".format(result)
        print "    time      = %.6f sec" % (te-ts)
        return result
    return wrapper
 
@logger
def multipy(x, y):
    return x * y
 
@logger
def sum_num(n):
    s = 0
    for i in xrange(n+1):
        s += i
    return s
 
print multipy(2, 10)
print sum_num(100)
print sum_num(10000000)

上面那个打日志仍是有点粗糙,让咱们看一个更好一点的(带log level参数的):

import inspect
def get_line_number():
    return inspect.currentframe().f_back.f_back.f_lineno
 
def logger(loglevel):
    def log_decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            ts = time.time()
            result = fn(*args, **kwargs)
            te = time.time()
            print "function   = " + fn.__name__,
            print "    arguments = {0} {1}".format(args, kwargs)
            print "    return    = {0}".format(result)
            print "    time      = %.6f sec" % (te-ts)
            if (loglevel == 'debug'):
                print "    called_from_line : " + str(get_line_number())
            return result
        return wrapper
    return log_decorator

可是,上面这个带log level参数的有两具很差的地方,
1) loglevel不是debug的时候,仍是要计算函数调用的时间。
2) 不一样level的要写在一块儿,不易读。

咱们再接着改进:

import inspect
 
def advance_logger(loglevel):
 
    def get_line_number():
        return inspect.currentframe().f_back.f_back.f_lineno
 
    def _basic_log(fn, result, *args, **kwargs):
        print "function   = " + fn.__name__,
        print "    arguments = {0} {1}".format(args, kwargs)
        print "    return    = {0}".format(result)
 
    def info_log_decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            result = fn(*args, **kwargs)
            _basic_log(fn, result, args, kwargs)
        return wrapper
 
    def debug_log_decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            ts = time.time()
            result = fn(*args, **kwargs)
            te = time.time()
            _basic_log(fn, result, args, kwargs)
            print "    time      = %.6f sec" % (te-ts)
            print "    called_from_line : " + str(get_line_number())
        return wrapper
 
    if loglevel is "debug":
        return debug_log_decorator
    else:
        return info_log_decorator

你能够看到两点,
1)咱们分了两个log level,一个是info的,一个是debug的,而后咱们在外尾根据不一样的参数返回不一样的decorator。
2)咱们把info和debug中的相同的代码抽到了一个叫_basic_log的函数里,DRY原则。

16.5 一个MySQL的Decorator

下面这个decorator是我在工做中用到的代码,我简化了一下,把DB链接池的代码去掉了,这样能简单点,方便阅读。

import umysql
from functools import wraps
 
class Configuraion:
    def __init__(self, env):
        if env == "Prod":
            self.host    = "coolshell.cn"
            self.port    = 3306
            self.db      = "coolshell"
            self.user    = "coolshell"
            self.passwd  = "fuckgfw"
        elif env == "Test":
            self.host   = 'localhost'
            self.port   = 3300
            self.user   = 'coolshell'
            self.db     = 'coolshell'
            self.passwd = 'fuckgfw'
 
def mysql(sql):
 
    _conf = Configuraion(env="Prod")
 
    def on_sql_error(err):
        print err
        sys.exit(-1)
 
    def handle_sql_result(rs):
        if rs.rows > 0:
            fieldnames = [f[0] for f in rs.fields]
            return [dict(zip(fieldnames, r)) for r in rs.rows]
        else:
            return []
 
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            mysqlconn = umysql.Connection()
            mysqlconn.settimeout(5)
            mysqlconn.connect(_conf.host, _conf.port, _conf.user, \
                              _conf.passwd, _conf.db, True, 'utf8')
            try:
                rs = mysqlconn.query(sql, {})
            except umysql.Error as e:
                on_sql_error(e)
 
            data = handle_sql_result(rs)
            kwargs["data"] = data
            result = fn(*args, **kwargs)
            mysqlconn.close()
            return result
        return wrapper
 
    return decorator
 
@mysql(sql = "select * from coolshell" )
def get_coolshell(data):
    ... ...
    ... ..

16.6 线程异步

下面量个很是简单的异步执行的decorator,注意,异步处理并不简单,下面只是一个示例。

from threading import Thread
from functools import wraps
 
def async(func):
    @wraps(func)
    def async_func(*args, **kwargs):
        func_hl = Thread(target = func, args = args, kwargs = kwargs)
        func_hl.start()
        return func_hl
 
    return async_func
 
if __name__ == '__main__':
    from time import sleep
 
    @async
    def print_somedata():
        print 'starting print_somedata'
        sleep(2)
        print 'print_somedata: 2 sec passed'
        sleep(2)
        print 'print_somedata: 2 sec passed'
        sleep(2)
        print 'finished print_somedata'
 
    def main():
        print_somedata()
        print 'back in main'
        print_somedata()
        print 'back in main'
 
    main()

16.7 超时函数

这个函数的做用在于能够给任意可能会hang住的函数添加超时功能,这个功能在编写外部API调用 、网络爬虫、数据库查询的时候特别有用。

timeout装饰器的代码以下:

# coding=utf-8
# 测试utf-8编码
import sys

reload(sys)
sys.setdefaultencoding('utf-8')

import signal, functools


class TimeoutError(Exception): pass


def timeout(seconds, error_message="Timeout Error: the cmd 30s have not finished."):
    def decorated(func):
        result = ""

        def _handle_timeout(signum, frame):
            global result
            result = error_message
            raise TimeoutError(error_message)

        def wrapper(*args, **kwargs):
            global result
            signal.signal(signal.SIGALRM, _handle_timeout)
            signal.alarm(seconds)

            try:
                result = func(*args, **kwargs)
            finally:
                signal.alarm(0)
                return result
            return result

        return functools.wraps(func)(wrapper)

    return decorated


@timeout(2)  # 限定下面的slowfunc函数若是在5s内不返回就强制抛TimeoutError Exception结束
def slowfunc(sleep_time):
    a = 1
    import time
    time.sleep(sleep_time)
    return a


# slowfunc(3) #sleep 3秒,正常返回 没有异常


print slowfunc(11)  # 被终止

 

16.8 Trace函数

有时候出于演示目的或者调试目的,咱们须要程序运行的时候打印出每一步的运行顺序 和调用逻辑。相似写bash的时候的bash -x调试功能,而后Python解释器并无 内置这个时分有用的功能,那么咱们就“本身动手,丰衣足食”。

Trace装饰器的代码以下:

# coding=utf-8
# 测试utf-8编码
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

import sys,os,linecache
def trace(f):
  def globaltrace(frame, why, arg):
    if why == "call": return localtrace
    return None
  def localtrace(frame=1, why=2, arg=4):
    if why == "line":
      # record the file name and line number of every trace
      filename = frame.f_code.co_filename
      lineno = frame.f_lineno
      bname = os.path.basename(filename)
      print "{}({}): {}".format(  bname,
                    lineno,
                    linecache.getline(filename, lineno)),
    return localtrace
  def _f(*args, **kwds):
    sys.settrace(globaltrace)
    result = f(*args, **kwds)
    sys.settrace(None)
    return result
  return _f

@trace
def xxx():
  a=1
  print a
  print 22
  print 333

xxx() #调用

#######################################
C:\Python27\python.exe F:/sourceDemo/flask/study/com.test.bj/t2.py
t2.py(31):   a=1
t2.py(32):   print a
1
t2.py(33):   print 22
22
t2.py(34):   print 333
333

Process finished with exit code 0

 

16.9 单例模式

# coding=utf-8
# 测试utf-8编码
# 单例装饰器
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

# 使用装饰器实现简单的单例模式
def singleton(cls):
    instances = dict()  # 初始为空
    def _singleton(*args, **kwargs):
        if cls not in instances:  #若是不存在, 则建立并放入字典
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return _singleton

@singleton
class Test(object):
    pass
if __name__ == '__main__':
    t1 = Test()
    t2 = Test()
    # 二者具备相同的地址
    print t1
    print t2

16.10 LRUCache

下面要分享的这个LRUCache不是我作的,是github上的一个库,咱们在实际环境中有用到。

先来讲下这个概念,cache的意思就是缓存,LRU就是Least Recently Used,即最近最少使用,是一种内存管理算法。总结来讲这就是一种缓存方法,基于时间和容量。

通常在简单的python程序中,遇到须要处理缓存的状况时最简单的方式,声明一个全局的dict就能解决(在python中应尽可能避免使用全局变量)。可是只是简单状况,这种状况会带来的问题就是内存泄漏,由于可能会出现一直不命中的状况。

由此致使的一个需求就是,你要设定这个dict的最大容量,防止发生泄漏。但仅仅是设定最大容量是不够的,设想当你的dict变量已被占满,仍是没有命中,该如何处理。

这时就须要加一个失效时间了。若是在指定失效期内没有使用到该缓存,则删除。

综述上面的需求和功能就是LRUCache干的事了。不过这份代码作了更进一层的封装,可让你直接把缓存功能作为一个装饰器来用。具体实现能够去参考代码,别人实现以后看起来并不复杂 :)

from lru import lru_cache_function

@lru_cache_function(max_size=1024, expiration=15*60)
def f(x):
    print "Calling f(" + str(x) + ")"
    return x

f(3) # This will print "Calling f(3)", will return 3
f(3) # This will not print anything, but will return 3 (unless 15 minutes have passed between the first and second function call).

代码: https://github.com/the5fire/Python-LRU-cache/blob/master/lru.py

从python3.2开始内置在functools了lru_cache的功能,说明这个需求是很广泛的。

17. 小结

在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式须要经过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator能够用函数实现,也能够用类实现。

decorator能够加强函数的功能,定义起来虽然有点复杂,但使用起来很是灵活和方便。

最后留个小做业:

请编写一个decorator,能在函数调用的先后打印出'begin call''end call'的日志。

再思考一下可否写出一个@log的decorator,使它既支持:

@log
def f():
    pass

又支持:

@log('execute')
def f():
    pass

 

18. Refer:

[1] 12步轻松搞定python装饰器

http://python.jobbole.com/81683/

[2] 装饰器

liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386819879946007bbf6ad052463ab18034f0254bf355000

[3] Python修饰器的函数式编程

http://coolshell.cn/articles/11265.html

[4] Python Decorator Library

https://wiki.python.org/moin/PythonDecoratorLibrary

[5] Python装饰器实例:调用参数合法性验证

http://python.jobbole.com/82114/

[6] Python 装饰器

http://python.jobbole.com/82344/

[7] 两个实用的Python的装饰器

http://blog.51reboot.com/%E4%B8%A4%E4%B8%AA%E5%AE%9E%E7%94%A8%E7%9A%84python%E7%9A%84%E8%A3%85%E9%A5%B0%E5%99%A8/

[8] Python 中的闭包总结

http://dwz.cn/2CiO78

[9] Python 的闭包和装饰器

http://www.javashuo.com/article/p-ymcolzcp-hy.html

[10] Python修饰器的问题

https://segmentfault.com/q/1010000004595899

[11] Python 有哪些优雅的代码实现让本身的代码更pythonic?

https://www.zhihu.com/question/37751951/answer/125640796

相关文章
相关标签/搜索