Python 枚举类:给常量发"身份证"
引言:从"临时工牌"到"正式编制"
想象你管理一家公司:
- 临时工牌(大写变量):
JAN = 1,FEB = 2——简单,但谁都能改,而且JAN和1没区别,类型还是int; - 正式编制(枚举类):
Month.Jan——有身份、有编制、不可篡改,还能一眼看出"我是月份,不是普通数字"。
Python 的 Enum 就是给常量发正式编制的工具。
一、问题:大写变量的三大缺陷
python
JAN = 1
FEB = 2
MAR = 3
# ...
def print_month(m):
if m == JAN:
print('January')
print_month(1) # ✅ 能工作
print_month('Jan') # ❌ 不工作,但传字符串也合法
JAN = 99 # ❌ 被篡改了,程序不报错| 缺陷 | 说明 |
|---|---|
| 类型不明确 | JAN 本质是 int,跟月份没关系 |
| 可被修改 | 变量随时能被重新赋值 |
| 无上下文 | 1 可能是月份、星期、状态码,分不清 |
二、枚举类:常量的"正式编制"
2.1 快速定义:一行代码创建枚举
python
from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))发生了什么:
- 创建了一个叫
Month的枚举类型; - 每个成员自动获得一个
value,从1开始递增。
2.2 使用枚举成员
python
# 引用成员
print(Month.Jan) # Month.Jan
print(Month.Jan.value) # 1
# 遍历所有成员
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)
# Jan => Month.Jan , 1
# Feb => Month.Feb , 2
# ...生活化理解:Month 是公司花名册,Month.Jan 是员工"一月"——有名字(name)、有工号(value)、有部门(Month),不是临时工。
2.3 自定义枚举类:精确控制
python
from enum import Enum, unique
@unique # 装饰器:检查没有重复 value
class Weekday(Enum):
Sun = 0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6@unique 的作用:
python
@unique
class Bad(Enum):
A = 1
B = 1 # ❌ ValueError: duplicate values found没有 @unique,重复值静默通过,埋下隐患。
三、枚举成员的访问方式
3.1 四种访问方式
python
day1 = Weekday.Mon
# 方式1:属性访问(最常用)
print(day1) # Weekday.Mon
print(Weekday.Tue) # Weekday.Tue
# 方式2:名称字符串访问
print(Weekday['Tue']) # Weekday.Tue
# 方式3:value 值访问
print(Weekday(1)) # Weekday.Mon
print(Weekday(7)) # ❌ ValueError: 7 is not a valid Weekday
# 方式4:比较
print(day1 == Weekday.Mon) # True
print(day1 == Weekday.Tue) # False
print(day1 == Weekday(1)) # True3.2 为什么 Weekday(1) 有用?
python
# 从数据库读出 value=3,转回枚举
db_value = 3
day = Weekday(db_value)
print(day) # Weekday.Wed
# 如果不用枚举
day = 3
if day == 3: # 3 是什么?星期三?三月?状态码3?
pass生活化理解:Weekday(3) 是"根据工号查员工"——工号 3 对应"星期三",有明确身份;不用枚举就像只知道工号 3,却不知道这人是谁。
四、知识链条:从散乱常量到枚举类
需要定义一组相关常量(月份、星期、状态码)
↓
方案1:大写变量(JAN = 1)
↓
问题:类型不明确、可被篡改、无上下文
↓
方案2:Enum 枚举类
↓
优势:类型安全、不可变、有名称、可遍历、可比较
↓
进阶:@unique 防重复 + 自定义 value + 多种访问方式五、常见误区与避坑指南
5.1 误区一:枚举成员当字符串用
python
class Color(Enum):
RED = 1
GREEN = 2
def paint(c):
if c == 'RED': # ❌ 错误!枚举不是字符串
pass
paint(Color.RED) # 不会进 if修正:比较枚举成员,不是字符串:
python
if c == Color.RED: # ✅
pass5.2 误区二:忘记 @unique 导致重复值
python
class Status(Enum):
OK = 200
SUCCESS = 200 # 没有 @unique,不报错
print(Status.OK == Status.SUCCESS) # True,意外!修正:加上 @unique:
python
@unique
class Status(Enum):
OK = 200
SUCCESS = 200 # ❌ ValueError,立刻发现5.3 误区三:试图修改枚举成员
python
Weekday.Mon = 99 # ❌ AttributeError: Cannot reassign members枚举成员不可变——这是特性,不是 bug。
5.4 误区四:枚举继承普通类
python
class MyEnum(str, Enum): # ❌ 错误!枚举不能继承非枚举类
A = 'a'修正:枚举只能继承 Enum 本身,或混入类型用 str, Enum(Python 3.11+ 有 StrEnum):
python
from enum import StrEnum
class Color(StrEnum):
RED = 'red'
GREEN = 'green'
print(Color.RED) # red
print(Color.RED.upper()) # RED(可以当 str 用)六、实际应用案例
案例 1:订单状态机
python
from enum import Enum, unique
@unique
class OrderStatus(Enum):
PENDING = 0 # 待支付
PAID = 1 # 已支付
SHIPPED = 2 # 已发货
DELIVERED = 3 # 已送达
CANCELLED = 4 # 已取消
class Order(object):
def __init__(self, order_id):
self.order_id = order_id
self.status = OrderStatus.PENDING
def pay(self):
if self.status != OrderStatus.PENDING:
raise ValueError('只有待支付订单才能支付!')
self.status = OrderStatus.PAID
print(f'订单 {self.order_id} 支付成功')
def ship(self):
if self.status != OrderStatus.PAID:
raise ValueError('只有已支付订单才能发货!')
self.status = OrderStatus.SHIPPED
print(f'订单 {self.order_id} 已发货')
def cancel(self):
if self.status in (OrderStatus.SHIPPED, OrderStatus.DELIVERED):
raise ValueError('已发货或已送达的订单不能取消!')
self.status = OrderStatus.CANCELLED
print(f'订单 {self.order_id} 已取消')
# 使用
order = Order('ORD001')
order.pay() # 订单 ORD001 支付成功
order.ship() # 订单 ORD001 已发货
order.cancel() # ❌ ValueError: 已发货或已送达的订单不能取消!生活化理解:OrderStatus 是订单的"人生阶段"——待支付是婴儿,已支付是学生,已发货是上班族,已送达是退休,已取消是"game over"。每个阶段能干的事不同,不能跳级。
案例 2:游戏难度选择
python
from enum import Enum
class Difficulty(Enum):
EASY = 1
NORMAL = 2
HARD = 3
NIGHTMARE = 4
@property
def enemy_hp_multiplier(self):
"""敌人血量倍数"""
return {
Difficulty.EASY: 0.5,
Difficulty.NORMAL: 1.0,
Difficulty.HARD: 1.5,
Difficulty.NIGHTMARE: 3.0,
}[self]
@property
def player_damage_multiplier(self):
"""玩家伤害倍数"""
return {
Difficulty.EASY: 2.0,
Difficulty.NORMAL: 1.0,
Difficulty.HARD: 0.8,
Difficulty.NIGHTMARE: 0.5,
}[self]
class Enemy(object):
def __init__(self, base_hp, difficulty):
self.hp = base_hp * difficulty.enemy_hp_multiplier
class Player(object):
def __init__(self, base_damage, difficulty):
self.damage = base_damage * difficulty.player_damage_multiplier
# 使用
diff = Difficulty.HARD
enemy = Enemy(100, diff)
player = Player(50, diff)
print(f'敌人血量: {enemy.hp}') # 150.0
print(f'玩家伤害: {player.damage}') # 40.0
# 切换难度
diff = Difficulty.EASY
enemy = Enemy(100, diff)
player = Player(50, diff)
print(f'敌人血量: {enemy.hp}') # 50.0
print(f'玩家伤害: {player.damage}') # 100.0生活化理解:Difficulty 是游戏的"难度旋钮"——每个档位对应一套参数配置,换档位就换整套规则,不用改代码里的魔法数字。
七、实战练习
练习:Student 性别枚举
把 Student 的 gender 属性改造为枚举类型,避免使用字符串:
python
from enum import Enum, unique
# 你的代码:定义 Gender 枚举
class Student(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
# 测试
bart = Student('Bart', Gender.Male)
if bart.gender == Gender.Male:
print('测试通过!')
else:
print('测试失败!')参考答案
python
from enum import Enum, unique
@unique
class Gender(Enum):
Male = 0
Female = 1
class Student(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
# 测试
bart = Student('Bart', Gender.Male)
if bart.gender == Gender.Male:
print('测试通过!')
else:
print('测试失败!')八、小结
- 问题:大写变量定义常量,类型不明确、可篡改、无上下文;
- 方案:
Enum枚举类,给常量发"正式编制"; - 快速定义:
Month = Enum('Month', ('Jan', 'Feb', ...)),自动分配 value; - 自定义类:继承
Enum,加@unique防重复,精确控制 value; - 访问方式:
Weekday.Mon、Weekday['Mon']、Weekday(1),支持比较和遍历; - 核心优势:类型安全 + 不可变 + 有语义 + 可扩展(加方法/属性);
- 适用场景:状态码、选项配置、业务枚举(订单状态、性别、难度级别)。
枚举类让"魔法数字"有了名字和身份——代码从"看不懂的密码"变成"自解释的文档"。