Python 為什麼要保留顯式的 self?

花下貓語:前兩天,我偶然在一個知識星球(劉欣老師的“碼農翻身”)裡看到一篇主題,劉老師表示 Python 的類方法非要帶個 self,而不像其它語言那樣隱藏起來,這讓人很不爽。我對此也有同感。在經過群聊討論後,我獲知 Guido 曾經專門撰文解釋過這個問題。這篇文章並不好懂,我抽空先翻譯出來了,看看能收到什麼回應。如果可能的話,後續再另寫文章分析。

--------------以下為譯文---------------

布魯斯·埃克爾(Bruce Eckel)發了篇博文 ,提議從類方法的形參列表中刪除“self”。我將解釋為什麼這個提議不能通過。(譯註:Bruce 是《Thinking in Java》、《Thinking in C++》等多本書籍的作者,也是個 Python 開發者。他的文章總結了當年在巴西 Pycon 上的一次討論,主要觀點是在定義類方法時,形參中的“self”是多餘的,而且由它引發的報錯信息具有一定的誤導性。)

Bruce 的提議

Bruce 知道,我們需要一種方法來區分對實例變量的引用和對其它變量的引用,因此他建議將“self”設為關鍵字。

考慮一種典型的類,它有一個方法,例如:

 
class C:
def meth(self, arg):
self.val = arg
return self.val

跟據 Bruce 的提議,這將變為:

 
class C:
def meth(arg): # Look ma, no self!
self.val = arg
return self.val

這樣每個方法會節省 6 個字符。但我不覺得 Bruce 提出這個建議是為了減少打字。

我認為他真正關心的是程序員(可能來自其它語言)所浪費的時間,有時候似乎不需要指定“self”參數,而且他們偶爾忘記了要加(即使他們十分清楚——習慣是一種強大的力量)。確實,與忘記在實例變量或方法引用之前鍵入“self.”相比,從參數列表中省略“self”,往往會導致很模糊的錯誤消息。

也許更糟糕的是(如 Bruce 所述),當正確地聲明瞭方法,但是在調用時的參數數量不對,這時收到的錯誤消息。如 Bruce 給出的以下示例:

 
Traceback (most recent call last):
File "classes.py", line 9, in
obj.m2(1)
TypeError: m2() takes exactly 3 arguments (2 given)

我贊同它是令人困惑的,但是我寧願去解決此錯誤消息,而不是修改語言。

為什麼 Bruce 的提議不可行

首先,讓我提出一些與 Bruce 的提議相反的典型論點。

這有一個很好的論據可以證明,在參數列表中使用顯式的“self”,可以增強以下兩種調用方法在理論上的等效性。假設“ foo”是“C”的一個實例:

 
foo.meth(arg) == C.meth(foo, arg)

(譯註:說實話,我沒有理解這個例子的意思。以下僅是個人看法。在類的內部定義方法時,可能會產生幾種不同的方法:實例方法類方法靜態方法 。它們的作用和行為是不同的,那麼在定義和調用時怎麼做區分呢?Python 約定了一種方式,即在定義時用第一個參數作區分:self 表示實例方法、cls或其它符號 表示類方法……三種方法都可以被類的實例調用,而且看起來一模一樣,如上例的等號左側那樣。這時候就要靠定義時賦予的參數來區分了,像上例等號右側,第一個參數是實例對象,表明此處是個實例方法。)

另一個論據是,在參數列表中使用顯式的“self”,將一個函數插入一個類,獲得動態地修改一個類的能力,創建出相應的一個類方法。

例如,我們可以創建一個與上面的“C”完全等效的類,如下所示:

 
# Define an empty class:
class C:

pass

# Define a global function:
def meth(myself, arg):
myself.val = arg
return myself.val

# Poke the method into the class:
C.meth = meth

請注意,我將“self”參數重命名為“myself”,以強調(在語法上)我們不是在此處定義一個方法(譯註:類外部的是函數 ,即 function,類內部的是方法 ,即 method)。

這樣之後,C 的實例就具有了一個“meth”方法,該方法有一個參數,且功能跟之前的完全一樣。對於在把方法插入類之前就創建的那些 C 的實例,它甚至也適用。

我想 Bruce 並不特別在意前述的等效性。我同意這只是理論上的重要。我能想到的唯一例外是舊式的調用超級方法的習語(idiom)。但是,這個習語很容易出錯(正是由於需要顯式地傳遞"self"的原因),這就是為什麼在 Python 3000 中,我建議在所有情況下都使用"super()"的原因。

Bruce 可能會想到一種使第二個等效例子起作用的方法——在某些情況下,這種等效性真的很重要。我不知道 Bruce 花了多少時間思考如何實現他的提議,但是我想他正在考慮將一個名為“self”的額外形參自動地添加到直接地在類內部定義的所有方法的思路(我必須說是“直接地”,以便那些嵌套在方法內部的函數,能免於這種自動操作)。這樣,可以使第一個等效例子保持等效。

但是,有一種情況我認為 Bruce 不能在不向編譯器中添加某種 ESP 的情況下解決:裝飾器。 我相信這是 Bruce 的提議的最終敗筆。

當裝飾一個方法時,我們不知道是否要自動地給它加一個“self”參數:裝飾器可以將函數變成一個靜態方法(沒有“self”)或一個類方法(有一個有趣的 self,它指向一個類而不是一個實例),或者可以做一些完全不同的事情(用純 Python 實現“ @classmethod”或“ @staticmethod”的裝飾器是繁瑣的)。除非知道裝飾器的用途,否則沒有其它辦法來確定是否要賦予正在定義的方法一個隱式的“self”參數。

我拒絕諸如特殊包裝的“ @classmethod”和“ @staticmethod”之類的黑科技。我也認為除了自檢外,自動地確定某個方法是類方法(class method)、實例方法(instance method)還是靜態方法(static method),這不是一個好主意(就像在 Bruce 的文章的評論中,有人建議的那樣):這使得很難僅僅根據方法前的“def”,來決定應該怎樣調用該方法。

(譯註:對於一個方法,在當前的添加了相應參數的情況下,可以簡單地加裝飾器,區分它是哪種方法,調用時也容易區分調用;但是,如果沒有加參數,即使可以用神奇的自動機制來區分出它是哪種方法,但在調用時,你不好確定該怎麼調用)。

在評論中,我看到了一些非常極端的對 Bruce 的提議的附和,但通常的代價是使得規則難以遵循,或者要求對語言進行更深層的修改,這令我們極其難以接受它,特別是合入 Python 3.1。順便說一句,對於 3.1,再次聲明我們的規則,新特性只有在保持向後兼容的情況下才是可接受的。

有一個似乎可行的建議(可以使它向後兼容)是把類中的

 
def foo(self, arg): ...

改成這樣的語法糖:

 
def self.foo(arg): ...

但我不認同它把“self”變為保留字(reserved word),或者要求前綴必須是“self”。如果這樣做了,那對於類方法,很容易也出現這種情況:

 
@classmethod
def cls.foo(arg): ...

好了,相比於現狀,我並沒有更喜歡這個。但是相比於 Bruce 的提議或在他的博客評論區中提出的更極端的說法,我認為這個要好得多,而且它具有向後兼容的巨大優勢,並且不需要很費力,就可以寫成帶有參考實現的 PEP。(我想 Bruce 應該會發現自己提案中的缺陷,如果他真的付出努力嘗試編寫可靠的 PEP 或者嘗試實現它。)

我可以繼續聊很多,但這是一個陽光明媚的週日早晨,而我還有其它的計劃... :-)

作者:Guido van Rossum,寫於:2008.10.26

英文: https://neopythonic.blogspot.com/2008/10/why-explicit-self-has-to-stay.html

作者簡介: Guido van Rossum,Python 的創造者,一直是“終身仁慈獨裁者”,直到 2018 年 7 月 12 日退位。目前,他是新的最高決策層的五位成員之一,依然活躍在社區中。

譯者簡介: 豌豆花下貓,生於廣東畢業於武大,現為蘇漂程序員,有一些極客思維,也有一些人文情懷,有一些溫度,還有一些態度。公眾號:「Python貓」(python_cat)。


分享到:


相關文章: