# 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)等設計模式。 ## 結語 受限於了解太少,無法用更高層次的視角來分享這些概念,因此結構不易閱讀,還請見諒。