本文 AI 產出,尚未審核

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__ 的存在有助於處理不可變類別(如 namedtupleint)的特殊需求。


程式碼範例

以下提供 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>
可變預設值 使用 listdict 等可變物件作為預設值,會在所有實例間共享同一個物件。 改用 None 作為預設,並在函式內部初始化:if arg is None: arg = []
過度使用 **kwargs 雖然 **kwargs 能簡化多層繼承,但過度濫用會讓呼叫者不易知道需要傳什麼參數。 明確 定義必要參數,僅在 Mixin 或抽象基底類別使用 **kwargs
__init__ 中執行耗時操作 例如大量 I/O、網路請求,會讓物件建立變慢,影響使用者體驗。 把耗時工作延遲到專門的 load() 方法或使用 lazy loading
__init__ 中拋出未捕獲的例外 若例外未被適當處理,會導致程式在建立物件時直接崩潰。 驗證 輸入時使用 raise ValueError,並在呼叫端加入 try/except

最佳實踐要點

  1. 保持建構子簡潔:只負責屬性初始化與簡單驗證,避免放置業務邏輯。
  2. 使用型別提示(type hints)def __init__(self, name: str, age: int): 能提升 IDE 輔助與程式可讀性。
  3. 文件化:在類別或 __init__ 上方加上 docstring,說明每個參數的意義與預期型別。
  4. 預設值與關鍵字參數:盡量提供合理的預設值,並使用關鍵字參數提升可讀性。
  5. 遵循單一職責原則:若建構子需要做太多事,考慮拆成多個輔助方法或使用工廠模式(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 的物件導向開發中,建立結構良好、易於擴充的類別,為日後的專案奠定堅實的基礎。祝你寫程式愉快!