Python 物件導向程式設計(OOP)── __init__ 建構子
簡介
在 Python 的物件導向程式設計中,__init__ 是每個類別最常被使用的特殊方法之一。它扮演「建構子」的角色,負責在物件被實例化時初始化屬性、執行必要的設定工作。若不了解 __init__ 的運作方式,往往會在撰寫類別時遇到屬性未正確設定、資料不一致或難以維護的問題。
本篇文章將以 淺顯易懂 的語言說明 __init__ 的核心概念、常見用法與實務上的最佳實踐,協助 初學者 能快速上手,同時也為 中級開發者 提供更深入的技巧與注意事項。
核心概念
1. __init__ 的基本語法
class MyClass:
def __init__(self, arg1, arg2):
# 這裡的 self 代表即將被建立的物件本身
self.arg1 = arg1
self.arg2 = arg2
# 其他初始化工作可以寫在這裡
self:必須是第一個參數,代表當前物件的實例。Python 會在呼叫建構子時自動傳入。- 其他參數:用來接受外部傳入的資料,並在建構子內部轉換或儲存成屬性。
小技巧:如果建構子不需要額外參數,只寫
def __init__(self):即可,否則一定要保留self。
2. 為什麼要使用 __init__ 而不是在類別外部直接賦值?
- 封裝性:所有屬性的初始設定都集中在建構子裡,外部呼叫者只需要提供必要的參數即可,降低錯誤機率。
- 可讀性:程式碼的意圖一目了然——「建立這個物件時,需要這些資訊」。
- 可維護性:若未來屬性變更,只需要修改
__init__,其他使用該類別的程式碼不必改動。
3. 預設值與關鍵字參數
class Person:
def __init__(self, name: str, age: int = 18, *, gender: str = "未指定"):
self.name = name
self.age = age
self.gender = gender
age有預設值 18,呼叫時可省略。*後面的參數只能以關鍵字方式傳入,提升可讀性。
p1 = Person("Alice") # age 使用預設值 18
p2 = Person("Bob", 25, gender="M") # 使用關鍵字參數
4. 多重繼承與 super().__init__()
在多重繼承的情境下,super() 能協助呼叫父類別的建構子,避免屬性被重複初始化。
class LoggerMixin:
def __init__(self, log_file: str, **kwargs):
super().__init__(**kwargs) # 呼叫下一個 MRO 中的 __init__
self.log_file = log_file
class DataModel(LoggerMixin):
def __init__(self, data_source: str, log_file: str):
super().__init__(log_file=log_file) # 只傳遞給 LoggerMixin
self.data_source = data_source
- 使用
**kwargs讓每層建構子只關心自己需要的參數,其餘交給super()處理。
5. __init__ 不是唯一的建構子
Python 也支援 __new__(負責真正建立物件)以及 類別方法(如 @classmethod def from_json(cls, ...))作為「替代建構子」。
在大多數情況下,我們只需要 __init__,但了解 __new__ 的存在有助於處理不可變類別(如 namedtuple、int)的特殊需求。
程式碼範例
以下提供 5 個實用範例,示範 __init__ 在不同情境下的寫法與技巧。
範例 1:基本的屬性初始化
class Rectangle:
"""簡易的矩形類別,計算面積與周長"""
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
# 使用
rect = Rectangle(3.5, 2.0)
print(f"面積: {rect.area()}, 周長: {rect.perimeter()}")
這個範例展示了 屬性直接從參數賦值 的最簡單寫法。
範例 2:帶預設值與關鍵字參數
class Car:
"""模擬汽車資訊,允許部分屬性使用預設值"""
def __init__(self, brand: str, model: str, year: int = 2020, *, color: str = "黑色"):
self.brand = brand
self.model = model
self.year = year
self.color = color
# 呼叫方式
c1 = Car("Toyota", "Corolla")
c2 = Car("Tesla", "Model 3", 2022, color="紅色")
透過
*強制color必須以關鍵字方式提供,提升程式可讀性。
範例 3:驗證參數合法性
class User:
"""使用者帳號,建構子會檢查 email 格式與年齡範圍"""
def __init__(self, username: str, email: str, age: int):
if "@" not in email:
raise ValueError("email 必須包含 '@'")
if not (0 < age < 120):
raise ValueError("年齡必須在 1~119 之間")
self.username = username
self.email = email
self.age = age
# 正確建立
u = User("alice", "alice@example.com", 30)
# 錯誤示範(會拋出例外)
# u2 = User("bob", "bobexample.com", 25) # ValueError
最佳實踐:在
__init__中盡早驗證輸入,能在錯誤發生時即時回饋使用者。
範例 4:多重繼承與 super()
class JsonSerializable:
"""提供物件轉成 JSON 的功能"""
def __init__(self, **kwargs):
super().__init__(**kwargs) # 確保其他父類別的 __init__ 被呼叫
self._json_fields = []
def to_json(self) -> str:
import json
data = {field: getattr(self, field) for field in self._json_fields}
return json.dumps(data, ensure_ascii=False)
class Product(JsonSerializable):
def __init__(self, name: str, price: float):
super().__init__() # 呼叫 JsonSerializable.__init__
self.name = name
self.price = price
self._json_fields = ["name", "price"]
p = Product("咖啡機", 2999.0)
print(p.to_json()) # {"name": "咖啡機", "price": 2999.0}
透過
super().__init__(),Product自動取得JsonSerializable的初始化流程,未來若再加入其他 Mixin,只需要保持同樣的呼叫方式即可。
範例 5:使用類別方法作為「替代建構子」
class Point:
"""二維座標點,提供從極座標建立的類別方法"""
def __init__(self, x: float, y: float):
self.x = x
self.y = y
@classmethod
def from_polar(cls, r: float, theta_deg: float):
import math
theta_rad = math.radians(theta_deg)
x = r * math.cos(theta_rad)
y = r * math.sin(theta_rad)
return cls(x, y)
# 正常建構
p1 = Point(3, 4)
# 使用替代建構子
p2 = Point.from_polar(5, 53.13) # 大約等於 (3, 4)
print(p2.x, p2.y)
@classmethod讓我們在 不改變__init__參數 的前提下,提供更多建立物件的方式,這在實務開發中相當常見。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記 self |
在 __init__ 定義或呼叫屬性時遺漏 self,會導致 NameError 或屬性未被設定。 |
始終 在方法第一個參數寫 self,且屬性存取時使用 self.<attr>。 |
| 可變預設值 | 使用 list、dict 等可變物件作為預設值,會在所有實例間共享同一個物件。 |
改用 None 作為預設,並在函式內部初始化:if arg is None: arg = []。 |
過度使用 **kwargs |
雖然 **kwargs 能簡化多層繼承,但過度濫用會讓呼叫者不易知道需要傳什麼參數。 |
明確 定義必要參數,僅在 Mixin 或抽象基底類別使用 **kwargs。 |
在 __init__ 中執行耗時操作 |
例如大量 I/O、網路請求,會讓物件建立變慢,影響使用者體驗。 | 把耗時工作延遲到專門的 load() 方法或使用 lazy loading。 |
在 __init__ 中拋出未捕獲的例外 |
若例外未被適當處理,會導致程式在建立物件時直接崩潰。 | 驗證 輸入時使用 raise ValueError,並在呼叫端加入 try/except。 |
最佳實踐要點
- 保持建構子簡潔:只負責屬性初始化與簡單驗證,避免放置業務邏輯。
- 使用型別提示(type hints):
def __init__(self, name: str, age: int):能提升 IDE 輔助與程式可讀性。 - 文件化:在類別或
__init__上方加上 docstring,說明每個參數的意義與預期型別。 - 預設值與關鍵字參數:盡量提供合理的預設值,並使用關鍵字參數提升可讀性。
- 遵循單一職責原則:若建構子需要做太多事,考慮拆成多個輔助方法或使用工廠模式(Factory)。
實際應用場景
| 場景 | 為何需要 __init__ |
範例 |
|---|---|---|
| 資料模型(ORM) | 建構子負責把資料庫欄位映射成屬性,並可在此驗證資料完整性。 | Django Model、SQLAlchemy declarative_base |
| API 客戶端 | 初始化時設定 API 金鑰、基礎 URL、連線逾時等。 | class GithubClient: |
| 圖形介面元件 | 需要在建立元件時設定尺寸、位置、樣式等屬性。 | class Button(QtWidgets.QPushButton): |
| 機器學習模型 | 初始化時載入模型檔案、設定推論裝置(CPU/GPU)。 | class SentimentAnalyzer: |
| 遊戲物件 | 角色、道具等在生成時需要設定座標、生命值、屬性等。 | class Enemy: |
這些情況中,__init__ 為物件提供了「一次性、完整」的設定入口,使得後續的程式碼可以安全且一致地使用該物件。
總結
__init__是 Python 類別的 建構子,負責在物件生成時初始化屬性與執行必要的檢查。- 必須 保留
self作為第一個參數,並在方法內部使用self.<attr>存取屬性。 - 透過 預設值、關鍵字參數、
super(),我們可以寫出彈性高且易於維護的建構子。 - 常見陷阱包括可變預設值、忘記
self、在建構子內執行耗時操作等,遵守最佳實踐可有效避免。 - 在資料模型、API 客戶端、GUI 元件、機器學習與遊戲開發等實務領域,
__init__為物件提供了清晰的初始化介面,是撰寫可讀、可靠程式碼的基石。
掌握了 __init__ 的正確用法,你就能在 Python 的物件導向開發中,建立結構良好、易於擴充的類別,為日後的專案奠定堅實的基礎。祝你寫程式愉快!