Python 枚举介绍,以及定义和使用 Python 枚举
前提
阅读本节的前提是对 Python 类的继承以及枚举的概念有所掌握,你可以查看Python 类继承介绍,以及实现 Python 类继承,多重继承,方法重写,编程教程的枚举介绍,以及枚举成员的组合,判断,移除来了解相关信息。
定义 Python 枚举
Python 并没有提供类似于enum
这样的关键字来定义枚举,你需要从enum
模块的Enum
类或其派生类,派生新的类作为 Python 枚举,其基本语法格式如下。
class <enumname>(Enum):
<membername> = <membervalue>
…
- enumname 部分
enumname
为 Python 枚举的名称,需要符合 Python 标识符规范。- membername 部分
membername
为 Python 枚举成员的名称,需要符合 Python 标识符规范,建议所有字母均大写。- membervalue 部分
membervalue
是一个表达式,用于计算 Python 枚举成员的值。
from enum import *
class SPORT(Enum):
'''一个关于运动的枚举,包含了篮球,足球和棒球'''
FOOTBALL = 1
BASKETBALL = 2
BASEBALL = 3
除了通过直接定义类,Python 还支持通过Enum
类或其派生类(Flag
,StrEnum
,IntEnum
等)的构造器来创建新的枚举,其基本格式如下。
Enum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
- value 参数
value
参数为 Python 枚举的名称。- names 参数
names
参数包含了所有枚举成员的信息,该参数可以是一个仅包含成员名称的字符串,名称之间使用空格(,
)分隔,比如'ONE,TWO'
,也可以是一个包含成员名称或名称和值的迭代器对象,比如['ONE','TWO']
,(('ONE',100),('TWO',200))
,还可以是一个包含成员名称和值的映射对象,比如{'ONE':100,'TWO':200}
。- module 参数
module
参数将成为枚举的__module__
特性的值,表示 Python 枚举所在的模块的名称。- qualname 参数
qualname
参数将成为枚举的__qualname__
特性的值,表示 Python 枚举在模块中的完全限定名,比如,World.PLANT
说明枚举PLANT
定义在模块的World
中,World
可能是一个类。- type 参数
type
参数为 Python 枚举的混合类型。- start 参数
start
参数为 Python 枚举成员的起始值,由auto
类使用。- boundary 参数
boundary
参数表示对超出范围的值的处理方式。
下面,我们使用StrEnum
的构造器定义了新的枚举PLANT
,由于是StrEnum
,因此枚举成员的值需要是字符串。
# 定义枚举 PLANT
PLANT = StrEnum('PLANT', (['TREE', 'one tree'], ['FLOWER', 'one flower']))
print(PLANT.__members__)
{'TREE': <PLANT.TREE: 'one tree'>, 'FLOWER': <PLANT.FLOWER: 'one flower'>}
Python 枚举的成员
Python 枚举成员(Member)是以类变量形式出现的特性(Attribute),与类变量一样,枚举成员拥有成员名称和值。比如,示例中枚举SPORT
的成员FOOTBALL
的名称为FOOTBALL
,值为1
。
虽然 Python 枚举成员在语法形式上与类变量类似,但他们是一种语法糖,枚举成员将是只读,不可写入的,每一个枚举成员都是枚举的一个实例,成员的类型是枚举自身,这一点可通过isinstance
和type
来验证。
print(type(SPORT.FOOTBALL))
# 判断成员 FOOTBALL 是否为 SPORT 的实例
print(isinstance(SPORT.FOOTBALL, SPORT))
<enum 'SPORT'>
True
Python 枚举成员的值可以是任意类型
这不同于我们对枚举的一般印象,因为很多语言中的枚举都和数字紧密相关,但 Python 并未对枚举成员值的类型施加限制,如果枚举没有指定混合类型,也没有从某些特定的类继承的话,比如IntEnum
,StrEnum
,IntFlag
。
官方并不推荐使用IntEnum
,IntFlag
等类,对枚举成员的值的类型进行约束,因为这将导致定义的 Python 枚举不符合一般约定。比如,从IntEnum
派生的枚举可以与整数进行有效比较,这不应该发生。
使用 @global_enum 修饰符将 Python 枚举成员定义至模块所在的命名空间
如果为 Python 枚举添加enum
模块中的@global_enum
修饰符,那么该枚举的成员将出现在模块所在的命名空间,你不再需要通过枚举来访问他们。
# 枚举成员 A 和 B 将出现在模块中
@global_enum
class LETTER(Enum):
A = 'a'
B = 'b'
# 下面的访问方式都是正确的
print(LETTER.A)
print(A)
A
A
使用 @verify 修饰符确保 Python 枚举成员值的连贯性
对于整数类型的 Python 枚举成员,如果你希望他们的值具有连贯性,比如从1
到9
,那么可以使用enum
模块的CONTINUOUS
和@verify
修饰符对枚举进行限制,如果枚举成员中的最大值与最小值之间存在未被使用的值,则将引发异常ValueError
。
由于下面的枚举NUM
未定义值为2
的枚举成员,因此将引发异常。
# 枚举 NUM 的成员的值需要是连贯的
@verify(CONTINUOUS)
class NUM(Enum):
ONE = 1
FOUR = 4
THREE = 3
ValueError: invalid enum 'NUM': missing values 2
Python 枚举成员的名称不能与其他枚举成员或特性的名称相同
在一般的 Python 类中,定义名称相同的特性,比如名称相同的类变量和实例方法,并不会产生语法上的错误,但这样的行为对于 Python 枚举成员是行不通的,枚举成员的名称不应该与其他枚举成员或特性的名称相同,这会导致异常TypeError
。
将类变量从 Python 枚举成员中排除
除了私有类变量,名称以单下划线(_
)开头和结束,以及名称存在于_ignore_
中的类变量,Python 枚举中的其他类变量都将被解释为枚举成员。
如果希望某个类变量从枚举成员中排除,那么可以使用enum
模块的nonmember
类为类变量赋值。当然,你也可以使用具有相反效果的member
类为类变量赋值,他将使类变量成为 Python 枚举成员,只不过,这样的操作基本上是多余的。
另外,可以将不希望解释为枚举成员的类变量名称,包含在类变量_ignore_
中,该变量可以是字符串,列表或元组对象。如果是字符串,那么多个变量名称需要使用空格(
)或逗号(,
)分隔。需要指出的是,_ignore_
应该定义在其对应的类变量之前,否则将导致异常ValueError
,使用member
类为_ignore_
对应的类变量赋值,不会达到预期效果。
class POSITION(Enum):
_ignore_ = ('Z', 'W')
# X 不是枚举成员
X = nonmember(1)
# Y 是枚举成员
Y = 2
# Z 和 W 不是枚举成员,虽然 W 使用了 member
Z = 3
W = member(4)
print(POSITION.__members__)
{'Y': <POSITION.Y: 2>}
Python 枚举成员的别名
在一个 Python 枚举中,当多个枚举成员拥有相同的名称时,之后定义的成员的名称,将成为首先定义的成员的别名。当你尝试获取之后定义的 Python 枚举成员时,返回的将是首先定义的枚举成员,这些枚举成员均指向枚举的同一个实例。
下面,我们为枚举SPORT
添加一个值为1
的枚举成员SOCCER
,他的名称SOCCER
将成为枚举成员FOOTBALL
的别名。
class SPORT(Enum):
# …
SOCCER = 1
print(SPORT.SOCCER)
print(SPORT.SOCCER.name)
SPORT.FOOTBALL
FOOTBALL
作为别名的 Python 枚举成员不会出现在对枚举的迭代遍历中
对一个 Python 枚举进行迭代遍历,你将得到该枚举所有未作为别名的枚举成员,这是较为合理的,因为包含别名可能会导致在迭代遍历中执行多余的操作。
# 这里不会出现 SOCCER,因为他是 FOOTBALL 的别名
for i in SPORT:
print(f'{i.name}={i.value}')
FOOTBALL=1
BASKETBALL=2
BASEBALL=3
使用 @unique 或 @verify 修饰符确保 Python 枚举成员值的不重复
如果不希望别名的情况发生,可以为 Python 枚举添加enum
模块的@unique
或@verify
修饰符,他们将确保枚举中的每一个枚举成员都拥有不同的值。其中,@verify
修饰符需要书写为@verify(UNIQUE)
,UNIQUE
位于enum
模块。
# 可以将 @verify(UNIQUE) 替换为 @unique
@verify(UNIQUE)
class ROLE(Enum):
PLAYER = 1
# ERROR 这里不能出现别名
HERO = 1
ValueError: aliases found in <enum 'ROLE'>: HERO -> PLAYER
获取 Python 枚举成员
有多种方式可以获取 Python 枚举中的某个指定成员,最为常见的是通过形式为m.name
的表达式,其中,m
为 Python 枚举类,name
为枚举成员(名称,标识符)。比如,SPORT.BASEBALL
获取了枚举SPORT
的成员BASEBALL
。
除了.
,你还可以通过[]
获取某个 Python 枚举成员,只需给出成员的名称即可,如果无法确定想要获取的枚举成员,那么使用[]
是一个不错的选择。假设,变量name
含有某个成员的名称,SPORT[name]
将返回该名称对应的 Python 枚举成员。
如果只知道某个 Python 枚举成员的值,那么通过该枚举的构造器可获取对应的枚举成员。比如,SPORT(2)
将返回枚举SPORT
的成员BASKETBALL
。
Python 枚举支持迭代遍历操作,你将得到所有未作为别名的枚举成员。如果要获取包括别名在内的全部成员,则可以使用 Python 枚举的__members__
特性,该特性是一个映射类型对象,包含了全部枚举成员的信息,其键值对的键为枚举成员的名称,值为枚举成员的值。
# 显示 SPORT 的全部成员,包括 SOCCER
print(SPORT.__members__)
{'FOOTBALL': <SPORT.FOOTBALL: 1>, 'BASKETBALL': <SPORT.BASKETBALL: 2>, 'BASEBALL': <SPORT.BASEBALL: 3>, 'SOCCER': <SPORT.FOOTBALL: 1>}
获取 Python 枚举成员的名称和值
Python 枚举成员拥有两个只读属性(Property)name
和value
,他们分别表示了 Python 枚举成员的名称和值,比如,SPORT.BASEBALL.value
将返回3
。
获取 Python 枚举中定义的枚举成员的个数
通过len
函数,你可以得到 Python 枚举所定义的枚举成员的个数。
# 获取枚举 NUM 中已经定义的成员的个数
print(f'NUM 成员的个数为:{len(NUM)}')
NUM 成员的个数为:4
比较 Python 枚举成员
在某些编程语言中,枚举可以有效的与整数类型进行比较操作,这带来了方便,但也会使代码的可读性降低。
Python 中的枚举成员与任何非枚举成员总是不相等的,比如,表达式SPORT.FOOTBALL==1
的运算结果为False
,虽然枚举SPORT
的成员FOOTBALL
的值(SPORT.FOOTBALL.value
)是1
。Python 枚举成员,以及枚举成员与非枚举成员之间,不能进行大于(>
),小于(<
),大于等于(>=
),小于等于(<=
)的比较,比如,表达式SPORT.FOOTBALL>=0
将导致异常TypeError
。
正如之前讲到的,Python 枚举成员是枚举的实例,因此,Python 枚举成员之间的比较,也就是实例之间的比较。当他们是同一个实例时,is
运算会返回True
,否则会返回False
,比如,表达式SPORT.FOOTBALL is SPORT.BASKETBALL
的运算结果为False
。
重新定义 Python 枚举会导致前后的同一个枚举成员不相等
如果你重新定义了 Python 枚举,那么之前保留的枚举成员不会与新的枚举成员相等,因为重新定义导致这些枚举成员成为了新的枚举实例。
football = SPORT.FOOTBALL
# 重新定义枚举 SPORT
class SPORT(Enum):
FOOTBALL = 1
BASKETBALL = 2
BASEBALL = 3
SOCCER = 1
# 比较之前和新的枚举成员 FOOTBALL,返回 False
print(football == SPORT.FOOTBALL)
False
继承自 IntEnum,IntFlag 的 Python 枚举的比较问题
由于enum
模块的IntEnum
,IntFlag
类的基类包括int
,因此从IntEnum
,IntFlag
类派生的 Python 枚举,并不遵守以上关于比较的规则,他们可以与某些数字进行有效的比较。假设将枚举SPORT
改为从IntEnum
类派生,那么表达式SPORT.FOOTBALL==1
将返回True
,而不是False
,表达式SPORT.FOOTBALL>=0
将返回True
,而不是引发异常。
Python 枚举成员的值
在默认情况下,Python 枚举成员的值就是书写在=
后面的表达式所返回的内容。如果你希望对这些内容施加新规则,从而产生新的值,那么可以通过定义__new__
方法来完成该任务,=
后的表达式的运算结果会作为该方法的参数。当然,__new__
方法需要返回一个枚举实例,而新的值应被存储在实例的_value_
变量中。
Python 枚举成员的 _value_ 变量
Python 枚举成员的_value_
变量,用于实际存储枚举成员的值,该值也可被枚举成员的只读属性value
返回。
在下面的代码中,我们使用元组为枚举SPEED
的成员LOW
和HIGH
赋值,__new__
方法会计算元组中的数字的和,并将其作为枚举成员的值。
class SPEED(int, Enum):
def __new__(cls, *args):
# 计算所有数字的和,并保存至 total
total = 0
for i in args:
total += i
# 创建枚举的实例,并将之前的计算结果赋值给 _value_
prop = int.__new__(cls)
prop._value_ = total
return prop
LOW = (1, 2, 3, 4, 5)
HIGH = (6, 7, 8, 9)
print(SPEED.LOW.value)
15
使用 auto 类自动设置 Python 枚举成员的值
Python 的enum
模块提供了名为auto
的类,可以实现 Python 枚举成员值的自动设置,只需要简单的在=
之后书写auto()
即可。
在默认情况下,auto()
会返回一个可用的递增的整数,从1
开始计算。如果 Python 枚举从StrEnum
类派生,那么auto()
会返回成员名称的小写字符串。如果 Python 枚举从Flag
类派生,那么auto()
会返回二的整数次幂2ⁿ
,n
从0
开始计算。
如何设置 auto() 为 Python 枚举成员返回的成员值?
当你使用修饰符@staticmethod
在枚举中定义静态方法_generate_next_value_
时,该静态方法的返回值可作为auto()
的返回值。
需要指出,_generate_next_value_
方法需要定义在所有使用auto()
赋值的枚举成员之前,否则将引发异常TypeError
。
_generate_next_value_(name, start, count, last_values)
- name 参数
name
参数为当前正在定义的 Python 枚举成员的名称。- start 参数
start
参数为 Python 枚举成员的预期起始值,默认为1
。- count 参数
count
参数为已经定义的 Python 枚举成员的个数,不包括当前正在定义的成员。- last_values 参数
last_values
是一个 Python 列表,包含了之前所有已定义枚举成员的值。
在下面的枚举DIRECTION
中,成员UP
和RIGHT
的值分别为1
和2
,成员LEFT
的值为101
,因为第三个auto()
会根据DOWN
对应的100
来决定返回值。
class DIRECTION(Enum):
UP = auto()
RIGHT = auto()
DOWN = 100
# 将根据 100 返回 101
LEFT = auto()
print(f'{DIRECTION.UP.value} {DIRECTION.RIGHT.value} {DIRECTION.DOWN.value} {DIRECTION.LEFT.value}')
1 2 100 101
为枚举DIRECTION
加入静态方法_generate_next_value_
,以控制auto()
的返回值,它是一个字符串,包含了枚举成员的名称和一个数字。
class DIRECTION(Enum):
# 需要定义在所有枚举成员之前
@staticmethod
def _generate_next_value_(name, start, count, last_values):
return f'{name}_{count}'
# …
UP_0 RIGHT_1 100 LEFT_3
Python 枚举的方法
Python 枚举可使用修饰符@classmethod
,@staticmethod
来定义类方法或静态方法,通过枚举或枚举成员可以调用他们。对于 Python 枚举中定义的实例方法,其第一参数self
表示枚举成员。
class DAY(str, ReprEnum):
MONDAY = 1
TUESDAY = 2
# self 代表了枚举成员
def show(self):
print(f'{self.name}={self.value}')
DAY.TUESDAY.show()
TUESDAY=2
超出取值范围的 Python 枚举成员
你可以在 Python 枚举中定义名称为_missing_
的类方法,用于处理枚举成员的取值超出范围的情况,比如,为枚举的构造器传递了一个值,该值没有对应任何一个枚举成员。
如果_missing_
方法最终返回了 Python 枚举的某个成员,那么该成员将作为值对应的枚举成员,如果_missing_
方法返回了None
,那么将引发异常,已说明取值是无效的。
_missing_(cls, value)
- cls 参数
cls
参数为 Python 枚举。- value 参数
value
为超出范围的取值。
在枚举ANIMAL
中,我们定义了类方法_missing_
,已处理超出范围的取值,如果取值为小于100
的整数,则将DOG
作为其对应的枚举成员,如果取值为大于100
小于200
的整数,则将CAT
作为其对应的枚举成员,其他情况返回None
。
class ANIMAL(Enum):
DOG = 100
CAT = 200
# 处理超出取值范围的枚举成员
@classmethod
def _missing_(cls, value):
if type(value) is int:
if value < 100:
# 小于 100 的整数,将被认为是 DOG
return ANIMAL.DOG
elif value < 200:
# 大于 100 小于 200 的整数,将被认为是 CAT
return ANIMAL.CAT
# 其余取值返回 None,这将导致异常的发生
return None
print(ANIMAL(99))
print(ANIMAL(111))
ANIMAL.DOG
ANIMAL.CAT
继承 Python 枚举
Python 允许从一个枚举派生另一个枚举,前提是作为基类的枚举尚未拥有任何枚举成员。enum
模块自身包含了从Enum
继承的Flag
,IntFlag
,IntEnum
,StrEnum
等类,他们均可作为其他枚举的基类。
- IntEnum 类
从
IntEnum
类继承的枚举的成员的值必须是整数类型,=
后的表达式的返回值将用于创建整数。IntEnum
类的基类包括Enum
和int
,因此他同时具有两者的特点,可以与其他数字进行算术运算,有效比较,或出现在任何允许使用整数的位置。当参与算术运算时,运算结果将不再是某个枚举成员。- StrEnum 类
从
StrEnum
类继承的枚举的成员的值必须是字符串类型。StrEnum
类的基类包括Enum
和str
,因此他同时具有两者的特点,可以出现在允许使用字符串的位置。- ReprEnum 类
从
ReprEnum
类继承的 Python 枚举必须指定一种类型(比如int
,str
,float
)作为枚举的基类,以确定枚举成员值的类型,当你为成员指定其他类型的值时,将发生隐式转换。- Flag,IntFlag 类
从
Flag
或IntFlag
类继承的枚举也被称为 Python 标志,他将支持枚举成员之间的组合操作。
标志
想要详细了解 Python 标志,你可以查看如何定义和使用支持成员组合的 Python 枚举?Python 标志介绍一节。
枚举JOB
的成员DOCTOR
,使用元组'100',8
来创建整数。枚举HERO
的成员TOM
,会将整数123
转换为字符串'123'
。枚举TIME
的成员MINUTE
将导致异常TypeError
,因为StrEnum
不支持非字符串类型的转换。
class JOB(IntEnum):
WORKER = 1
DOCTOR = '100', 8
# 参与算术运算后,运算结果不再是某个枚举成员
print(f'{type(JOB.WORKER)} {type(JOB.DOCTOR)}')
print(type(JOB.WORKER + 3))
print(type(JOB.WORKER + JOB.DOCTOR))
class HERO(str, ReprEnum):
# 123 会被转换为 '123'
TOM = 123
JERRY = 'mouse?'
class TIME(StrEnum):
SECOND = 'sec'
# ERROR 1 并不会转换为 '1'
MINUTE = 1
<enum 'JOB'> <enum 'JOB'>
<class 'int'>
<class 'int'>
…
TypeError: 1 is not a string