---
# System prepended metadata

title: Python底層細節 -- 類別、物件與方法
tags: [公開, Python]

---

# 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）等設計模式。

## 結語
受限於了解太少，無法用更高層次的視角來分享這些概念，因此結構不易閱讀，還請見諒。