Skip to content

Python 枚举类:给常量发"身份证"

引言:从"临时工牌"到"正式编制"

想象你管理一家公司:

  • 临时工牌(大写变量)JAN = 1FEB = 2——简单,但谁都能改,而且 JAN1 没区别,类型还是 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))    # True

3.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:   # ✅
    pass

5.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 性别枚举

Studentgender 属性改造为枚举类型,避免使用字符串:

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('测试失败!')

八、小结

  1. 问题:大写变量定义常量,类型不明确、可篡改、无上下文;
  2. 方案Enum 枚举类,给常量发"正式编制";
  3. 快速定义Month = Enum('Month', ('Jan', 'Feb', ...)),自动分配 value;
  4. 自定义类:继承 Enum,加 @unique 防重复,精确控制 value;
  5. 访问方式Weekday.MonWeekday['Mon']Weekday(1),支持比较和遍历;
  6. 核心优势类型安全 + 不可变 + 有语义 + 可扩展(加方法/属性);
  7. 适用场景:状态码、选项配置、业务枚举(订单状态、性别、难度级别)。

枚举类让"魔法数字"有了名字和身份——代码从"看不懂的密码"变成"自解释的文档"。