跳到主要内容

使用枚举类

当一个字段只能从有限集合中取值时,枚举类比散落在代码中的字符串和整数常量更清晰。它为每个选项提供稳定的成员名称、统一的类型和可读的表示,还能减少拼写错误与无效值。

Python 的枚举功能位于标准库 enum 模块。最常用的是 Enum;需要和旧 API 的整数或字符串互操作时,再考虑 IntEnum、StrEnum 或标志枚举。

本文示例适用于 Python 3.10 及以上版本;StrEnum 是 Python 3.11 新增的类型。学完后,你应该能够:

  • 定义和使用普通枚举,并区分成员名称、值和身份;
  • 使用 auto()、别名、@unique 和 _missing_();
  • 在需要兼容旧接口时选择 IntEnum 或 StrEnum;
  • 用 Flag/IntFlag 表示可组合的位标志;
  • 正确处理输入转换、序列化、比较和模式匹配。

先看一个完整例子​

下面的订单状态只能是预先定义的几种值。业务代码接收 OrderStatus,而不是在每个地方比较字符串:

from enum import Enum


class OrderStatus(Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
CANCELLED = "cancelled"

@property
def is_finished(self) -> bool:
return self in {self.SHIPPED, self.CANCELLED}


def next_action(status: OrderStatus) -> str:
if status is OrderStatus.PENDING:
return "等待付款"
if status is OrderStatus.PAID:
return "安排发货"
if status is OrderStatus.SHIPPED:
return "无需操作"
return "检查退款"


status = OrderStatus.PAID
print(status) # OrderStatus.PAID
print(status.name) # PAID
print(status.value) # paid
print(next_action(status)) # 安排发货
print(OrderStatus("paid") is OrderStatus.PAID) # True

成员名称 PAID 适合在 Python 代码中使用,成员值 "paid" 适合存入数据库或发送给外部系统。调用 OrderStatus("paid") 会按值查找成员;输入不在枚举中的值会抛出 ValueError。

定义和访问 Enum​

枚举成员是单例​

同一个枚举成员在进程中只有一个实例,可以用 is 比较身份:

from enum import Enum


class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"


print(Color.RED is Color("red")) # True
print(Color.RED == Color.RED) # True
print(Color.RED == "red") # False

普通 Enum 不会自动等于同值字符串或整数。这样可以避免把不同领域的相同字面量误当作同一类型。

通过名称、值和迭代访问​

print(Color.RED.name) # RED
print(Color.RED.value) # red
print(Color["GREEN"]) # Color.GREEN
print(list(Color)) # [Color.RED, Color.GREEN, Color.BLUE]

Color["GREEN"] 按成员名称查找,名称错误会抛出 KeyError;Color("green") 按成员值查找,值错误会抛出 ValueError。迭代枚举时只会得到规范成员,不会重复列出别名。

值应稳定、可序列化​

枚举值可以是字符串、整数或其他对象,但公开 API 通常选择简单、稳定的值:

class Priority(Enum):
LOW = 10
NORMAL = 20
HIGH = 30

不要依赖自动生成的整数值作为持久化格式。成员顺序一旦改变,自动编号可能改变;数据库或接口字段应使用明确的字符串值,或固定写出整数值并通过迁移管理变化。

auto() 与自动值​

auto() 让枚举类自动生成值:

from enum import Enum, auto


class Direction(Enum):
NORTH = auto()
EAST = auto()
SOUTH = auto()
WEST = auto()


print(Direction.NORTH.value) # 1

普通 Enum 默认从 1 开始递增。自动值适合值只在进程内部使用的场景,例如状态机分支或测试;如果值需要跨进程、跨版本保存,应显式定义稳定值。

需要自定义生成规则时,可以覆盖 _generate_next_value_(),但它必须放在第一个成员定义之前:

from enum import Enum, auto


class Event(Enum):
def _generate_next_value_(name, start, count, last_values):
return name.lower()

LOGIN = auto()
LOGOUT = auto()


print(Event.LOGIN.value) # login

自定义自动值会增加阅读和维护成本。除非规则确实重复且稳定,否则直接写出成员值通常更清楚。

别名与 @unique​

枚举允许多个名称共享同一个值,后定义的名称会成为别名:

class HttpStatus(Enum):
OK = 200
SUCCESS = 200


print(HttpStatus.SUCCESS is HttpStatus.OK) # True
print(HttpStatus.__members__) # 包含 OK 和 SUCCESS
print(list(HttpStatus)) # 只包含 OK

如果每个名称都必须拥有不同值,可以使用 @unique 让重复值在类定义时直接报错:

from enum import Enum, unique


@unique
class Result(Enum):
SUCCESS = "success"
FAILURE = "failure"

别名适合表达兼容旧名称或同义名称;领域规则要求严格唯一时,建议使用 @unique,尽早发现复制粘贴错误。

给枚举添加行为​

枚举成员可以拥有方法、property 和类方法:

from enum import Enum


class FileType(Enum):
TEXT = ("txt", "文本")
IMAGE = ("png", "图片")

def __init__(self, suffix: str, label: str):
self.suffix = suffix
self.label = label

@classmethod
def from_suffix(cls, suffix: str) -> "FileType":
normalized = suffix.lower().lstrip(".")
for file_type in cls:
if file_type.suffix == normalized:
return file_type
raise ValueError(f"不支持的文件扩展名:{suffix}")


print(FileType.TEXT.label) # 文本
print(FileType.from_suffix(".PNG")) # FileType.IMAGE

成员值是元组时,__init__() 会收到元组中的各个元素。把显示标签、解析逻辑等与枚举选项紧密相关的行为放在枚举类中,可以避免外部维护多套并行字典。

如果需要自定义显示文本,可以实现 __str__();但要注意,日志和调试工具可能依赖默认的 Enum.Member 表示,修改前应明确接口需求。

从外部输入转换枚举​

按值构造并处理错误​

外部输入通常是字符串,应在边界处完成转换:

def parse_status(raw: str) -> OrderStatus:
try:
return OrderStatus(raw.strip().lower())
except ValueError as error:
raise ValueError(f"未知订单状态:{raw}") from error

内部函数一旦拿到 OrderStatus,就不必重复处理大小写和拼写。不要在业务深处到处写 if raw == "paid",否则枚举的类型约束会被绕过。

_missing_() 提供自定义查找​

如果输入存在多个合法形式,可以覆盖 _missing_(value):

from enum import Enum


class YesNo(Enum):
YES = "yes"
NO = "no"

@classmethod
def _missing_(cls, value):
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in {"y", "是"}:
return cls.YES
if normalized in {"n", "否"}:
return cls.NO
return None


print(YesNo("是") is YesNo.YES) # True

找不到成员时应返回 None,让 Enum 继续抛出标准 ValueError。不要在 _missing_() 中静默把任意未知值映射为默认选项,否则错误输入会被隐藏。

IntEnum:与整数 API 互操作​

IntEnum 的成员是 int 的子类,可以传给只接受整数的旧接口:

from enum import IntEnum


class HttpCode(IntEnum):
OK = 200
NOT_FOUND = 404


print(HttpCode.OK == 200) # True
print(isinstance(HttpCode.OK, int)) # True
print(HttpCode.OK + 1) # 201

这种兼容性也会降低类型隔离:HttpCode.OK == 200 为真,算术结果还会变成普通整数。只要不需要兼容整数 API,普通 Enum 更能防止跨领域混用。

不要把 IntEnum 当作“带名字的所有整数常量”。它仍然应该表达一组有共同语义的有限选项,不应与任意整数自由混合。

StrEnum:与字符串 API 互操作​

Python 3.11 提供了 StrEnum,它继承自 str 和 Enum:

from enum import StrEnum, auto


class Environment(StrEnum):
DEVELOPMENT = auto()
PRODUCTION = auto()


print(Environment.DEVELOPMENT.value) # development
print(Environment.DEVELOPMENT == "development") # True

在 Python 3.10 及更早版本,可以使用 class Environment(str, Enum): 获得类似效果,但 auto() 的值生成规则和显示行为应在目标版本中验证。StrEnum 适合需要直接传给 JSON、HTTP 或命令行字符串 API 的边界;如果希望内部代码严格区分枚举和普通字符串,仍应使用普通 Enum。

Flag 和 IntFlag:组合位标志​

用 Flag 表示多个同时成立的选项​

普通枚举表示“只能选一个”,Flag 表示“可以组合多个”:

from enum import Flag, auto


class Permission(Flag):
READ = auto()
WRITE = auto()
DELETE = auto()


editor = Permission.READ | Permission.WRITE
print(Permission.WRITE in editor) # True
print(Permission.DELETE in editor) # False
print(editor) # Permission.READ|WRITE

| 合并标志,& 求交集,~ 取补集(受定义的标志范围约束)。使用 in 检查某个成员是否包含在组合值中,通常比手动位运算更易读。

IntFlag 与整数位掩码兼容​

旧系统经常使用整数位掩码,IntFlag 可以在保留枚举语义的同时与整数 API 互操作:

from enum import IntFlag


class Mode(IntFlag):
READ = 1
WRITE = 2
EXECUTE = 4


mode = Mode.READ | Mode.WRITE
print(int(mode)) # 3
print(mode & Mode.WRITE) # Mode.WRITE

Flag 更强调类型隔离;除非确实要传入整数系统调用、文件权限或协议字段,否则不必优先选择 IntFlag。

Flag 不是普通枚举的替代品​

订单状态、支付方式等互斥选项不应使用 Flag。如果把互斥状态错误地组合起来,代码还可能得到一个语法上有效、业务上无意义的值。选择枚举类型前,先回答“这些选项能否同时成立”。

序列化和反序列化​

持久化值而不是成员表示​

JSON、数据库和消息队列通常应保存 .value:

import json


payload = {"status": OrderStatus.PAID.value}
encoded = json.dumps(payload, ensure_ascii=False)
decoded = json.loads(encoded)
status = OrderStatus(decoded["status"])
print(status is OrderStatus.PAID) # True

不要把 str(member)(例如 OrderStatus.PAID)当作稳定的数据格式。类名或成员名重构后,这种文本就可能无法恢复。跨服务协议最好记录明确、文档化的字符串值。

Enum 与 dataclass、Pydantic 等模型​

数据模型框架通常能识别枚举并在输入时完成转换。仍应确认:

  • 框架序列化的是成员名还是 .value;
  • 未知值是报错、忽略还是使用默认值;
  • 数据库迁移是否保留既有值;
  • IntEnum 是否导致普通整数被意外接受。

不要只凭框架默认行为决定外部协议,最好为有效值、未知值和迁移场景编写测试。

模式匹配​

枚举可以用于 match,通常按成员写出分支:

def describe(status: OrderStatus) -> str:
match status:
case OrderStatus.PENDING:
return "待付款"
case OrderStatus.PAID:
return "已付款"
case OrderStatus.SHIPPED:
return "已发货"
case OrderStatus.CANCELLED:
return "已取消"
raise ValueError(f"未处理的状态:{status}")

在 Python 3.10 的结构化模式匹配中,写 case OrderStatus.PAID 可以匹配成员;不要写成裸名称 case PAID,那会被解析为捕获模式而不是枚举成员。新增成员后,应更新分支并通过测试发现遗漏。

动态创建枚举​

需要根据运行时数据创建枚举时,可以使用函数式 API:

from enum import Enum


DynamicColor = Enum("DynamicColor", {"RED": "red", "BLUE": "blue"})
print(DynamicColor.RED.value) # red

动态枚举适合插件元数据或代码生成场景。名称和值来自外部输入时,应先校验格式和来源;动态创建的类还要考虑如何导入、序列化和在类型检查器中表达。

常见误区​

直接比较错误的层级​

if status == "paid": # 普通 Enum 中为 False
...
if status is OrderStatus.PAID: # 推荐:比较成员身份
...

业务代码应统一使用枚举成员;只在边界处读取或写入 .value。对普通 Enum 来说,is 和 == 比较同一成员都可以,但 is 更直接地表达枚举单例语义。

把枚举值当作显示文本​

成员值应该服务于存储和协议稳定性,显示文本可能需要本地化、改名或包含上下文。把 PAID 直接展示给用户通常不合适,应为枚举提供单独的 label 或交给翻译层处理。

修改成员或值​

枚举成员定义后不能重新绑定:

# OrderStatus.PAID = "done" # AttributeError

如果业务状态需要运行时变化,那是实例状态或数据库数据,不应修改枚举类本身。新增、弃用成员应通过版本和迁移策略管理。

过度使用枚举​

只有一个简单布尔值时,bool 往往比 Enum(YES, NO) 更直接;自由扩展、来自配置文件的开放集合也不适合硬编码枚举。枚举的价值在于集合有限、语义稳定且需要集中约束。

选择哪一种枚举​

可以按边界需求快速判断:

  1. 选项互斥、希望强类型隔离:使用 Enum。
  2. 必须兼容整数 API:使用 IntEnum,并接受它与整数相等的副作用。
  3. 必须兼容字符串 API:Python 3.11+ 使用 StrEnum,更早版本使用 str, Enum 并验证行为。
  4. 多个权限或开关可以同时启用:使用 Flag;需要整数位掩码兼容时使用 IntFlag。
  5. 值要跨版本持久化:显式定义稳定的字符串或整数值,不依赖 auto()。

小结​

  • Enum 用稳定的成员名称和值表达有限集合,成员是单例;
  • .name 用于代码中的名称,.value 用于存储和外部协议;
  • auto() 适合内部值,持久化场景应显式定义值;@unique 可阻止意外别名;
  • _missing_() 可以集中处理边界输入,但不应把未知值静默映射为默认值;
  • IntEnum 和 StrEnum 提供旧 API 兼容性,也会降低类型隔离;
  • Flag/IntFlag 表示可组合位标志,不应替代互斥状态枚举;
  • 在边界完成枚举转换,序列化 .value,并为新增成员和未知值编写测试。