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

語言: CN / TW / HK

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 或者嘗試實現它。)
我可以繼續聊很多,但這是一個陽光明媚的週日早晨,而我還有其它的計劃... :-)

以上就是本次分享的所有內容,想要了解更多 python 知識歡迎前往公眾號:Python 程式設計學習圈 ,傳送 “J” 即可免費獲取,每日干貨分享