12個步驟教你理解Python裝飾器

這篇文章主要介紹了12個步驟教你理解Python裝飾器,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下

前言

或許你已經用過裝飾器,它的使用方式非常簡單但理解起來困難(其實真正理解的也很簡單),想要理解裝飾器,你需要懂點函數式編程的概念,python函數的定義以及函數調用的語法規則等,雖然我沒法把裝飾器變得簡單,但是我希望可以通過下面的步驟讓你由淺入深明白裝飾器是什麼。假定你擁有最基本的Python知識,本文闡述的東西可能對那些在工作中經常接觸Python的人有很大的幫助。

1、函數(Functions)

在Python裏,函數是用def關鍵字後跟一個函數名稱和一個可選的參數表列來創建的,可以用關鍵字return指定返回值。下面讓我們創建和調用一個最簡單的函數:

>>> def foo():
... return 1
>>> foo()
1

該函數的函數體(在Python裏將就是多行語句)是強制性的並且通過縮進來表明。我們可以通過在函數名後面添加雙括號來調用函數。

2、作用域(Scope)

在Python中,每個函數都會創建一個作用域。Pythonistas也可能稱函數擁有它們自己的命名空間(namespace)。這意味着當在函數體裏遇到變量名時,Python首先在該函數的命名空間中查找,Python包含了一些讓我們查看命名空間的函數。讓我們寫一個簡單的函數來探查一下local和global作用域的區別。

>>> a_string = "This is a global variable"
>>> def foo():
... print locals()
>>> print globals() # doctest: +ELLIPSIS
{..., 'a_strin': 'This ia a global variable'}
>>> foo() # 2
{}

內建的globals函數返回一個字典對象,它包含所有Python知道的變量名(爲了清楚明瞭起見,我已經忽略了一些Python自動創建的變量)。在#2處我調用了函數foo,它將函數內部的local namespace裏的內容打印了出來。正如我們看到的foo函數擁有自己的獨立namespace,現在它還是空的。

3、變量解析規則(variable resolution rules)

當然,這並不意味着在函數內部我們不能訪問全局變量。Python的作用域規則是,變量的創建總會創建一個新的local變量,但是變量的訪問(包括修改)會先查找local作用域然後順着最鄰近的作用域去尋找匹配。因此,如果我們修改foo函數來讓它打印global變量,結果就會像我們希望的那樣:

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

在#1處,Python在函數中尋找一個local變量,但是沒有找到,然後在global變量中找到了一個同名的變量。

另一方面,如果我們嘗試在函數裏給global變量賦值,結果將不如我們所願:

>>> 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'

正如我們所見,全局變量可以被訪問到(如果是可變類型,其甚至可以被改變),但是(默認情況下)不能被賦值。在函數內部的#1處我們實際上創建了一個新的local變量,它和全局變量擁有相同的名字,它將全局變量給覆蓋了。我們可以通過在foo函數內部打印local namespace來發現到它已經有了一個條目,通過對函數外部的#2處的輸出結果我們可以看到,變量a_string的值根本就沒有被改變。

4、變量的生命週期(Variable lifetime)

也要注意到,變量不僅“生活在”一個命名空間裏,它們還有生命週期。考慮下面的代碼:

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

在#1處不僅因爲作用域規則引發了問題(儘管這是出現了NameError的原因),而且也出於在Python和許多其它語言裏的函數調用實現的原因。此處,我們沒有任何可用的語法來獲取變量x的值——字面上是不存在的。每次當調用foo函數時,它的namespace被重新構建,並且當函數結束時被銷燬。

5、函數的參數(Function parameters)

Python允許我們向函數傳遞參數。參數名成爲了該函數的local變量。

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

Python有許多不同的定義和傳遞函數參數的方法。要想更詳細深入地瞭解請參照the Python documentation on defining functions。這裏我展示一個簡版:函數參數既可以是強制的位置參數(positional parameters)或者是命名參數,參數的默認值是可選的。

>>> 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處我們定義了一個帶有一個位置參數x和一個命名參數y的函數。正如我們看到的,在#2處我們可以通過普通的值傳遞來調用函數,即使一個參數(譯者注:這裏指參數y)在函數定義裏被定義爲一個命名參數。在#3處我們可以看到,我們甚至可以不爲命名參數傳遞任何值就可以調用函數——如果foo函數沒有接收到傳給命名參數y的值,Python將會用我們聲明的默認值0來調用函數。當然,我們不能漏掉第一個(強制的,定好位置的)參數——#4以一個異常描述了這種錯誤。

都很清晰和直接,不是嗎?下面變得有點兒讓人疑惑——Python也支持函數調用時的命名參數而不只是在函數定義時。請看#5處,這裏我們用兩個命名參數調用函數,儘管這個函數是以一個命名和一個位置參數來定義的。因爲我們的參數有名字,所以我們傳遞的參數的位置不會產生任何影響。 相反的情形當然也是正確的。我們的函數的一個參數被定義爲一個命名參數但是我們通過位置傳遞參數—— #4處的調用foo(3, 1)將一個3作爲第一個參數傳遞給我們排好序的參數x並將第二個參數(整數1)傳遞給第二個參數,儘管它被定義爲一個命名參數。

Whoo!這就像用很多話來描述一個非常簡單的概念:函數的參數可以有名稱或者位置。

6、內嵌函數(Nested functions)

Python允許創建嵌套函數,這意味着我們可以在函數內聲明函數並且所有的作用域和聲明週期規則也同樣適用。

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

這看起來稍顯複雜,但其行爲仍相當直接,易於理解。考慮一下在#1處發生了什麼——Python尋找一個名爲x的local變量,失敗了,然後在最鄰近的外層作用域裏搜尋,這個作用域是另一個函數!變量x是函數outer的local變量,但是和前文提到的一樣,inner函數擁有對外層作用域的訪問權限(最起碼有讀和修改的權限)。在#2處我們調用了inner函數。請記住inner也只是一個變量名,它也遵從Python的變量查找規則——Python首先在outer的作用域裏查找之,找到了一個名爲inner的local變量。

7、函數是一等公民(Functions are first class objects in Python)

在Python中,這是一個常識,函數是和其它任何東西一樣的對象。呃,函數包含變量,它不是那麼的特殊!

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

你也許從沒想到過函數也有屬性,但是在Python中,和其它任何東西一樣,函數是對象。(如果你發覺這令你感到困惑,請等一下,知道你瞭解到在Python中像其它任何東西一樣,class也是對象!)也許正是因爲這一點使Python多少有點“學術”的意味——在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處我們調用傳入apply的函數——在Python裏雙括號是調用操作符,並且調用變量名包含的值。在#3處你可以看出在Python中將函數當做值進行傳遞並沒有任何特殊語法——函數名就像任何其它變量一樣只是變量標籤。

你之前可能見過這種行爲——Python將函數作爲參數經常見於像通過爲key參數提供一個函數來自定義sorted內建函數等操作中。但是,將函數作爲返回值返回會怎樣呢?請考慮:

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

這乍看起來有點奇怪。在#1處我返回了變量inner,它碰巧是一個函數標籤。這裏沒有特殊語法——我們的函數返回了inner函數(調用outer()函數並不產生可見的執行)。還記得變量的生命週期嗎?每當outer函數被調用時inner函數就會重新被定義一次,但是如果inner函數不被(outer)返回那麼當超出outer的作用域後,inner將不復存在了。

在#2處我們可以獲取到返回值,它是我們的inner函數,它被存儲於一個新的變量foo。我們可以看到,如果我們計算foo,它真的包含inner函數,我們可以通過使用調用運算符(雙括號,還記得嗎?)來調用它。這看起來可能有點怪異,但是到目前爲止沒有什麼難以理解,不是麼?挺住,因爲接下來的東西將會很怪異。

8、閉包(Closures)

讓我們不從定義而是從另一個代碼示例開始。如果我們將上一個例子稍加修改會怎樣呢?

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

從上一個例子中我們看到inner是一個由outer返回的函數,存儲於一個名爲foo的變量,我們可以通過foo()調用它。但是它能運行嗎?讓我們先來考慮一下作用域規則。

一切都依照Python的作用域規則而運行——x是outer函數了一個local變量。當inner在#1處打印x時,Python在inner中尋找一個local變量,沒有找到;然後它在外層作用域即outer函數中尋找並找到了它。

但是自此處從變量生命週期的角度來看又會如何呢?變量x是函數outer的local變量,這意味着只有當outer函數運行時它才存在。只有當outer返回後我們才能調用inner,因此依照我們關於Python如何運作的模型來看,在我們調用inner的時候x已經不復存在了,那麼某個運行時錯誤可能會出現。

事實與我們的預想並不一致,返回的inner函數的確正常運行。Python支持一種稱爲閉包(function closures)的特性,這意味着定義於非全局作用域的inner函數在定義時記得它們的外層作用域長什麼樣。這可以通過查看inner函數的func_closure屬性來查看,它包含了外層作用域裏的變量。

請記住,每次當outer函數被調用時inner函數都被重新定義一次。目前x的值沒有改變,因此我們得到的每個inner函數和其它的inner函數擁有相同的行爲,但是如果我們將它做出一點改變呢?

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

從這個例子中你可以看到closures——函數記住他們的外層作用域的事實——可以用來構建本質上有一個硬編碼參數的自定義函數。我們沒有將數字1或者2傳遞給我們的inner函數但是構建了能"記住"其應該打印數字的自定義版本。

closures就是一個強有力的技術——你甚至想到在某些方面它有點類似於面向對象技術:outer是inner的構造函數,x扮演着一個類似私有成員變量的角色。它的作用有很多,如果你熟悉Python的sorted函數的key參數,你可能已經寫過一個lambda函數通過第二項而不是第一項來排序一些列list。也許你現在可以寫一個itemgetter函數,它接收一個用於檢索的索引並返回一個函數,這個函數適合傳遞給key參數。

但是讓我們不要用閉包做任何噩夢般的事情!相反,讓我們重新從頭開始來寫一個decorator!

9、裝飾器(Decorators)

一個decorator只是一個帶有一個函數作爲參數並返回一個替換函數的閉包。我們將從簡單的開始一直到寫出有用的decorators。

>>> 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

請仔細看我們的decorator實例。我們定義了一個接受單個參數some_func的名爲outer的函數。在outer內部我們定義了一個名爲inner的嵌套函數。inner函數打印一個字符串然後調用some_func,在#1處緩存它的返回值。some_func的值可能在每次outer被調用時不同,但是無論它是什麼我們都將調用它。最終,inner返回some_func的返回值加1,並且我們可以看到,當我們調用存儲於#2處decorated裏的返回函數時我們得到了輸出的文本和一個返回值2而不是我們期望的調用foo產生的原始值1.

我們可以說decorated變量是foo的一個“裝飾”版本——由foo加上一些東西構成。實際上,如果我們寫了一個有用的decorator,我們可能想用裝飾後的版本來替換foo,從而可以得到foo的“增添某些東西”的版本。我們可以不用學習任何新語法而做到這一點——重新將包含我們函數的變量進行賦值:

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

現在任何對foo()的調用都不會得到原始的foo,而是會得到我們經過裝飾的版本!領悟到了一些decorator的思想嗎?

10、裝飾器的語法糖--@符號(The @ symbol applies a decorator to a function)

Python 2.4通過在函數定義前添加一個@符號實現對函數的包裝。在上面的代碼示例中,我們用一個包裝了的函數來替換包含函數的變量來實現了包裝。

>>> add = wrapper(add)

這一模式任何時候都可以用來包裝任何函數,但是如果們定義了一個函數,我們可以用@符號像下面示例那樣包裝它:

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

請注意,這種方式和用wrapper函數的返回值來替換原始變量並沒有任何不同,Python只是增添了一些語法糖(syntactic sugar)讓它看起來更明顯一點。

11、*args and **kwargs

我們已經寫了一個有用的decorator,但是它是硬編碼的,它只適用於特定種類的函數——帶有兩個參數的函數。我們函數內部的checker函數接受了兩個參數,然後繼續將參數閉包裏的函數。如果我們想要一個能包裝任何類型函數的decorator呢?讓我們實現一個在不改變被包裝函數的前提下對每一次被包裝函數的調用增添一次計數的包裝器。這意味着這個decorator需要接受所有待包裝的任何函數並將傳遞給它的任何參數傳遞給被包裝的函數來調用它(被包裝的函數)。

這種情況很常見,所以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爲我們自動解析了參數,我們也可以像在#1處一樣自己解析出來。這看起來不錯,*args既表示當調用函數是從一個iterable抽取位置參數,也表示當定義一個函數是接受任何額外的位置變量。

當我們引入**時,事情變得更加複雜點,與*表示iterables和位置參數一樣,**表示dictionaries & key/value對。很簡單,不是麼?

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

當我們定義一個函數時我們可以用**kwargs表明所有未捕獲的keyword變量應該被存儲在一個名爲kwargs的字典中。前面的例子中的args和本例中的kwargs都不是Python語法的一部分,但是在函數定義時使用這兩個作爲變量名時一種慣例。就像*一樣,我們可以在函數調用時使用**。

>>> dct = {'x': 1, 'y': 2}
>>> def bar(x, y):
... rturn x + y
>>> bar(**dct)
3

12、更通用的裝飾器(More generic decorators)

用我們掌握的新“武器”我們可以寫一個decorator用來“記錄”函數的參數。爲了簡單起見,我們將其打印在stdout上:

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

注意到在#1處inner函數帶有任意數量的任何類型的參數,然後在#2處將它們傳遞到被包裝的函數中。這允許我們包裝或者裝飾任何函數。

>>> @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

對函數的調用會產生一個"logging"輸出行,也會輸出一個如我們期望的函數返回值。

如果你一直跟到了最後一個實例,祝賀你,你已經理解了decorators了!你可以用你新掌握的“武器”做更好的事情了。你也可能考慮進一步學習,Bruce Eckel有一篇關於decorators的優秀文章,他使用對象而不是函數實現了decorator。你可能會發現OOP代碼比我們的純函數版本更具可讀性。Bruce還有一篇後續文,providing parameters to decorators,用對象實現它可能也比函數實現更簡單。最後,你也可能會去探查一下內建的functoolswraps函數,它是一個可以用在我們的裝飾器中用來修改函數的簽名的裝飾器,修改之後d的函數更像被裝飾的函數。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章