深刻淺出 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

相關文章
相關標籤/搜索