函式和方法的裝飾器
內容
- 警告警告警告
- 摘要
- 動機
為什麼這很難? - 背景
- 關於“Decorator”名稱
- 設計目標
- 當前語法
- 語法的選擇
裝飾器位置
語法形式
為什麼是@? - 當前實現與歷史
社群共識 - 例子
- (不再是)未決問題
- 參考資料
- 版權
警告警告警告
本文件旨在描述裝飾器語法和做出決定的過程。它既不試圖涵蓋大量潛在的替代語法,也不試圖詳盡列出每種形式的所有優點和缺點。
摘要
當前用於轉換函式和方法的方式(例如,將它們宣告為類或靜態方法)很笨拙,並且可能導致難以理解的程式碼。在理想的情況下,這些轉換應該在程式碼中作宣告的位置進行。本 PEP 引入了對函式或方法宣告作轉換的新語法。
動機
當前對函式或方法作變換的方式會把實際的變換置於函式體之後。對於大型函式,這會將函式行為的關鍵組成部分與其餘的函式外部介面的定義分開。例如:
def foo(self):
perform method operation
foo = classmethod(foo)
對於較長的方法,這變得不太可讀。在概念上只是宣告一個函式,使用其名稱三遍就很不 pythonic。此問題的解決方案是將方法的轉換移到方法本身的宣告附近。新語法的意圖是替換
def foo(cls):
pass
foo = synchronized(lock)(foo)
foo = classmethod(foo)
成為一種將裝飾符放置在函式的宣告中的寫法:
@classmethod
@synchronized(lock)
def foo(cls):
pass
以這種方式來修改類也是可能的,儘管好處不能立即體現。幾乎可以肯定,使用類裝飾器可以完成的任何事情都可以使用元類來完成,但是使用元類非常晦澀,所以就有吸引力找到一種對類進行簡單修改的更簡便的方法。對於 Python 2.4 來說,僅添加了函式/方法裝飾器。
PEP 3129 (譯註:譯文在此) 提議從 Python 2.6 開始新增類裝飾器。
為什麼這很難?
自 2.2 版本以來,Python 中提供了兩個裝飾器(classmethod() 和 staticmethod() )。大約從那時起,就已經假設最終會在語言中新增對它們的一些語法支援。既然有了此假設,人們可能想知道為什麼還會很難達成共識。
在 comp.lang.python 和 python-dev 郵件列表中,關於如何最好地實現函式裝飾器的討論,時不時就會展開。沒有一個明確的爭辯理由,但是如下問題看起來分歧最大。
- 關於“意圖的宣告”放置何處的分歧。幾乎所有人都同意,在函式定義的末尾裝飾/轉換函式不是最佳的。除此之外,似乎沒有明確的共識將這些資訊放在何處。
- 語法約束。Python 是一種語法簡單的語言,除開“搗亂”(無論從外表上還是考慮到語言解析器),對可以完成和不能完成的事情都有相當嚴格的約束。沒有明顯的方法來組織這些資訊,以便剛接觸該概念的人們會想:“哦,是的,我知道你在做什麼。” 看起來最好的辦法就是防止新使用者對語法的含義形成錯誤的心智模型。
- 總體上不熟悉該概念。對於那些熟悉代數(或者只是基本算術)或至少使用過其它程式語言的人來說,Python 的大部分內容都是符合直覺的。但在 Python 中遇到裝飾器概念之前,很少有人會接觸到這個概念。沒有一個很強的先驗模因(preexisting meme)能包含這個概念。
- 語法上的討論所獲得的關注,大體上超過了所有其它東西所獲得的關注。讀者可以看到的三元運算子討論,與PEP 308相關,也是這樣的例子。
背景
人們普遍同意,裝飾器語法對於當前而言是可取的。Guido 在第十屆Python大會 [3] 的 DevDay 主題演講中提到了對裝飾器的語法支援[2],儘管他後來說[5],這只是他“半開玩笑”提議的幾種擴充套件之一。會議結束後不久,Michael Hudson 在 python-dev 上提出了主題[4],將最初的括號語法歸因於Gareth McCaughan [6] 先前在 comp.lang.python 上的提議。
類裝飾器似乎是顯而易見的下一步,因為類定義和函式定義在語法上相似,但是 Guido 仍然有疑慮,類裝飾器幾乎肯定不會在 Python 2.4 中出現。
從 2002 年 2 月到 2004 年 7 月,python-dev 裡的討論一直此起彼伏。數百篇回帖,人們提出了許多可能的語法變體。Guido 列了一份提案清單,帶到 EuroPython 2004 [7] 上討論。之後,他決定使用Java風格的[10] @decorator 語法,該語法在 2.4a2 中首次出現。
Barry Warsaw 將其命名為“pie-decorator”語法,以紀念 Pie-thon Parrot 比賽(譯註:這是當年的一件逸事,Parrot 虛擬機器與 CPython 虛擬機器比賽效能優劣),該事件與裝飾器語法幾乎同時發生,而且 @ 看起來有點像餡餅。Guido 在 Python-dev 上概述了他的要點[8],其中包括 這篇文章[9],談論了一些(許多)被否決的內容。
關於“Decorator”名稱
對於將此特性命名為“decorator”,有很多人抱怨。主要問題是該名稱與GoF書 中的用法不一致[11]。名稱“ decorator”可能更多是用在編譯器領域中——一個語法樹被遍歷和註解。很有可能會出現一個更好的名稱。
設計目標
新的語法應該:
- 適用於任意包裝器(wrapper),包括使用者定義的可呼叫物件以及現有的內建型別classmethod() 和 staticmethod() 。此要求還意味著裝飾器語法必須支援將引數傳遞給 wrapper 的建構函式
- 每個定義需支援多重包裝器
- 過程應清晰可見;至少應該明顯到令新使用者在編寫程式碼時可以安全地忽略它
- 成為一種“……一旦解釋就容易記住的”語法
- 不會使將來的擴充套件變困難
- 易於輸入;使用了它的程式應該期望經常使用它
- 不會對快速瀏覽程式碼造成困難。搜尋所有定義、特定定義或函式的入參應該要容易
- 不應使輔助支援工具,如語言敏感的編輯器和其它“ 玩具解析器工具 ”[12] ,變得複雜化
- 允許將來的編譯器針對裝飾器進行優化。Python 的 JIT 編譯器有希望在將來成為現實,這就要求裝飾器的語法要先於函式的定義
- 從當前隱藏的函式末尾,移到最前面[13]
安德魯·庫奇林(Andrew Kuchling)在他的部落格[14]中連結了許多有關動機和用例的討論。特別值得注意的是Jim Huginin 的用例列表[15]。
當前語法
當前在 Python 2.4a2 中實現的函式裝飾器的語法為:
@dec2
@dec1
def func(arg1, arg2, ...):
pass
這等效於:
def func(arg1, arg2, ...):
pass
func = dec2(dec1(func))
但沒有對變數 func 的過渡性賦值。裝飾器靠近函式的宣告。@ 符號清楚地表明這裡正在發生新的事情。
應用順序[16](從下到上)的基本原理是,它與函式用途的一般順序相匹配。在數學中,組合函式 (g o f)(x) 會轉換為 g(f(x))。在 Python 中,"@g @f def foo()" 轉換為 foo = g(f(foo))。
裝飾器語句是被約束的——任意的表示式都不能用。Guido 出於直覺[17],更喜歡這種方式。
當前語法還允許裝飾器在宣告時,可以呼叫一個返回裝飾器的函式:
@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
pass
這等效於:
func = decomaker(argA, argB, ...)(func)
使用返回裝飾器的函式的基本原理是,@ 符號後的部分可以被視為表示式(儘管句法上被限為一個函式),然後該表示式返回的任何內容將被呼叫。參見宣告引數[16]。
語法的選擇
大量的[18]不同語法被提了出來——與其嘗試令這些語法單獨起作用,更值得將它們分為多個領域討論。試圖單獨討論每種可能的語法[19]將是一種瘋狂的舉動,並且會產生一個完全不明智的 PEP。
裝飾器位置
第一個語法點是裝飾器的位置。對於以下示例,我們使用了 2.4a2 中的 @ 語法。
def 語句之前的裝飾器是第一種選擇,並且在 2.4a2 中就使用了它:
@classmethod
def foo(arg1,arg2):
pass
@accepts(int,int)
@returns(float)
def bar(low,high):
pass
有許多人對該位置提出了反對意見——最主要的反對意見是,這是 Python 中第一個真正的前一行程式碼會對下一行產生影響的情況。2.4a3 中可用的語法要求每行一個裝飾器(在 a2 中,可以在同一行上指定多個裝飾器),最後在 2.4 的最終版本中,每行只保留一個裝飾器。
人們還抱怨說,當使用多個裝飾器時,語法很快會變得笨重。但是,有人指出,在單個函式上使用大量裝飾器的可能性很小,因此這並不是一個大問題。
這種形式的一些優點是裝飾器位於方法的主體之外——顯然,它們是在定義函式時執行的。
另一個好處是,寫在函式定義的前面,適合在不知道程式碼內容時,就改變程式碼的語義,也就是說,你知道如何正確地解釋程式碼的語義,如果該語法沒有出現在函式定義之前,你需要回看並改變初始的理解。
Guido 決定他更喜歡[20]在“def”的前面行裡放置裝飾器,因為長長的引數列表就意味著裝飾器最好被“隱藏”起來 。
第二種形式是把裝飾器放在 def 與函式名稱之間,或者在函式名稱與引數列表之間:
def @classmethod foo(arg1,arg2):
pass
def @accepts(int,int),@returns(float) bar(low,high):
pass
def foo @classmethod (arg1,arg2):
pass
def bar @accepts(int,int),@returns(float) (low,high):
pass
對該形式有兩個異議。第一,它很容易破壞原始碼的“可擴充套件性”——你無法再通過搜尋“def foo(”來找到函式的定義;第二,更嚴重的是,在使用多個裝飾器的情況下,語法將會非常笨拙。
接下來的一種形式,它有一定數量的堅定支持者,就是把裝飾器放在"def"行的引數列表與末尾的“:”號之間:
def foo(arg1,arg2) @classmethod:
pass
def bar(low,high) @accepts(int,int),@returns(float):
pass
Guido 將反對這種形式的論點(其中許多也適用於以前的形式)總結 [13]為:
- 它把重要的資訊(例如,這是一種靜態方法)藏在了簽名之後,很容易就看漏
- 很容易錯過長引數列表和長裝飾器列表之間的過渡資訊
- 剪下並貼上裝飾器列表以進行重用很麻煩,因為它在程式碼行的中間開始和結束
下一種形式是將裝飾器語法放在方法體的開頭,與當前文件字串(doctring)的所在位置相同:
def foo(arg1,arg2):
@classmethod
pass
def bar(low,high):
@accepts(int,int)
@returns(float)
pass
對此形式的主要反對意見是,它需要“窺視”方法體才能確定裝飾器。另外,即使裝飾器程式碼在方法體內,但它並不是在執行方法時執行。Guido 認為 docstring 並不構成一個很好的反例,甚至“docstring”裝飾器很有可能有助於將 docstring 移到函式體之外。
最後一種形式是用一個程式碼塊將方法的程式碼巢狀起來。在此示例中,我們將使用“decorate”關鍵字,因為 @ 語法毫無意義。
decorate:
classmethod
def foo(arg1,arg2):
pass
decorate:
accepts(int,int)
returns(float)
def bar(low,high):
pass
這種形式將導致被裝飾方法和非裝飾方法的縮排不一致。此外,被裝飾的方法體將從第三層縮排開始。
語法形式
@decorator:
@classmethod
def foo(arg1,arg2):
pass
@accepts(int,int)
@returns(float)
def bar(low,high):
pass
反對這種語法的主要意見是 Python 中當前未使用過 @ 符號(IPython 和 Leo 均使用了@符號),並且 @ 符號沒有意義。另一個反對意見是,這會將當前未使用的字元(從有限的集合中)“浪費”在不被認為是主要用途的事物上。
| decorator:
|classmethod
def foo(arg1,arg2):
pass
|accepts(int,int)
|returns(float)
def bar(low,high):
pass
這是 @decorator 語法的一個變體——它的優點是不會破壞 IPython 和 Leo。與 @ 語法相比,它的主要缺點是 | 符號看起來像大寫字母 I 和小寫字母 l。
列表語法:
[classmethod]
def foo(arg1,arg2):
pass
[accepts(int,int), returns(float)]
def bar(low,high):
pass
對列表語法的主要反對意見是它當前是有意義的(當在方法之前使用時)。而且也沒有任何跡象表明該表示式是個裝飾器。
使用其它括號(<…>,[[…]],…)的列表語法:
<classmethod>
def foo(arg1,arg2):
pass
<accepts(int,int), returns(float)>
def bar(low,high):
pass
這些替代寫法都沒有太大的吸引力。涉及其它括號的寫法僅用於使裝飾器構造得不像是個列表。它們沒有做到任何使解析變得更容易的事情。'<…>'寫法存在解析問題,因為'<'和'>'已經解析為未配對。它們還引起了進一步的解析歧義,因為右尖括號(>)可能是一個大於號,而不是裝飾器的閉合符。
decorate()
該寫法提議不用新的語法來實現——它提議用一個可自省的魔術函式來控制其後的函式。Jp Calderone 和 Philip Eby 都提供了此功能的實現。Guido 堅決反對這一點——不用新的語法,這樣的函式的魔力會極其高:
通過 sys.settraceback 使用具有“遠距動作”(action-at-a-distance)功能的函式,可能會適合一種潛在的功能,該功能無法通過其它任何不更改語言的方式實現,但是對於裝飾器而言,情況並非如此。此處普遍持有的觀點是,需要新增裝飾器作為一種語法功能,以避免 2.2 和 2.3 中使用的字尾表示法帶來的問題。裝飾器被認定為一項重要的新語言功能,其設計需要具有前瞻性,而不是受到 2.3 版中可以實現的東西所約束。
新關鍵字(和程式碼塊)
這個想法是來自 comp.lang.python 的共識(有關更多資訊,請參見下面的社群共識。)Robert Brewer 撰寫了詳細的J2 提案[21]文件,概述了支援這種形式的論點。此形式的最初問題有:
- 它需要一個新關鍵字,因此還需要一個"from __future__ import decorators"的語句。
- 關鍵字的選擇仍有爭議。但是,"using"已成為該共識的選擇,並被用於提案和實現中。
- 關鍵字/程式碼塊形式會產生類似於普通程式碼塊的內容,但並不是。嘗試在此塊中使用語句將導致語法錯誤,這可能會使使用者感到困惑。
幾天後,Guido 出於兩個主要理由拒絕了該提案[22]。首先:
… 縮排塊的句法形式強烈暗示了其內容應為語句序列,但實際上它卻不是——只有表示式是允許的,並且這些表示式存在隱式的“收集中”狀態,直到它們可以被應用在隨後的函式定義為止。…
其次:
… 關鍵字開始於塊的開頭,會引起很多關注。對於“ if”、“ while”、“ for”、“ try”、“ def”和“ class”,這是正確的。但是,“ using”關鍵字(或其它位置的關鍵字)不值得引起這種關注。重點應該放在裝飾器或裝飾器套件上,因為它們是隨後的函式定義的重要裝飾符。…
其它形式
Wiki 頁面[23]上還有許多其它變體和提議。
為什麼是@?
Java 中有一些時間最初使用 @ 作為Javadoc 註釋[24]中的標記,後來在 Java 1.5 中用作註解[10],這與 Python 的裝飾器相似。@ 以前沒有在 Python 中用作標記的事實也意味著,很顯然早期版本的 Python 不可能解析此類程式碼,從而可能導致細微的語義錯誤。這也意味著,什麼是裝飾器和什麼不是裝飾器,這種不確定性被移除了。也就是說,@ 仍然是一個相當隨意的選擇。有些人建議使用 | 代替。
對於使用類似列表的語法(無論出現在何處)來指定裝飾器,一些替代方法被提了出來:[| … |],
[…]
和 <…>。
當前實現與歷史
Guido 徵集一名志願者來實現他所偏好的語法,Mark Russell 響應並向 SF 提交了補丁[25]。這個新語法在 2.4a2 中可用。
@dec2
@dec1
def func(arg1, arg2, ...):
pass
這等效於:
def func(arg1, arg2, ...):
pass
func = dec2(dec1(func))
儘管沒有在中間建立名為 func 的變數。
在 2.4a2 中實現的版本允許在一行上包含多個 @decorator 子句。在 2.4a3 版中,此規定已嚴格限制為每行只允許一個裝飾器。
Michael Hudson 的一個實現了“list-after-def”語法的 早期補丁[26] 還繼續活躍著。
在釋出 2.4a2 之後,Guido 表示,如果社群可以達成社群共識、提供一份體面的提案和實現方案,他將對社群提案進行重新稽核,以迴應社群的反應。在出現了驚人數量的帖子之後,Python Wiki [18]收集了大量的替代方案,社群共識出現了(見下)。Guido 隨後拒絕了此方案[22],但補充說:
在 Python 2.4a3(將於本週四釋出)中,一切還儲存在 CVS 中。對於 2.4b1,我將考慮將 @ 更改為其它單個字元,儘管我認為 @ 具有與 Java 類似功能所使用的相同字元的優點。有人認為這並不完全相同,因為 Java 中的 @ 用於不更改語義的屬性。但是 Python 的動態特性使它的語法元素永遠不會與其它語言中的類似構造具有完全相同的含義,並且肯定存在明顯的重疊。關於對第三方工具的影響:IPython 的作者認為不會有太大影響;Leo 的作者說 Leo 將倖免於難(儘管這將使他和他的使用者有一些過渡性的痛苦)。我實際上覺得選擇一個在 Python 語法中其它地方已經使用過的字元,可能會使外部工具更難以適應,因為在這種情況下解析將變得更加微妙。但坦率地說,我還沒有決定,所以這裡有些擺動的空間。我現在不想再考慮其它的語法選擇:必須在某個時候停止,每個人都有話說,但演出必須繼續。
社群共識
本節記錄了被否決的 J2 語法,為了歷史的完整性而將其包括在內。
在 comp.lang.python 上出現的共識是要提議 J2 語法(“J2”是在 PythonDecorators Wiki 頁面上的叫法):在 def 語句之前,作為字首的新關鍵字using 及裝飾器程式碼塊。例如:
using:
classmethod
synchronized(lock)
def func(cls):
pass
該語法的主要論點來自“可讀性計數”(readability counts)學說。簡而言之,它們是:
- 一個套件比多個 @ 行更好。using 關鍵字和其程式碼塊將單塊的 def 語句轉換成多塊的複合結構,類似於 try/finally 和其它。
- 關於識別符號(token),關鍵字比標點符號更好。關鍵字與識別符號的現有用法相符。不需要新的識別符號類別。關鍵字將 Python 裝飾器與 Java 註解和 .Net 屬性區分開,它們顯而易見並非同類。
羅伯特·布魯爾(Robert Brewer)為此形式撰寫了詳細的提案[21],邁克爾·斯帕克斯(Michael Sparks)製作了補丁[27]。
如前所述,Guido 否決了此形式,並在給 python-dev 和 comp.lang.python 的訊息[22]中概述了它的問題。
例子
在 comp.lang.python 和 python-dev 郵件列表裡的許多討論,都集中在裝飾器的使用上,認為它是一種比 staticmethod() 和 classmethod() 內建函式更簡潔的方法。當然其能力要比那個強大得多。本節介紹了一些使用示例。
定義在退出時執行的函式。請注意,該函式實際上並不是通常意義上的“包裝”。
def onexit(f):
import atexit
atexit.register(f)
return f
@onexit
def func():
...
請注意,此示例可能不適合實際使用,僅用於演示目的。
用單例例項定義一個類。請注意,一旦類消失,進取的程式設計師需要更有創造力才能建立更多的例項。(出自 python-dev 上的 Shane Hathaway )
def singleton(cls):
instances = {}
def getinstance():
if cls not in instances:
instances[cls] = cls()
return instances[cls]
return getinstance
@singleton
class MyClass:
...
向一個函式新增屬性。(基於 Anders Munch 在 python-dev 上釋出的示例)
def attrs(**kwds):
def decorate(f):
for k in kwds:
setattr(f, k, kwds[k])
return f
return decorate
@attrs(versionadded="2.2",
author="Guido van Rossum")
def mymethod(f):
...
限定函式引數和返回型別。請注意,這會將 func_name 屬性從舊函式複製到新函式。func_name 在 Python 2.4a3 中是可寫的:
def accepts(*types):
def check_accepts(f):
assert len(types) == f.func_code.co_argcount
def new_f(*args, **kwds):
for (a, t) in zip(args, types):
assert isinstance(a, t), \
"arg %r does not match %s" % (a,t)
return f(*args, **kwds)
new_f.func_name = f.func_name
return new_f
return check_accepts
def returns(rtype):
def check_returns(f):
def new_f(*args, **kwds):
result = f(*args, **kwds)
assert isinstance(result, rtype), \
"return value %r does not match %s" % (result,rtype)
return result
new_f.func_name = f.func_name
return new_f
return check_returns
@accepts(int, (int,float))
@returns((int,float))
def func(arg1, arg2):
return arg1 * arg2
宣告一個類實現特定的一個(一組)介面。摘自 Bob Ippolito 在 python-dev 上發表的文章,基於其在PyProtocols [28]的經驗基礎上。
def provides(*interfaces):
"""
An actual, working, implementation of provides for
the current implementation of PyProtocols. Not
particularly important for the PEP text.
"""
def provides(typ):
declareImplementation(typ, instancesProvide=interfaces)
return typ
return provides
class IBar(Interface):
"""Declare something about IBar here"""
@provides(IBar)
class Foo(object):
"""Implement something here..."""
當然,儘管沒有語法上的支援,但所有這些示例如今都是可能的。
(不再是)未決問題
尚不確定類裝飾器是否會在將來整合到 Python 中。Guido 表達了對這一概念持懷疑態度,但不同的人在 python-dev 裡提出了一些有力的論據[29](搜尋 PEP 318 -- 發帖草案)。類裝飾器在 Python 2.4 中是極不可能的。
PEP 3129 [#PEP-3129]提議從 Python 2.6 開始新增類裝飾器。
@ 字元的選擇將在 Python 2.4b1 之前重新檢查。(最後,@ 字元被保留。)
以上就是本次分享的所有內容,想要了解更多 python 知識歡迎前往公眾號:Python 程式設計學習圈 ,傳送 “J” 即可免費獲取,每日干貨分享
- 介紹一款能取代 Scrapy 的爬蟲框架 - feapder
- 直觀講解一下 RPC 呼叫和 HTTP 呼叫的區別!
- MySQL 億級資料分頁的優化
- Python 多執行緒小技巧:比 time.sleep 更好用的暫停寫法!
- Python面試官:請說說併發場景鎖怎麼用?
- Python如何非同步傳送日誌到遠端伺服器?
- Python 中的數字到底是什麼?
- 如何建立一個完美的 Python 專案?
- 詳解 Python 的二元算術運算,為什麼說減法只是語法糖?
- Python 為什麼沒有 main 函式?為什麼我不推薦寫 main 函式?
- Bug分析,假刪除導致文章釋出成功卻打不開的問題
- Python 進階:queue 佇列原始碼分析
- Python例項篇:自動操作Excel檔案(既簡單又特別實用)
- 誰說程式設計師不懂浪漫,當代碼遇到文學..
- Python 為什麼沒有 void 關鍵字?
- 程式語言中分號“;”的簡明歷史
- Python 什麼情況下會生成 pyc 檔案?
- 函式和方法的裝飾器
- Python 任務自動化工具:nox 的配置與 API
- 你可能不知道的 Python 技巧