# Python底層細節 -- 類別、物件與方法
<!--0727-->
*關於類別、實例以及魔術方法(部分參考自[**《Python进阶系列》十二:最全魔术方法整理**](https://blog.csdn.net/qq_37085158/article/details/124986720)以及chatGPT)。*
## 前言
在學習過程中的很長一段時間內都對這些觀念一知半解,趁著心血來潮整理成筆記。
若有錯誤或可改進之處歡迎指教。
## 類別、實例、屬性與方法
1. 一切都是物件(everything is an object)。
2. 從類別創建出來的物件稱為實例(instance)。實例當然也是物件,但物件不一定是實例。例如類別也是一個物件,但不是實例。使用「實例」一詞時通常是為了強調此物件是來自某個類別。
3. 一切類別皆繼承自`object`(這是一個類別);換言之,所有類別向上追溯,最上層必定是`object`。
4. 自定義新類別時,若無特別指定,預設是繼承自`object`類別。
5. 關於創建實例過程中的一些細節,見下一節的`__new__()`。
6. 定義、設計類別時常見的`self`,其實是指向創建自該類別的實例,而不是類別本身。因此,`self.some_attribute`語意上是指該實例的屬性。
8. 方法也是物件的屬性,如同函數可以用變數儲存一樣。方法分為實例方法(instance method)、類別方法(class method)與靜態方法(static method)。
9. 類別中定義的方法多數時候都需要將`self`作為第一個引數傳入,此稱為「實例方法」。若是類別中定義的方法沒有接受`self`作為參數傳入,那麼這個方法將跟類別、跟實例都無關,並且這個方法無法存取該類別的實例的其他方法或屬性。這樣的方法實際上是**錯誤**的使用方式,應該考慮使用`@staticmethod`或`@classmethod`的裝飾器。
10. 以此程式碼為例:
```py
class MyClass:
def __init__(self): # 初始化
print('__init__() was called.')
def my_method(): # 沒有接受self作為參數傳入
print('my_method() was called.')
def another_method(): # 沒有接受self作為參數傳入
print('another_method() was called.')
self.the_last_method()
def the_last_method(self): # 接受self作為參數傳入,常見的實例方法
print('the_last_method() was called.')
```
```py
my_obj = MyClass()
>>> __init__() was called.
```
接下來,嘗試呼叫`my_obj.my_method()`時,在Python內部實際上是解釋成呼叫`MyClass.my_method(my_obj)`,也就是自動將`self`指向的實例當作傳入方法的第一個引數。因此沒有接受`self`作為參數傳入的方法就會多拿到一個引數,因此會引發錯誤:
```py
my_obj.my_method() # 等同於MyClass.my_method(my_obj)
TypeError: MyClass.my_method() takes 0 positional arguments but 1 was given
```
改成使用`MyClass.my_method()`,透過類別呼叫能正常運作:
```py
MyClass.my_method()
>>> my_method() was called.
```
而因為在`another_method()`中嘗試存取該實例的其他實例方法,因此會產生錯誤(存取實例屬性同理):
```py
MyClass.another_method()
>>> another_method() was called.
>>> NameError: name 'self' is not defined
```
8. 若是在類別底下定義屬性,但沒有與指向實例的`self`綁定,則為「類別屬性」。如下列程式碼:
```py
class MyClass:
att1 = 'my 1st att' # 類別屬性
@classmethod
def print_att1():
print(__class__.att1) # `__class__`會回傳當前的類別:`MyClass`
def change_att1(self):
__class__.att1 = 'att1 was changed'
print('change_att1() was called.')
```
與類別屬性的互動如下:
```py
my_obj = MyClass()
my_obj.att1 # 透過實例存取類別屬性
>>> 'my 1st att'
MyClass.att1 # 透過類別存取類別屬性
>>> 'my 1st att'
my_obj.print_att1()
>>> my 1st att
my_obj.change_att1() # 等同於MyClass.change_att1(my_obj)
>>> change_att1() was called.
MyClass.print_att1()
>>> att1 was changed
```
9. 若是打算透過實例修改類別屬性,會錯誤變成將該屬性添加至實例的屬性中,如下所示。因此應該另外定義類別方法來修改類別屬性。
```python
my_obj.att1 = 'new att'
print(my_obj.att1)
>>> new att
print(MyClass.att1)
>>> att1 was changed
```
11. **方法(method)與函式(function)的差別**:「方法」與類別或類別實例綁定,也屬於類別或實例的屬性;而「函式」是可以從任何地方呼叫的程式碼片段。
12. **類別方法(classmethod)與靜態方法(staticmethod)的差別**:使用`@classmethod`方法後,該方法需將類別作為第一個引數傳入,並且因為沒有傳入指向實例的`self`參數而無法**直接**存取到任何實例(依舊可以在class中追蹤所有實例化的物件並透過如`cls.instances`來存取),適合實現不需創建實例也能調用的行為;而使用`@staticmethod`的裝飾器後,該方法既不接受實例作為引數,也不接受類別作為引數,完全獨立於類別或實例。表現就如同一個定義在類別中的普通函數。[**Python 的 staticmethod 與 classmethod**](https://ji3g4zo6qi6.medium.com/python-tips-5d36df9f6ad5)以及[**解析Python物件導向設計的3種類型方法**](https://www.learncodewithmike.com/2020/01/python-method.html)針對這個主題有更好的說明。
13. 裝飾器的本質是一個函式,它接受函式作為參數,並回傳一個函式。因此可以在不改動原先函式的情況下,實現功能的增強或修改。以下是範例程式碼:
```py
def my_decorator(func):
def wrapper(*args, **kwargs):
# 在 func 被調用前做點什麼
result = func(*args, **kwargs) # 呼叫原始函式
# 在 func 被調用後做點什麼
return result
return wrapper
@my_decorator
def my_function():
print("This is the function.")
```
---
## 魔術方法(Magic Method)
1. 由雙底線開頭並由雙底線結尾。通常由Python自行定義、自行呼叫,但允許改寫。
2. 常見的魔術方法包含:`__init__`、`__str__`、`__name__`、`__class__`、`__repr__`等等。
3. `__str__()`:
想要直接print自定義的物件時,印出來會是難以閱讀的結果,如:
```py
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
```
```py
person1 = Person('Mike', 23)
print(person1) # 輸出結果難以使用
>>> <__main__.Person object at 0x7e7dd18c9a80>
```
其中包含類別名稱以及記憶體位址。若改寫`__str__()`的話就能印出讓自己看得懂的東西:
```py
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f'This is {self.name}, {self.age} years old.'
```
```py
person1 = Person('Mike', 23)
print(person1)
>>> This is Mike, 23 years old.
```
而實際上,Python在print出結果之前執行了**型別轉換(type casting)**,將`print(person1)`解釋成`print(str(person1))`。而`str(person1)`會取得`person1.__str__()`的回傳值,最後再把回傳值印出來。這就是為什麼改寫`__str__()`可以影響print出來的結果。
4. `__new__()`:
`__new__()`是一個類別方法,通常內部長得像這樣:
```py
class MyClass(object):
def __new__(cls, *args, **kwargs):
instance = super(MyClass, cls).__new__(cls, *args, **kwargs)
# 在這裡可以添加額外的實例創建邏輯
return instance
```
而在創建物件時,`__new__()`會在`__init()__`之前被呼叫。它既然在內部呼叫`super().__new__()`,則父類也會呼叫`super().__new__()`,如此遞迴呼叫,最終到達`object`這個類別的`__new__()`,會為實例分配記憶體位址,再從遞迴一層一層根據類別賦予該物件屬性與方法。
5. 實例化是指調用`__new__()`,也就是分配記憶體並且回傳實例的過程;而物件初始化則是調用`__init__()`,也就是賦予物件屬性、方法以及設定屬性初始值的過程。如此分開的邏輯也使得Python的物件創建過程非常靈活,藉由微調這兩個方法也可以實現單例(singleton)等設計模式。
## 結語
受限於了解太少,無法用更高層次的視角來分享這些概念,因此結構不易閱讀,還請見諒。