DEV Community

codemee
codemee

Posted on • Edited on

Python 在類別內定義函式到底為什麼一定要有 self 參數?

在類別內定義函式時, 大家想必寫過無數次的 self 參數, 或是漏掉 self 參數在叫用時被噴了錯誤訊息, 你也許會覺得為什麼不能像是其他物件導向程式語言一樣, 自動來個 this 不是簡單很多嗎?這主要是 Python 在實作物件系統上有很不同的思維。

類別中定義的函式和一般的函式沒什麼不一樣

其實在類別中定義函式的確並不一定要有 self 參數, 例如:

>>> class A:
...     def class_method():
...         print("class method")
...
Enter fullscreen mode Exit fullscreen mode

如果使用 type 檢查, 它就像是一般在類別外定義的函式一樣會告訴你它是 function 型別:

>>> type(A.class_method)
<class 'function'>
>>>
Enter fullscreen mode Exit fullscreen mode

我們也的確可以像是一般函式那樣叫用它:

>>> A.class_method()
class method
>>>
Enter fullscreen mode Exit fullscreen mode

只是必須以剛剛產生的 A 類別物件來取用, 這時類別的用途就像名稱空間

如果我們嘗試建立一個 A 類別的物件, 再透過這個物件來叫用定義在類別中的函式, 就會噴出錯誤訊息:

>>> a = A()
>>> a.class_method()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: class_method() takes 0 positional arguments but 1 was given
>>>
Enter fullscreen mode Exit fullscreen mode

錯誤訊息說 class_method() 不需要位置參數, 但叫用的時候卻傳入了 1 個位置參數, 明明我們叫用的時候什麼都沒傳, 為什麼錯誤訊息中會說傳入了 1 個參數呢?如果使用 type 來檢查, 就會發現奇怪的現象:

>>> type(a.class_method)
<class 'method'>
>>>
Enter fullscreen mode Exit fullscreen mode

你會看到剛剛明明是 function 型別, 怎麼現在變成是 method 型別了?

類別的函式變身為類別實例中的方法

上面的現象就是當我們透過類別的實例取用定義在類別內的函式時, Python 會先在實例中找尋是否有符合指定名稱的屬性, 如果你看 a 物件的 __dict__ 字典, 就會發現是空的, 根本沒有 class_method

>>> a.__dict__
{}
>>>
Enter fullscreen mode Exit fullscreen mode

這時 Python 會往 a.__class__ 物件找, 看看它認不認識 class_method

>>> a.__class__.__dict__
mappingproxy({'__module__': '__main__', 'class_method': <function A.class_method at 0x0000016F7B921E50>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None})
>>>
Enter fullscreen mode Exit fullscreen mode

發現 class_method, 而且它是 function 型別, 屬於可叫用 (callable) 的物件, 這時 Python 會建立一個 method 型別的物件, 並且在其中紀錄想要取用 class_method 的物件, 以及 class_method 本身。我們可以透過 method 物件的 __self____func__ 來查看:

>>> a.class_method.__self__
<__main__.A object at 0x0000016F7B8B5910>
>>> a.class_method.__func__
<function A.class_method at 0x0000016F7B921E50>
>>>
Enter fullscreen mode Exit fullscreen mode

當你對 method 型別的物件進行叫用 (call) 操作時, 執行的是 method 型別客製版本的 __call__, 這個客製版本實際上執行的是叫用 __func__, 並將 __self__ 插入成為第一個參數, 因此, 以下各種叫用方式都會發生一樣的錯誤:

>>> a.class_method.__func__(a.class_method.__self__)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: class_method() takes 0 positional arguments but 1 was given
>>> a.class_method()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: class_method() takes 0 positional arguments but 1 was given
>>>
>>> a.class_method.__call__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: class_method() takes 0 positional arguments but 1 was given
>>>
Enter fullscreen mode Exit fullscreen mode

這個從類別內的函式變成 method 物件的動作稱為綁定 (bound), 它會在每次透過類別的實例取用定義在類別內的函式時發生, 也正是在這時, 函式才變成方法, 也正因為如此, 要當成方法使用的函式在定義時都需要至少一個參數, 才能接收 method 物件所傳入實際上叫用方法的物件。

動態變更類別內的函式

使用 class 定義的類別自己也是個物件, 我們可以隨意變更它的內容, 接著就來幫它新增一個方法:

>>> def instance_method(self):
...     print("instance method")
...
>>> A.instance_method = instance_method
Enter fullscreen mode Exit fullscreen mode

由於新增的函式符合變身為方法的要求, 因此可以透過類別的實例叫用:

>>> a.instance_method()
instance method
>>>
Enter fullscreen mode Exit fullscreen mode

這裡必須注意, 要新增方法必須將函式加在類別上, 如果加在類別的實例上, 並不符合前述綁定方法的規則, 例如:

>>> a.wrong_method = instance_method
>>> a.wrong_method()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: instance_method() missing 1 required positional argument: 'self'
>>>
Enter fullscreen mode Exit fullscreen mode

錯誤訊息告訴我們所叫用的函式需要 1 個位置參數, 但卻沒有傳入任何參數, 這就是因為當取用 a.wrong_method 時, a 自己就認得 wrong_method, 並不是往回在類別中找到, 因此不會依循綁定方法的規則建立 method 物件, 所以是當成一般函式。你可以透過 a.__dict__ 以及 type(a.wrong_method) 來確立:

>>> a.__dict__
{'wrong_method': <function instance_method at 0x0000016F7B9215E0>}
>>> type(a.wrong_method)
<class 'function'>
>>>
Enter fullscreen mode Exit fullscreen mode

你可以看到 a.wrong_methodfunction 型別, 不是 method

我可以不要取名 self 嗎?

依照上述, 其實 self 就是一個普普通通的參數, 你當然不一定要取名為 'self', 不過 Python 是一個高度依賴慣例 (convention) 的程式語言, 官方用 'self'、大家都用 'self', 你不用就會讓你的程式不容易懂, 還是順從主流, 乖乖地用 'self', 不但維持一致性, 而且一看就知道這個函式要做為方法用, 其中的 self 參數會接收到叫用此方法的物件。

類別方法也適用綁定規則

如果類別中有使用 @classmethod 裝飾器或是 classmethod() 定義的類別方法 (class method), 也適用剛剛描述的綁定規則, 例如:

>>> class B:
...     @classmethod
...     def class_method(cls):
...         print("real class method")
...
>>> B.class_method()
real class method
Enter fullscreen mode Exit fullscreen mode

類別方法與單純定義在類別中的函式最大的差異就是類別方法可以透過該類別的實例來叫用:

>>> b = B()
>>> b.class_method()
real class method
>>>
Enter fullscreen mode Exit fullscreen mode

咦?等等, 這個 class_method() 需要傳入一個參數, 但是透過類別物件叫用時傳入的是什麼呢?

如果我們觀察所取得的 method 物件, 會發現綁定到類別方法的 method 物件其 __self__ 屬性紀錄的是類別物件自己, 而不是取用此類別方法的物件:

>>> b.class_method.__func__
<function B.class_method at 0x0000022EE2AEE4C0>
>>> b.class_method.__self__
<class '__main__.B'>
>>>
Enter fullscreen mode Exit fullscreen mode

由於傳入的是類別物件本身, 因此類別方法的第 1 個參數慣例上就命名為 cls

其實 @classmethod 裝飾完的結果就已經是 method 物件了:

>>> type(B.class_method)
<class 'method'>
>>>
Enter fullscreen mode Exit fullscreen mode

綁定方法時又建立了新的 method 物件, 以下可以看到兩個 method 物件並不是同一個物件:

>>> id(B.class_method)
2400394917312
>>> id(b.class_method)
2400394917760
Enter fullscreen mode Exit fullscreen mode

基本上, 只要是透過實例取用類別中可叫用的物件, 都適用綁定方法的規則。

小結

雖然你不一定要瞭解上述的運作原理也可以善用類別與物件, 不過透過解析背後的運作方式, 更能體會 Python 程式語言的設計思維, 相信往後在定義類別時, 一定更能得心應手。

Top comments (0)