PEP 318 – 函式和方法的裝飾器
- 作者:
- Kevin D. Smith <Kevin.Smith at theMorgue.org>,Jim J. Jewett,Skip Montanaro,Anthony Baxter
- 狀態:
- 最終版
- 型別:
- 標準跟蹤
- 建立日期:
- 2003年6月5日
- Python 版本:
- 2.4
- 釋出歷史:
- 2003年6月9日,2003年6月10日,2004年2月27日,2004年3月23日,2004年8月30日,2004年9月2日
警告警告警告
本文件旨在描述裝飾器語法以及導致這些決策的流程。它不試圖涵蓋大量潛在的替代語法,也不試圖詳盡列出每種形式的所有優缺點。
摘要
當前轉換函式和方法(例如,將它們宣告為類方法或靜態方法)的方法很笨拙,可能導致難以理解的程式碼。理想情況下,這些轉換應該在宣告本身的程式碼的同一位置進行。此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中新增類裝飾器。
為什麼這如此困難?
自Python 2.2版本以來,Python中提供了兩個裝飾器(classmethod()和staticmethod())。從那時起,人們就一直認為最終會向語言中新增對它們的語法支援。鑑於此假設,人們可能會想知道為什麼如此難以達成共識。在comp.lang.python和python-dev郵件列表中,關於如何最好地實現函式裝飾器的討論時斷時續。沒有一個明確的原因說明為什麼會這樣,但一些問題似乎最具爭議。
- 關於“意圖宣告”屬於何處的爭論。幾乎所有人都同意在函式定義結束時裝飾/轉換函式是次優的。除此之外,似乎沒有明確的共識將此資訊放置在哪裡。
- 語法限制。Python是一種語法簡單的語言,對什麼可以做和不能做有相當嚴格的限制,而不會“搞砸”(無論是視覺上還是關於語言解析器)。沒有明顯的方法可以構造這些資訊,以便新手會認為“哦,是的,我知道你在做什麼。”似乎最好的辦法是防止新使用者對語法含義形成 wildly incorrect 的心理模型。
- 對概念的整體陌生。對於對代數(甚至基本算術)有一點了解或至少使用過一種其他程式語言的人來說,Python的很多內容都是直觀的。很少有人在Python中遇到裝飾器概念之前有過任何經驗。沒有強大的預先存在的模因來捕捉這個概念。
- 一般來說,語法討論似乎比其他任何事情都更容易引起爭議。讀者可以參考與PEP 308相關的三元運算子討論,作為另一個例子。
背景
普遍認為語法支援比現狀更可取。Guido在他的第10屆Python大會DevDay主題演講中提到了裝飾器的語法支援,儘管他後來表示這只是他“半開玩笑”提出的幾個擴充套件之一。Michael Hudson在會議後不久在python-dev上提出了這個話題,將最初的括號語法歸因於Gareth McCaughan之前在comp.lang.python上的一個提議。
類裝飾看起來是顯而易見的下一步,因為類定義和函式定義在語法上相似,但是Guido仍然不相信,並且類裝飾幾乎肯定不會出現在Python 2.4中。
討論從2002年2月到2004年7月在python-dev上斷斷續續地進行。釋出了數百數百篇文章,人們提出了許多可能的語法變體。Guido帶著一份提案列表參加了EuroPython 2004,在那裡進行了討論。此後,他決定我們將採用Java風格的@裝飾器語法,這首次出現在2.4a2中。Barry Warsaw將此命名為“pie-decorator”語法,以紀念在裝飾器語法同時發生的Pie-thon Parrot槍戰,並且因為@看起來有點像一個餡餅。Guido在Python-dev上概述了他的理由,包括關於一些(許多)被拒絕的形式的這篇文章。
關於名稱“裝飾器”
關於此功能選擇名稱“裝飾器”的抱怨很多。主要原因是該名稱與其在GoF書中的用法不一致。“裝飾器”這個名稱可能更多地源於它在編譯器領域的使用——語法樹被遍歷和註釋。很有可能出現更好的名稱。
設計目標
新語法應該
- 適用於任意包裝器,包括使用者定義的可呼叫物件和現有的內建函式
classmethod()和staticmethod()。此要求還意味著裝飾器語法必須支援向包裝器建構函式傳遞引數 - 每個定義使用多個包裝器
- 使其發生的事情顯而易見;至少,新使用者在編寫自己的程式碼時可以安全地忽略它應該很明顯
- 是一個“……一旦解釋就容易記住”的語法
- 不使未來的擴充套件更加困難
- 易於輸入;使用它的程式預計會非常頻繁地使用它
- 不使其更難以快速瀏覽程式碼。仍然應該易於搜尋所有定義、特定定義或函式接受的引數
- 不會不必要地使次要支援工具(例如語言敏感編輯器和其他“玩具解析器工具”)複雜化
- 允許未來的編譯器最佳化裝飾器。隨著Python JIT編譯器在某個時候出現的希望,這往往要求裝飾器語法出現在函式定義之前
- 從函式的末尾(目前它被隱藏在那裡)移到前面,在那裡它更引人注目
Andrew Kuchling在他的部落格中提供了許多關於動機和用例的討論連結。特別值得注意的是Jim Huginin的用例列表。
當前語法
Python 2.4a2中實現的函式裝飾器當前語法是
@dec2
@dec1
def func(arg1, arg2, ...):
pass
這等效於
def func(arg1, arg2, ...):
pass
func = dec2(dec1(func))
無需將中間賦值給變數func。裝飾器靠近函式宣告。@符號明確表明這裡正在發生一些新事情。
應用順序(從下到上)的理由是它符合函式應用的通常順序。在數學中,函式組合 (g o f)(x) 轉換為 g(f(x))。在Python中,@g @f def foo() 轉換為 foo=g(f(foo))。
裝飾器語句可以接受的內容受到限制——任意表達式將不起作用。Guido更喜歡這一點,因為有一種直覺。
當前語法還允許裝飾器宣告呼叫返回裝飾器的函式
@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
pass
這等效於
func = decomaker(argA, argB, ...)(func)
擁有一個返回裝飾器的函式的理由是,@符號後面的部分可以被視為一個表示式(儘管在語法上僅限於一個函式),並且該表示式返回的任何內容都會被呼叫。參見宣告引數。
語法替代方案
已經提出了大量不同的語法——與其嘗試逐個處理這些語法,不如將語法討論分解為幾個領域。嘗試單獨討論每種可能的語法將是一種瘋狂行為,並會產生一個完全笨拙的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決定他更喜歡將裝飾器放在“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
對此形式有幾個反對意見。首先是它會破壞原始碼的易於“grep”性——您不能再搜尋“def foo(”並找到函式的定義。第二個更嚴重的反對意見是,在有多個裝飾器的情況下,語法會非常笨拙。
下一種形式,有許多強烈支持者,是將裝飾器放在引數列表和“def”行中的尾隨:之間
def foo(arg1,arg2) @classmethod:
pass
def bar(low,high) @accepts(int,int),@returns(float):
pass
Guido 總結了反對這種形式的論點(其中許多也適用於前一種形式),如下所示
- 它將關鍵資訊(例如,它是靜態方法)隱藏在簽名之後,容易被忽略
- 很容易錯過長引數列表和長裝飾器列表之間的過渡
- 剪下和貼上裝飾器列表以供重用很麻煩,因為它開始和結束在行中間
下一種形式是裝飾器語法在方法體內部開頭,與文件字串現在所在的位置相同
def foo(arg1,arg2):
@classmethod
pass
def bar(low,high):
@accepts(int,int)
@returns(float)
pass
反對這種形式的主要理由是它需要“窺視”方法體才能確定裝飾器。此外,即使程式碼在方法體內部,它也不會在方法執行時執行。Guido認為文件字串不是一個好的反例,而且很有可能“文件字串”裝飾器可以幫助將文件字串移到函式體之外。
最後一種形式是一個包圍方法程式碼的新塊。對於這個例子,我們將使用“decorate”關鍵字,因為它與@語法無關。
decorate:
classmethod
def foo(arg1,arg2):
pass
decorate:
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
反對這種語法的主要反對意見是,@符號目前未在Python中使用(但在IPython和Leo中都有使用),並且@符號沒有意義。另一個反對意見是,這“浪費”了一個目前未使用的字元(來自有限的集合)來處理一個不被認為是主要用途的東西。
|裝飾器:|classmethod def foo(arg1,arg2): pass |accepts(int,int) |returns(float) def bar(low,high): pass
這是@裝飾器語法的一個變體——它的優點是它不會破壞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()decorate()提案是*不*實現新語法——而是使用一個魔術函式,透過自省來操作後續函式。Jp Calderone和Philip Eby都實現了這樣的函式。Guido對此非常堅決地反對——如果沒有新語法,這樣的函式的魔術性將非常高透過sys.settraceback使用具有“遠距離作用”的函式可能對於一個無法透過其他方式獲得但又不需要修改語言的晦澀功能來說是可以的,但這不適用於裝飾器。這裡普遍的觀點是,裝飾器需要作為一種語法特性新增,以避免2.2和2.3中使用的字尾表示法的問題。裝飾器註定是一個重要的新的語言特性,其設計需要具有前瞻性,而不是受限於2.3中可以實現的功能。- 新關鍵字(和塊)
這個想法是comp.lang.python的共識替代方案(更多內容見下面的社群共識)。Robert Brewer撰寫了一份詳細的J2提案文件,概述了支援這種形式的論點。這種形式的最初問題是
- 它需要一個新關鍵字,因此需要一個
from __future__ import decorators語句。 - 關鍵字的選擇有爭議。然而,
using成為了共識選擇,並用於提案和實現中。 - 關鍵字/塊形式產生了一個看起來像普通程式碼塊的東西,但實際上不是。嘗試在此塊中使用語句會導致語法錯誤,這可能會使使用者感到困惑。
幾天後,Guido 拒絕了該提案,主要基於兩點,首先是
...縮排塊的語法形式強烈暗示其內容應該是一系列語句,但實際上並非如此——只允許表示式,並且正在對這些表示式進行隱式“收集”,直到它們可以應用於隨後的函式定義為止。...其次
...塊開頭的關鍵字會引起很多關注。對於“if”、“while”、“for”、“try”、“def”和“class”都是如此。但是“using”關鍵字(或任何其他代替它的關鍵字)不值得這種關注;重點應該放在套件內部的裝飾器或裝飾器上,因為它們是對後續函式定義的重要修飾符。...邀請讀者閱讀完整的回覆。
- 它需要一個新關鍵字,因此需要一個
- 其他形式
wiki頁面上還有很多其他變體和提案。
為什麼是@?
Java在Javadoc註釋中最初使用@作為標記,後來在Java 1.5中用於註解,這與Python裝飾器類似,有一些歷史淵源。@以前未在Python中用作標記的事實也意味著此類程式碼不可能被早期版本的Python解析,從而可能導致微妙的語義錯誤。這也意味著消除了什麼是裝飾器和什麼不是裝飾器的歧義。儘管如此,@仍然是一個相當任意的選擇。有人建議使用|代替。
對於使用列表式語法(無論出現在何處)指定裝飾器的語法選項,提出了幾種替代方案:[|...|]、*[...]*和<...>。
當前實現,歷史
Guido要求志願者實現他偏愛的語法,Mark Russell挺身而出,向SF提交了一個補丁。這種新語法在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之前的一個實現了def後列表語法的補丁也仍然存在。
2.4a2釋出後,為了回應社群的反應,Guido表示,如果社群能夠提出社群共識、一份不錯的提案和一份實現,他將重新審視社群提案。在令人驚訝的大量帖子之後,在Python wiki中收集了大量的替代方案後,社群共識出現了(如下)。Guido隨後拒絕了這種替代形式,但補充說
在Python 2.4a3(本週四釋出)中,一切保持與CVS中相同。對於2.4b1,我將考慮將@更改為其他單個字元,儘管我認為@的優點是它與Java中類似功能使用的字元相同。有人爭論說它不完全相同,因為Java中的@用於不改變語義的屬性。但Python的動態特性使得它的語法元素與T其他語言中的類似結構永遠不會完全相同,並且確實存在顯著的重疊。關於對第三方工具的影響:IPython的作者認為不會有太大影響;Leo的作者表示Leo會繼續存在(儘管會給他和他的使用者帶來一些過渡性痛苦)。我實際上預計,選擇一個Python語法中已在其他地方使用的字元可能更難讓外部工具適應,因為在這種情況下解析將不得不更微妙。但我坦率地說還沒有決定,所以這裡還有一些迴旋餘地。我目前不想考慮進一步的語法替代方案:事情總要有個了斷,每個人都發表了自己的看法,並且演出必須繼續。
社群共識
本節記錄了被拒絕的J2語法,併為了歷史完整性而包含在內。
在comp.lang.python上出現的共識是提議的J2語法(“J2”是它在PythonDecorators wiki頁面上的引用方式):在新關鍵字using字首的def語句之前的裝飾器塊。例如
using:
classmethod
synchronized(lock)
def func(cls):
pass
支援這種語法的主要論點屬於“可讀性很重要”的原則。簡而言之,它們是
- 一套語句勝過多個@行。
using關鍵字和塊將單塊def語句轉換為多塊複合結構,類似於try/finally等。 - 對於新標記,關鍵字比標點符號更好。關鍵字與現有的標記使用相匹配。不需要新的標記類別。關鍵字將Python裝飾器與Java註解和.Net屬性區分開來,後者是顯著不同的事物。
Robert Brewer為這種形式撰寫了詳細提案,Michael Sparks則製作了一個補丁。
如前所述,Guido拒絕了這種形式,並在一封傳送給python-dev和comp.lang.python的訊息中概述了他遇到的問題。
示例
關於comp.lang.python和python-dev郵件列表的大部分討論都集中在使用裝飾器作為使用staticmethod()和classmethod()內建函式更簡潔的方式。此功能比這強大得多。本節提供了一些使用示例。
- 定義一個在退出時執行的函式。請注意,該函式實際上並未按通常意義進行“包裝”。
def onexit(f): import atexit atexit.register(f) return f @onexit def func(): ...
請注意,此示例可能不適用於實際使用,僅用於示例目的。
- 使用單例例項定義一個類。請注意,一旦類消失,有進取心的程式設計師將不得不更有創意地建立更多例項。(來自Shane Hathaway在
python-dev上的帖子。)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的經驗。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..."""
當然,所有這些示例在今天都是可能實現的,儘管沒有語法支援。
(不再)未解決的問題
版權
本文件已置於公共領域。
來源:https://github.com/python/peps/blob/main/peps/pep-0318.rst