Python 在類別內定義函式到底為什麼一定要有 self 參數？
Tags: python
Published: 2022-02-11 09:02:38

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

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

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

```python
>>> class A:
...     def class_method():
...         print("class method")
...
```

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

```python
>>> type(A.class_method)
<class 'function'>
>>>
```

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

```python
>>> A.class_method()
class method
>>>
```

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

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

```python
>>> 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
>>>
```

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

```python
>>> type(a.class_method)
<class 'method'>
>>>
```

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

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

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

```python
>>> a.__dict__
{}
>>>
```

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

```python
>>> 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})
>>>
```

發現 `class_method`, 而且它是 `function` 型別, 屬於[**可叫用 (callable)**](https://docs.python.org/3/reference/datamodel.html#index-32) 的物件, 這時 Python 會建立一個 `method` 型別的物件, 並且在其中紀錄想要取用 `class_method` 的物件, 以及 `class_method` 本身。我們可以透過 `method` 物件的 `__self__` 與 `__func__` 來查看：

```python
>>> a.class_method.__self__
<__main__.A object at 0x0000016F7B8B5910>
>>> a.class_method.__func__
<function A.class_method at 0x0000016F7B921E50>
>>>
```

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

```python
>>> 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
>>>
```

這個從類別內的函式變成 `method` 物件的動作稱為[**綁定 (bound)**](https://docs.python.org/3/reference/datamodel.html#index-35), 它會在每次透過類別的實例取用定義在類別內的函式時發生, 也正是在這時, 函式才變成**方法**, 也正因為如此, 要當成方法使用的函式在定義時都需要至少一個參數, 才能接收 `method` 物件所傳入實際上叫用方法的物件。

## 動態變更類別內的函式

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

```python
>>> def instance_method(self):
...     print("instance method")
...
>>> A.instance_method = instance_method
```

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

```python
>>> a.instance_method()
instance method
>>>
```

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

```python
>>> 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'
>>>
```

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

```python
>>> a.__dict__
{'wrong_method': <function instance_method at 0x0000016F7B9215E0>}
>>> type(a.wrong_method)
<class 'function'>
>>>
```

你可以看到 `a.wrong_method` 是 `function` 型別, 不是 `method`。

## 我可以不要取名 self 嗎？

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

## 類別方法也適用綁定規則

如果類別中有使用 `@classmethod` 裝飾器或是 `classmethod()` 定義的[類別方法 (class method)](https://docs.python.org/3/library/functions.html?highlight=classmethod#classmethod), 也適用剛剛描述的綁定規則, 例如：

```python
>>> class B:
...     @classmethod
...     def class_method(cls):
...         print("real class method")
...
>>> B.class_method()
real class method
```

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

```python
>>> b = B()
>>> b.class_method()
real class method
>>>
```

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

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

```python
>>> b.class_method.__func__
<function B.class_method at 0x0000022EE2AEE4C0>
>>> b.class_method.__self__
<class '__main__.B'>
>>>
```

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

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

```python
>>> type(B.class_method)
<class 'method'>
>>>
```

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

```python
>>> id(B.class_method)
2400394917312
>>> id(b.class_method)
2400394917760
```

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

## 小結

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