一、前言 #
你有沒有寫過這種程式碼?
status = 1 # 1 = 待處理, 2 = 進行中, 3 = 完成, 4 = 取消
過了三個月回來看——「1 到底是什麼意思?」。或者更慘的是,某天有人不小心傳了 status = 99 進去,程式直接走進一條不存在的分支。
這就是所謂的**魔術數字(magic number)**問題。常數散落在程式碼各處,沒有統一管理,也沒有型別檢查。
Python 的 enum 模組就是解決這個問題的標準方案。它讓你把一組相關的常數定義成一個列舉型別,有名字、有值、有型別安全。如果你這週有在看拍拍君的 Rust 系列,會發現 Rust 的 enum 更強大(可以攜帶資料),但 Python 的 enum 在日常使用中已經非常好用了。
今天就來好好認識它!
二、安裝 #
好消息——不用裝任何東西!enum 是 Python 3.4+ 的標準庫模組:
from enum import Enum
如果你用的是 Python 3.11+,還可以用更方便的 StrEnum。後面會講到。
三、基本用法:定義你的第一個 Enum #
最簡單的列舉 #
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
使用起來:
>>> Color.RED
<Color.RED: 1>
>>> Color.RED.name
'RED'
>>> Color.RED.value
1
>>> Color.RED == Color.RED
True
>>> Color.RED == Color.GREEN
False
>>> Color.RED == 1
False # ⚠️ Enum 成員不等於裸數字!
最後一點很重要:Color.RED == 1 是 False。這就是型別安全——你不會不小心拿一個列舉和一個隨便的數字做比較然後以為它們相等。
迭代與成員存取 #
# 迭代所有成員
for color in Color:
print(f"{color.name} = {color.value}")
# RED = 1
# GREEN = 2
# BLUE = 3
# 用名稱存取
>>> Color["RED"]
<Color.RED: 1>
# 用值存取
>>> Color(2)
<Color.GREEN: 2>
# 用值存取時,值不存在會噴錯
>>> Color(99)
ValueError: 99 is not a valid Color
那個 ValueError 就是我們要的——再也不會有無效的狀態悄悄溜進系統。
四、實用子類別:IntEnum、StrEnum、auto() #
IntEnum:需要和整數比較時 #
有時候你確實需要列舉能和整數相容(例如和舊 API 對接):
from enum import IntEnum
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
>>> Priority.HIGH == 3
True # IntEnum 可以直接和 int 比較
>>> Priority.HIGH > Priority.LOW
True # 支援大小比較
>>> Priority.HIGH + 10
13 # 甚至可以做算術(但請三思)
IntEnum 犧牲了一些型別安全,換來與整數的相容性。只在你確實需要的時候才用它。
StrEnum(Python 3.11+):字串列舉 #
from enum import StrEnum
class Status(StrEnum):
PENDING = "pending"
ACTIVE = "active"
ARCHIVED = "archived"
>>> Status.ACTIVE == "active"
True # 可以直接跟字串比較
>>> f"目前狀態:{Status.ACTIVE}"
'目前狀態:active' # f-string 自動用 value
StrEnum 在 API 開發中超好用。例如你在用 FastAPI 或 Pydantic,可以直接把它當作字串欄位的型別。
auto():讓 Python 自動分配值 #
懶得手動編號?用 auto():
from enum import Enum, auto
class Direction(Enum):
NORTH = auto()
SOUTH = auto()
EAST = auto()
WEST = auto()
>>> list(Direction)
[<Direction.NORTH: 1>, <Direction.SOUTH: 2>, <Direction.EAST: 3>, <Direction.WEST: 4>]
auto() 預設從 1 開始遞增。如果你只在乎名稱不在乎值,這是最簡潔的寫法。
五、進階技巧 #
自訂方法 #
Enum 本質上是個 class,所以你可以加方法:
from enum import Enum
class HttpStatus(Enum):
OK = 200
NOT_FOUND = 404
SERVER_ERROR = 500
@property
def is_error(self):
return self.value >= 400
def describe(self):
descriptions = {
200: "成功",
404: "找不到資源",
500: "伺服器內部錯誤",
}
return descriptions.get(self.value, "未知狀態")
>>> HttpStatus.NOT_FOUND.is_error
True
>>> HttpStatus.OK.describe()
'成功'
Flag:位元旗標組合 #
Flag 讓你用 |(OR)來組合多個值,非常適合表示「權限」之類的概念:
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
# 組合權限
user_perm = Permission.READ | Permission.WRITE
>>> user_perm
<Permission.READ|WRITE: 3>
>>> Permission.READ in user_perm
True
>>> Permission.EXECUTE in user_perm
False
# 典型使用:檔案權限
admin_perm = Permission.READ | Permission.WRITE | Permission.EXECUTE
>>> admin_perm
<Permission.READ|WRITE|EXECUTE: 7>
如果你熟悉 Linux 的 chmod,這應該很眼熟——rwx 就是 7!
確保唯一值:@unique #
預設情況下,Enum 允許多個名稱指向同一個值(別名):
from enum import Enum, unique
# 沒有 @unique 時,這不會報錯
class Shape(Enum):
SQUARE = 2
DIAMOND = 1
CIRCLE = 3
ALIAS_FOR_SQUARE = 2 # 這是 SQUARE 的別名
>>> Shape.ALIAS_FOR_SQUARE is Shape.SQUARE
True # 它們是同一個成員
如果你不希望出現別名(通常不希望),加上 @unique 裝飾器:
@unique
class Shape(Enum):
SQUARE = 2
DIAMOND = 1
CIRCLE = 3
ALIAS_FOR_SQUARE = 2 # 💥 ValueError!
match-case 搭配 Enum(Python 3.10+) #
from enum import Enum, auto
class Command(Enum):
START = auto()
STOP = auto()
RESTART = auto()
def handle(cmd: Command):
match cmd:
case Command.START:
print("啟動服務 🚀")
case Command.STOP:
print("停止服務 🛑")
case Command.RESTART:
print("重啟服務 🔄")
case _:
print("未知命令")
handle(Command.START)
# 啟動服務 🚀
這比一堆 if-elif 清楚多了,而且 IDE 會提醒你是否遺漏了某個 case。
六、實戰範例:訂單狀態機 #
來看一個比較完整的例子——用 Enum 管理訂單狀態的轉換:
from enum import StrEnum, auto
class OrderStatus(StrEnum):
CREATED = "created"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
@property
def next_statuses(self):
"""定義合法的狀態轉換"""
transitions = {
"created": [OrderStatus.PAID, OrderStatus.CANCELLED],
"paid": [OrderStatus.SHIPPED, OrderStatus.CANCELLED],
"shipped": [OrderStatus.DELIVERED],
"delivered": [],
"cancelled": [],
}
return transitions[self.value]
def can_transition_to(self, target: "OrderStatus") -> bool:
return target in self.next_statuses
class Order:
def __init__(self, order_id: str):
self.order_id = order_id
self.status = OrderStatus.CREATED
def update_status(self, new_status: OrderStatus):
if not self.status.can_transition_to(new_status):
raise ValueError(
f"不能從 {self.status.value} 轉換到 {new_status.value}!"
f"允許的目標:{[s.value for s in self.status.next_statuses]}"
)
old = self.status
self.status = new_status
print(f"訂單 {self.order_id}: {old.value} → {new_status.value}")
# 使用
order = Order("ORD-001")
order.update_status(OrderStatus.PAID)
# 訂單 ORD-001: created → paid
order.update_status(OrderStatus.SHIPPED)
# 訂單 ORD-001: paid → shipped
# 嘗試非法轉換
try:
order.update_status(OrderStatus.CREATED)
except ValueError as e:
print(f"❌ {e}")
# ❌ 不能從 shipped 轉換到 created!允許的目標:['delivered']
把狀態轉換規則直接寫在 Enum 裡面,邏輯集中、好維護、好測試。
七、Enum vs 其他方案比較 #
| 方案 | 型別安全 | 自動補全 | 值驗證 | 可迭代 |
|---|---|---|---|---|
魔術數字 status = 1 |
❌ | ❌ | ❌ | ❌ |
常數 STATUS_OK = 1 |
❌ | ⚠️ | ❌ | ❌ |
dict |
❌ | ❌ | ❌ | ✅ |
Enum |
✅ | ✅ | ✅ | ✅ |
八、Python enum vs Rust enum #
既然這週我們也在學 Rust,來個快速比較:
| 特性 | Python enum |
Rust enum |
|---|---|---|
| 基本列舉 | ✅ class Color(Enum) |
✅ enum Color { Red, Green } |
| 攜帶資料 | ❌(只有 name + value) | ✅ Move { x: i32, y: i32 } |
| Pattern matching | ✅ match-case(3.10+) |
✅ match(更強大) |
| 方法 | ✅ 可以加方法 | ✅ impl 區塊 |
| 型別安全 | ⚠️ 部分(IntEnum 會破壞) |
✅ 完全 |
Python 的 enum 比較像「有名字的常數集合」,而 Rust 的 enum 是「可以攜帶不同資料的代數型別」。想深入了解 Rust 這邊的故事,可以看 Rust struct 與 enum。
結語 #
enum 是 Python 標準庫中最被低估的模組之一。它看似簡單,卻能:
- ✅ 消滅魔術數字和魔術字串
- ✅ 提供 IDE 自動補全和型別檢查
- ✅ 防止無效值進入系統
- ✅ 讓程式碼的「意圖」更清晰
下次當你想要定義一組常數的時候,不要再用 STATUS_OK = 1 了——給它一個 Enum 吧!拍拍君保證你以後會感謝自己的 ✨