HOWTO · Python
Python 中的類方法
本教程討論 Python 中的類方法。
本頁內容
類別是物件的藍圖,但也能保存它所建立的每個物件共用的資料與行為。Python 提供例項方法、類方法與靜態方法,讓程式碼能清楚表達操作需要處理的位置。
例項方法的第一個引數會接收一個例項,慣例上命名為 self。當操作需要某個特定物件的資料時,例如員工的姓名或年齡,就應使用例項方法。類方法的第一個引數則接收類別物件,慣例上命名為 cls。它可以直接透過類別呼叫,不必先建立例項,也能讀取或更新類別層級的狀態。
傳給 cls 的值是實際用來呼叫方法的類別,而不是任意例項。這項特性使類方法特別適合用於替代建構函式:方法可以驗證輸入並回傳 cls(...),讓子類別取得自身的例項。靜態方法不會接收 self 或 cls;它是恰好放在類別中的具名工具函式。
Python 提供兩種受支援的方式建立類方法:使用 classmethod() 轉換既有函式,或使用 @classmethod 裝飾器。對於新程式碼,裝飾器通常更清楚,因為方法的繫結方式就在定義處可見。轉換形式仍適合維護已經有一般函式的程式碼。
在 Python 中使用 classmethod() 函式建立類方法
classmethod() 函式接受一個函式並回傳類方法描述器。先在類別中定義函式,再將轉換後的描述器指派回該屬性。在此範例中,當以 Emp.department_name() 呼叫時,department_name 的 cls 會收到 Emp,因此能取得共用的 department 屬性。
class Emp:
department = "Python"
def department_name(cls):
print(f"Department: {cls.department}")
Emp.department_name = classmethod(Emp.department_name)
Emp.department_name()
輸出:
Department: Python
這種轉換不表示 classmethod() 已經過時;它是調整既有方法的有效方式。然而,對於新定義,以下的裝飾器形式更容易瀏覽,也不會將函式定義與轉換分開。不論使用哪一種形式,當方法的用途是類別層級的設定、建構或其他不依賴單一物件的行為時,請透過類別呼叫它。
在 Python 中使用 @classmethod 裝飾器建立類方法
將 @classmethod 直接放在方法定義上方,即可將其第一個參數繫結至類別。參數名稱只是慣例而非語言規定,但 cls 能清楚表達方法的角色。方法可使用 cls 存取類別屬性、呼叫其他類方法,或建立新物件,而不必硬編碼基底類別的名稱。
以下以慣用的裝飾器形式完成同樣的類別層級查詢:
class Emp:
department = "Python"
@classmethod
def department_name(cls):
print(f"Department: {cls.department}")
Emp.department_name()
輸出:
Department: Python
類方法能被繼承。關鍵在於,即使方法定義於父類別,像 Manager.from_record(...) 這樣的呼叫仍會將 cls 繫結至 Manager。因此工廠方法應回傳 cls(...),而不是 Employee(...):後者總是會建立父類別型別。Python 不會依引數型別選擇方法來實作同名方法多載;當這些方式更能描述 API 時,請使用不同的方法名稱、選用引數,或像具名替代建構函式這樣的類方法。
下列工廠方法會解析簡單的紀錄、將年齡轉為整數,並回傳呼叫它的類別。這是成功案例,因為輸入恰好含有兩個非空白、以逗號分隔的欄位。
class Employee:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_record(cls, record):
name, age_text = record.split(",")
return cls(name, int(age_text))
employee = Employee.from_record("Matt,15")
print(f"{employee.name}: {employee.age}")
輸出:
Matt: 15
此呼叫中 from_record() 會回傳 Employee 物件,因為 cls 是 Employee。子類別可重複使用此方法而無須重寫解析邏輯。例如,先定義 class Manager(Employee): pass,再呼叫 Manager.from_record("Ava,30"),就會建立 Manager,因為此時 cls 指向 Manager。
輸入驗證應放在外部文字轉為結構化資料的邊界。split(",") 可能產生錯誤數量的欄位,int() 也可能拒絕非數字年齡。請提出清楚的 ValueError,讓呼叫端能刻意處理格式錯誤的紀錄,而不是得到狀態不可靠的物件。以下邊界案例會捕捉該錯誤並印出可預測的訊息。
class Employee:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_record(cls, record):
parts = record.split(",")
if len(parts) != 2 or not all(part.strip() for part in parts):
raise ValueError("record must contain a name and age")
name, age_text = parts
try:
return cls(name.strip(), int(age_text))
except ValueError as error:
raise ValueError("age must be an integer") from error
# boundary case: a record without an age is rejected
try:
Employee.from_record("Matt,")
except ValueError as error:
print(f"Invalid record: {error}")
輸出:
Invalid record: record must contain a name and age
當行為需要某個物件的狀態時,例如更新 self.age,請選擇例項方法。當行為需要類別共用的狀態,或必須建立接收呼叫的類別時,請選擇類方法。當邏輯在概念上屬於類別、卻不需要例項或類別狀態時,請選擇靜態方法。靜態方法仍可透過類別呼叫,但因為 Python 不會傳入 cls 引數,它們無法自動檢視或修改類別屬性。這項區分能讓 API 更直接,並讓讀者與維護者看見每個方法所依賴的項目。