快轉到主要內容
  1. 教學文章/

Python enum:打造型別安全的常數管理

·5 分鐘· loading · loading · ·
Python Enum 型別安全 設計模式
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 37: 本文

一、前言
#

你有沒有寫過這種程式碼?

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 == 1False。這就是型別安全——你不會不小心拿一個列舉和一個隨便的數字做比較然後以為它們相等。

迭代與成員存取
#

# 迭代所有成員
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 開發中超好用。例如你在用 FastAPIPydantic,可以直接把它當作字串欄位的型別。

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 吧!拍拍君保證你以後會感謝自己的 ✨

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 37: 本文

相關文章

Python contextlib:掌握 Context Manager 的進階魔法
·7 分鐘· loading · loading
Python Contextlib Context Manager With 資源管理
Python 裝飾器:讓你的函式穿上超能力外套
·7 分鐘· loading · loading
Python Decorator 裝飾器 進階語法 設計模式
sqlite3:Python 內建輕量資料庫完全攻略
·9 分鐘· loading · loading
Python Sqlite3 SQL 資料庫 Database
pathlib:優雅處理檔案路徑的現代方式
·6 分鐘· loading · loading
Python Pathlib 檔案處理 標準庫
httpx:Python 新世代 HTTP 客戶端完全攻略
·4 分鐘· loading · loading
Python Httpx HTTP Async Requests
Python collections 模組:讓你的資料結構更強大
·5 分鐘· loading · loading
Python Collections Counter Defaultdict Deque Namedtuple