函式講完,這篇補上「帶著狀態的可呼叫物件」——class。Python 的 OOP 比 Java 自由很多:沒有真正的 private、屬性是動態加上去的、用 duck typing 取代 interface。Python 也提供 @dataclass、@property、dunder 方法這些大幅減少樣板碼的工具,這篇一次串完。
定義 class
class User:
def __init__(self, name, age): # constructor
self.name = name # instance attribute
self.age = age
def greet(self):
return f'Hi, I am {self.name}'
u = User('Jeremy', 30)
print(u.name) # 'Jeremy'
print(u.greet()) # 'Hi, I am Jeremy'
要點:
__init__是初始化方法(不是真的 constructor,instance 由__new__建立)self是「目前 instance」的明示參數,呼叫時不用傳,是 Python 的設計取捨- 屬性直接賦值就建立,沒有「宣告」的概念
class attribute vs instance attribute
class Counter:
total = 0 # class attribute(所有 instance 共用)
def __init__(self):
self.value = 0 # instance attribute(每個 instance 獨立)
def inc(self):
self.value += 1
Counter.total += 1
陷阱:可變 class attribute 會被所有 instance 共用:
class Bag:
items = [] # ❌ 所有 instance 共用同一個 list
def add(self, x):
self.items.append(x)
a = Bag()
b = Bag()
a.add(1)
b.items # [1] ← 不該共用
正確:把可變狀態放 __init__ 內。
方法種類:instance / class / static
class Pizza:
def __init__(self, size):
self.size = size
def describe(self): # instance method
return f'Pizza size {self.size}'
@classmethod
def small(cls): # class method(cls = 類別本身)
return cls(8)
@classmethod
def large(cls):
return cls(16)
@staticmethod
def is_valid_size(size): # 與 class / instance 都無關
return 4 <= size <= 32
p = Pizza.small()
Pizza.is_valid_size(10)
@classmethod 常拿來當替代 constructor,提供多種建立方式。
@staticmethod 等同放在 class namespace 下的純函式,少用。
屬性可見性的「君子協定」
Python 沒有真的 private,靠命名慣例:
| 命名 | 意思 |
|---|---|
name | 公開 |
_name | 「內部用」,外部請別碰(純慣例,仍可存取) |
__name | name mangling,會被改成 _ClassName__name(避免子類別不小心覆蓋,不是真隱藏) |
class A:
def __init__(self):
self.__secret = 42
a = A()
a.__secret # ❌ AttributeError
a._A__secret # ✅ 仍然拿得到
@property:把方法包裝成屬性
class Circle:
def __init__(self, r):
self._r = r
@property
def area(self):
return 3.14159 * self._r ** 2
@property
def radius(self):
return self._r
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError('negative')
self._r = value
c = Circle(5)
c.area # 用屬性語法呼叫,不用括號
c.radius = 10 # 走 setter,會做驗證
對外像屬性、對內可加邏輯,是 Python 替代「getter / setter」的標準做法。
繼承
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return 'some sound'
class Dog(Animal):
def speak(self): # 覆寫
return 'woof'
class Puppy(Dog):
def __init__(self, name, age):
super().__init__(name) # 呼叫父類 init
self.age = age
def speak(self):
return f'{super().speak()} (small)'
p = Puppy('Rex', 1)
p.speak() # 'woof (small)'
多重繼承與 MRO
Python 支援多重繼承,靠 MRO(Method Resolution Order,C3 線性化) 決定方法查找順序:
class A:
def hi(self): return 'A'
class B(A):
def hi(self): return 'B'
class C(A):
def hi(self): return 'C'
class D(B, C):
pass
D.__mro__ # (D, B, C, A, object)
D().hi() # 'B'
super() 不是「父類」,而是「MRO 的下一個」。實務上多用 mixin,少做深層多重繼承。
多型
Python 是 duck typing,「會叫的就是鴨子」。沒有 interface,也不用 instanceof 判斷型別:
def make_speak(animal):
return animal.speak() # 只要有 speak 方法就行
class Dog:
def speak(self): return 'woof'
class Cat:
def speak(self): return 'meow'
make_speak(Dog())
make_speak(Cat())
新型寫法可用 Protocol(typing 模組)做結構性子型別檢查,最後一篇會講。
dunder methods(魔術方法)
雙底線開頭結尾的方法,定義 class 怎麼跟運算子、內建函式互動。
| dunder | 觸發場景 |
|---|---|
__init__ | Foo() 初始化 |
__new__ | 建立 instance(極少自己寫) |
__del__ | 被回收時 |
__repr__ | repr(x) / debug 印出 |
__str__ | str(x) / print(x) |
__eq__, __hash__ | == / 放進 set / dict key |
__lt__ 等 | <、>、<=、>= |
__len__ | len(x) |
__iter__, __next__ | for 走訪 |
__getitem__, __setitem__ | x[i] |
__contains__ | in 運算子 |
__call__ | x(...) 把 instance 當 callable |
__enter__, __exit__ | with 上下文管理器 |
__enter__ / __exit__ 是 context manager 協議,用來搭配 with 語法管理需要成對 open/close 的資源(檔案、網路連線、鎖等)。進入 with 區塊時呼叫 __enter__,離開時(不管是正常結束還是丟 exception)呼叫 __exit__。
範例:
class Vec:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f'Vec({self.x}, {self.y})'
def __add__(self, other):
return Vec(self.x + other.x, self.y + other.y)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
a = Vec(1, 2)
b = Vec(3, 4)
print(a + b) # Vec(4, 6)
{a, Vec(1, 2)} # {Vec(1, 2)}(去重)
實作 __eq__ 通常要一起實作 __hash__,否則 instance 無法放進 set / dict key。
為什麼要一起實作?Python 規定「相等的物件必須有相同 hash」(反之不需要)。若只覆寫 __eq__ 不管 __hash__,Python 會自動把 __hash__ 設為 None——instance 不能放進 set、不能當 dict 的 key。想保持 hashable 就要兩個都寫。
dataclass:少寫樣板
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p) # Point(x=1, y=2)
p == Point(1, 2) # True
@dataclass 自動產生 __init__、__repr__、__eq__。其他選項:
@dataclass(frozen=True, slots=True)
class Config:
host: str
port: int = 8080
frozen=True:immutable,賦值會丟錯slots=True:用__slots__,省記憶體、限制不能新增未宣告屬性- 也支援
field(default_factory=list)處理可變預設值
abstract base class
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save(self, key: str, value: str) -> None: ...
@abstractmethod
def load(self, key: str) -> str: ...
class MemoryStorage(Storage):
def __init__(self):
self.data = {}
def save(self, key, value):
self.data[key] = value
def load(self, key):
return self.data[key]
# Storage() # ❌ 不能實例化抽象類別
m = MemoryStorage() # ✅
繼承後沒實作所有 @abstractmethod 就無法實例化。
下一篇進模組與套件,把這篇散在單檔的 class 怎麼跨多檔組織、怎麼包成可發布的 package 講清楚。
Latest Updates
- 2026.06.11 Content updated
