Day 13 物件導向入門
引言
過去十二天,我們寫的都是函式導向的程式:用變數儲存資料、用函式處理資料,再用串列、字典把資料裝在一起。這種寫法在規模小的時候很直覺,但專案一長,函式之間的關係就會變得難以追蹤。光是要記住「哪些函式會改哪些變數」「哪些變數在哪個模組裡」就要花上不少心思,維護起來也容易踩到地雷。
物件導向(OOP)提供了另一種思路:把「資料」與「操作資料的函式」綁在一起,這個組合就稱為「物件」。學會這種寫法,程式碼會更像在描述真實世界的角色與行為,閱讀起來也更貼近自然語言。學物件導向不只是為了寫大型軟體,更是為了看懂接下來的 PyTorch 神經網路:PyTorch 把每一層神經網路定義成一個 class(類別),模型的每一層都是物件;如果你不懂 self、__init__ 與方法呼叫,看 PyTorch 模型程式碼會霧煞煞。今天先用簡單的生活範例把基礎打穩,明天再進到繼承與小專案。
類別與物件:class 的基本結構
類別(class)是物件的藍圖,物件(object)是類別的實例。可以把類別想成「房屋的設計圖」,把物件想成「依照設計圖蓋出來的每一棟房子」。在 Python 中定義類別非常簡單,使用 class 關鍵字即可。類別內的函式稱為方法(method),第一個參數慣例上命名為 self,代表「這個物件自己」。
class Dog:
# 建構子:建立物件時自動執行
def __init__(self, name, age):
self.name = name # 實例屬性
self.age = age
def bark(self):
print(f"{self.name} 汪汪叫!")
def introduce(self):
print(f"我叫 {self.name},今年 {self.age} 歲")
# 建立物件
dog1 = Dog("小黑", 3)
dog1.bark() # 輸出:小黑 汪汪叫!
dog1.introduce() # 輸出:我叫 小黑,今年 3 歲
__init__ 是建構子,在建立物件時會自動呼叫,用來設定初始狀態。self.name = name 的意思是「把傳進來的參數 name 存到這個物件自己的 name 屬性」。呼叫方法時,dog1.bark() 其實是 Python 在幫我們把 dog1 傳給 bark 的 self 參數,所以即使定義時有 self,呼叫端不必再傳一次。這種「方法自動拿到自己」的設計,就是物件導向把資料與行為綁在一起的關鍵。
同一個類別可以建立多個物件,每個物件各自擁有自己的屬性。dog1.name 與 dog2.name 是兩個完全獨立的變數,修改其中一個不會影響到另一個。這種隔離讓我們能用同一個藍圖產出多個獨立個體,是模擬真實世界最直觀的方式。當一個程式需要同時處理多筆同性質的資料(例如 100 張訂單、1000 張圖片),這種設計就能讓我們用一致的介面操作每一筆資料,而不需為每筆單獨寫程式。
屬性:實例屬性與類別屬性
屬性(attribute)是儲存在物件裡的變數。剛剛看到的 self.name 是「實例屬性」,每個物件各自獨立。相對地,「類別屬性」則是所有物件共享的,放在類別內、方法外。類別屬性適合存放所有實例都相同的設定,例如物種、單位、預設值等。
class Dog:
species = "Canis familiaris" # 類別屬性,所有物件共享
def __init__(self, name, age):
self.name = name # 實例屬性,每個物件獨立
self.age = age
dog1 = Dog("小黑", 3)
dog2 = Dog("小白", 5)
print(dog1.species) # 輸出:Canis familiaris
print(dog2.species) # 輸出:Canis familiaris
dog1.age = 4 # 只影響 dog1
print(dog1.age) # 輸出:4
print(dog2.age) # 輸出:5
類別屬性可以透過類別本身或任一個物件存取,但只有從類別修改才會真正改變共享的值。如果用 dog1.species = "其他物種",其實是在 dog1 上「新增」一個實例屬性,把原本的類別屬性遮蔽起來,並沒有改到全體共享的值。如果之後想動態新增屬性,也可以直接 dog1.color = "black",不過建議一開始就在 __init__ 中定義完整,讓結構清楚、除錯時也比較容易找到欄位來源。實務上也建議把類別屬性集中在類別最上方,這樣閱讀程式的人能一眼看到所有物件共用的設定。
方法:實例方法、類別方法、靜態方法
方法(method)是定義在類別內的函式,依照用途可以分成三種。常見的是實例方法,第一個參數是 self,能存取物件的屬性。如果想操作整個類別而不是單一物件,可以用 @classmethod 裝飾,第一個參數是 cls。如果方法和類別、物件都沒關係,只是想歸類在一起,可以用 @staticmethod。
class Circle:
pi = 3.14159
def __init__(self, radius):
self.radius = radius
# 實例方法:用 self 存取物件屬性
def area(self):
return Circle.pi * self.radius ** 2
# 類別方法:用 cls 存取類別屬性,常用來建立替代建構子
@classmethod
def from_diameter(cls, diameter):
return cls(diameter / 2)
# 靜態方法:不存取 self 或 cls,純函式
@staticmethod
def unit():
return "cm"
c1 = Circle(5)
print(c1.area()) # 輸出:78.53975
c2 = Circle.from_diameter(10)
print(c2.radius) # 輸出:5.0
print(Circle.unit()) # 輸出:cm
@classmethod 常用來做「替代建構子」,例如從字串、字典、檔案內容建立物件;@staticmethod 則像一般函式,只是為了組織程式碼而放在類別內。初學者先熟悉實例方法即可,後續兩種看專案需要再使用。當你開始看到別人的程式碼出現 @classmethod,第一直覺應該是「這是用來提供另一種建立物件的方式」,這對理解大型專案的設計會很有幫助。
特殊方法:__str__ 與 __repr__
當你 print 一個物件時,預設會顯示像 <__main__.Dog object at 0x000001> 這種沒什麼用的記憶體位址。透過定義 __str__ 與 __repr__ 這兩個特殊方法(dunder methods,因為名字前後各兩個底線),可以讓 print 與除錯輸出更友善。這些特殊方法會在特定場合被 Python 自動呼叫,是「物件與內建機制溝通」的標準介面。
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
# 給 print() 與 str() 使用
return f"Dog(name={self.name}, age={self.age})"
def __repr__(self):
# 給除錯與互動式環境使用
return f"Dog({self.name!r}, {self.age})"
dog = Dog("小黑", 3)
print(dog) # 輸出:Dog(name=小黑, age=3)
print(repr(dog)) # 輸出:Dog('小黑', 3)
__str__ 偏向給一般使用者看,__repr__ 偏向給開發者除錯用,理想上 __repr__ 應該盡量像一段可執行的程式碼。除了這兩個,還有 __len__、__eq__、__getitem__ 等特殊方法,能讓物件支援 len()、== 比較、[i] 索引這些內建語法,是 Python 物件導向很重要的一環。等我們之後寫到 PyTorch 的自定義層時,就會用到 __call__ 讓物件可以像函式一樣被 invoke,這也是深度學習框架大量依賴的特殊方法機制。
結語
今天從最基本的 class、__init__、self 開始,建立物件導向的基礎觀念。我們了解到實例屬性與類別屬性的差別、三種方法的用途,以及透過 __str__、__repr__ 讓物件輸出更友善。物件導向的核心不是語法,而是「把資料與行為綁在一起」的思考方式,後續寫模型、寫工具都會用上。建議自己動手定義一個簡單的類別,例如 Notebook(記事本)或 Todo(待辦事項),把今天學到的屬性、方法、特殊方法都實際用過一次。動手寫過一次,會比只看文章更能體會 self 與物件狀態的存在感,也會發現實際呼叫時少了 self 反而會更直覺。
明天,我們會進一步學繼承、封裝與多型,再用這些觀念實作一個簡單的記帳小專案,順便為之後 PyTorch 的 nn.Module 鋪路。等你跟著進度寫完兩天,就會發現「類別 + 繼承」其實就是把生活裡的分類思考(動物→狗貓、記帳→收入/支出)直接映射到程式碼,沒有想像中那麼抽象。
留言
張貼留言