enum
官方文档:https://docs.python.org/zh-cn/3/library/enum.html
一个枚举(enum)是:
- 是绑定到唯一值的符号名称(成员)集合
- 可以被执行迭代以按定义顺序返回其规范的(即非别名的)成员
- 使用 调用 语法按值返回成员
- 使用 索引 语法按名称返回成员
枚举是使用 class
语法或是使用函数调用语法来创建的:
1
2
3
4
5
6
7
8
9
10
| >>> from enum import Enum
>>> # class syntax
>>> class Color(Enum):
... RED = 1
... GREEN = 2
... BLUE = 3
>>> # functional syntax
>>> Color = Enum('Color', ['RED', 'GREEN', 'BLUE'])
|
虽然我们可以使用 class
语法来创建枚举,但枚举并不是常规的 Python 类。
- 类
Color
是一个 枚举 (或 enum ) - 属性
Color.RED
、 Color.GREEN
等是 枚举成员 (或 members )并且在功能上是常量。 - 枚举成员有 names 和 values (
Color.RED
的 name 是 RED
,Color.BLUE
的 value 是 3
,等等)
快速上手
Enum
Enum
是一组与互不相同的值分别绑定的符号名。类似于全局变量,但提供了更好用的 repr()
、分组、类型安全和一些其它特性。
它们最适用于当某个变量可选的值有限时。例如,从一周中选取一天:
1
2
3
4
5
6
7
8
9
| >>> from enum import Enum
>>> class Weekday(Enum):
... MONDAY = 1
... TUESDAY = 2
... WEDNESDAY = 3
... THURSDAY = 4
... FRIDAY = 5
... SATURDAY = 6
... SUNDAY = 7
|
或是 RGB 三原色:
1
2
3
4
5
| >>> from enum import Enum
>>> class Color(Enum):
... RED = 1
... GREEN = 2
... BLUE = 3
|
正如你所见,创建一个 Enum
就是简单地写一个继承 Enum
的类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| >>> Weekday(3)
<Weekday.WEDNESDAY: 3>
>>> print(Weekday.THURSDAY)
Weekday.THURSDAY
>>> type(Weekday.MONDAY)
<enum 'Weekday'>
>>> isinstance(Weekday.FRIDAY, Weekday)
True
>>> print(Weekday.TUESDAY.name)
TUESDAY
>>> Weekday.WEDNESDAY.value
3
|
Flag
如果变量只需要存一天,这个 Weekday
枚举是不错,但如果需要好几天呢?比如要写个函数来描绘一周内的家务,并且不想用 list
,则可以使用不同类型的 Enum
:
1
2
3
4
5
6
7
8
9
| >>> from enum import Flag
>>> class Weekday(Flag):
... MONDAY = 1
... TUESDAY = 2
... WEDNESDAY = 4
... THURSDAY = 8
... FRIDAY = 16
... SATURDAY = 32
... SUNDAY = 64
|
这里做了两处改动:继承了 Flag
,而且值都是2的幂。
就像最开始的 Weekday
枚举一样,可以只用一种类型:
1
2
3
| >>> first_week_day = Weekday.MONDAY
>>> first_week_day
<Weekday.MONDAY: 1>
|
但 Flag
也允许将几个成员并入一个变量:
1
2
3
| >>> weekend = Weekday.SATURDAY | Weekday.SUNDAY
>>> weekend
<Weekday.SATURDAY|SUNDAY: 96>
|
甚至可以在一个 Flag
变量上进行迭代:
1
2
3
4
| >>> for day in weekend:
... print(day)
Weekday.SATURDAY
Weekday.SUNDAY
|
让我们来安排家务吧:
1
2
3
4
5
| >>> chores_for_ethan = {
... 'feed the cat': Weekday.MONDAY | Weekday.WEDNESDAY | Weekday.FRIDAY,
... 'do the dishes': Weekday.TUESDAY | Weekday.THURSDAY,
... 'answer SO questions': Weekday.SATURDAY,
... }
|
一个显示某天家务的函数:
1
2
3
4
5
6
7
| >>> def show_chores(chores, day):
... for chore, days in chores.items():
... if day in days:
... print(chore)
...
>>> show_chores(chores_for_ethan, Weekday.SATURDAY)
answer SO questions
|
✔️ 如果成员值是什么无所谓,可以少些工作,用 auto()
来取值:
1
2
3
4
5
6
7
8
9
10
| >>> from enum import auto
>>> class Weekday(Flag):
... MONDAY = auto()
... TUESDAY = auto()
... WEDNESDAY = auto()
... THURSDAY = auto()
... FRIDAY = auto()
... SATURDAY = auto()
... SUNDAY = auto()
... WEEKEND = SATURDAY | SUNDAY
|
枚举成员及其属性 (key and value) 的访问
有时,要在程序中访问枚举成员(如,开发时不知道颜色的确切值,Color.RED
不适用的情况)。Enum
支持如下访问方式:
1
2
3
4
| >>> Color(1)
<Color.RED: 1>
>>> Color(3)
<Color.BLUE: 3>
|
若要用 名称 访问枚举成员时,可使用枚举项:
1
2
3
4
| >>> Color['RED']
<Color.RED: 1>
>>> Color['GREEN']
<Color.GREEN: 2>
|
若有了枚举成员,需要获取 name
或 value
:
1
2
3
4
5
| >>> member = Color.RED
>>> member.name
'RED'
>>> member.value
1
|
重复的枚举成员和值
两个枚举成员的名称不能相同:
1
2
3
4
5
6
7
| >>> class Shape(Enum):
... SQUARE = 2
... SQUARE = 3
...
Traceback (most recent call last):
...
TypeError: 'SQUARE' already defined as 2
|
然而,一个枚举成员可以关联多个其他名称。如果两个枚举项 A
和 B
具有相同值(并且首先定义的是 A
),则 B
是成员 A
的别名。对 A
按值检索将会返回成员 A
。按名称检索 B
也会返回成员 A
:
1
2
3
4
5
6
7
8
9
10
11
12
| >>> class Shape(Enum):
... SQUARE = 2
... DIAMOND = 1
... CIRCLE = 3
... ALIAS_FOR_SQUARE = 2
...
>>> Shape.SQUARE
<Shape.SQUARE: 2>
>>> Shape.ALIAS_FOR_SQUARE
<Shape.SQUARE: 2>
>>> Shape(2)
<Shape.SQUARE: 2>
|
默认情况下,枚举允许多个名称作为同一个值的别名。若不想如此,可以使用 unique()
装饰器:
1
2
3
4
5
6
7
8
9
10
11
| >>> from enum import Enum, unique
>>> @unique
... class Mistake(Enum):
... ONE = 1
... TWO = 2
... THREE = 3
... FOUR = 3
...
Traceback (most recent call last):
...
ValueError: duplicate values found in <enum 'Mistake'>: FOUR -> THREE
|
使用自动设定的值
如果具体的枚举值无所谓是什么,可以使用 auto
:
1
2
3
4
5
6
7
8
| >>> from enum import Enum, auto
>>> class Color(Enum):
... RED = auto()
... BLUE = auto()
... GREEN = auto()
...
>>> [member.value for member in Color]
[1, 2, 3]
|
枚举值将交由 _generate_next_value_()
选取,该函数可以被重写:
1
2
3
4
5
6
7
8
9
10
11
12
13
| >>> class AutoName(Enum):
... @staticmethod
... def _generate_next_value_(name, start, count, last_values):
... return name
...
>>> class Ordinal(AutoName):
... NORTH = auto()
... SOUTH = auto()
... EAST = auto()
... WEST = auto()
...
>>> [member.value for member in Ordinal]
['NORTH', 'SOUTH', 'EAST', 'WEST']
|
迭代遍历
对枚举成员的迭代遍历不会列出别名:
1
2
3
4
5
| >>> list(Shape)
[<Shape.SQUARE: 2>, <Shape.DIAMOND: 1>, <Shape.CIRCLE: 3>]
>>> list(Weekday)
[<Weekday.MONDAY: 1>, <Weekday.TUESDAY: 2>, <Weekday.WEDNESDAY: 4>, <Weekday.THURSDAY: 8>, <Weekday.FRIDAY: 16>, <Weekday.SATURDAY: 32>, <Weekday.SUNDAY: 64>]
|
👆 Shape.ALIAS_FOR_SQUARE
和 Weekday.WEEKEND
等别名没有被显示。
特殊属性 __members__
是一个名称与成员间的只读有序映射。包含了枚举中定义的所有名称,包括别名:
1
2
3
4
5
6
7
| >>> for name, member in Shape.__members__.items():
... name, member
...
('SQUARE', <Shape.SQUARE: 2>)
('DIAMOND', <Shape.DIAMOND: 1>)
('CIRCLE', <Shape.CIRCLE: 3>)
('ALIAS_FOR_SQUARE', <Shape.SQUARE: 2>)
|
__members__
属性可用于获取枚举成员的详细信息。比如查找所有别名:
1
2
| >>> [name for name, member in Shape.__members__.items() if member.name != name]
['ALIAS_FOR_SQUARE']
|
比较运算
枚举成员是按 ID 进行比较的:
1
2
3
4
5
6
| >>> Color.RED is Color.RED
True
>>> Color.RED is Color.BLUE
False
>>> Color.RED is not Color.BLUE
True
|
枚举值之间无法进行有序的比较。枚举的成员不是整数(另请参阅下文 IntEnum):
1
2
3
4
| >>> Color.RED < Color.BLUE
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'Color' and 'Color'
|
相等性比较的定义如下:
1
2
3
4
5
6
| >>> Color.BLUE == Color.BLUE
True
>>> Color.BLUE == Color.RED
False
>>> Color.BLUE != Color.RED
True
|
与非枚举值的比较将总是不等的(同样 IntEnum
有意设计为其他的做法,参见下文):
1
2
| >>> Color.BLUE == 2
False
|
合法的枚举成员和属性
以上大多数示例都用了整数作为枚举值。使用整数确实简短方便(并且是 Functional API 默认提供的值),但并非强制要求。绝大多数情况下,人们并不关心枚举的实际值是什么。但如果值确实重要,可以使用任何值。
枚举是 Python 的类,可带有普通方法和特殊方法。假设有如下枚举:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| >>> class Mood(Enum):
... FUNKY = 1
... HAPPY = 3
...
... def describe(self):
... # self is the member here
... return self.name, self.value
...
... def __str__(self):
... return 'my custom str! {0}'.format(self.value)
...
... @classmethod
... def favorite_mood(cls):
... # cls here is the enumeration
... return cls.HAPPY
...
|
那么:
1
2
3
4
5
6
| >>> Mood.favorite_mood()
<Mood.HAPPY: 3>
>>> Mood.HAPPY.describe()
('HAPPY', 3)
>>> str(Mood.FUNKY)
'my custom str! 1'
|
合法的规则如下:以单下划线开头和结尾的名称是保留值,不能使用;在枚举中定义的其他所有属性都将成为该枚举的成员,但特殊方法(__str__()
、__add__()
等)、描述符(方法也是描述符)和在 _ignore_
中列出的变量名除外。
注意:如果你的枚举定义了 __new__()
和/或 __init__()
,则给予枚举成员的任何值都将被传递给这些方法。 请参阅示例 Planet。
如果定义了 __new__()
方法,则它会在创建 Enum 成员时被使用;然后它将被 Enum 的 __new__()
所替换,该方法会在类创建后被用于查找现有成员。 详情参见 何时使用 new() 与 init()。
受限的 Enum 子类化
新建的 Enum
类必须包含:一个枚举基类、至多一种数据类型和按需提供的基于 object
的混合类。这些基类的顺序如下:
1
2
| class EnumName([mix-in, ...,] [data-type,] base-enum):
pass
|
仅当未定义任何成员时,枚举类才允许被子类化。因此不得有以下写法:
1
2
3
4
5
6
| >>> class MoreColor(Color):
... PINK = 17
...
Traceback (most recent call last):
...
TypeError: <enum 'MoreColor'> cannot extend <enum 'Color'>
|
但以下代码是可以的:
1
2
3
4
5
6
7
8
| >>> class Foo(Enum):
... def some_behavior(self):
... pass
...
>>> class Bar(Foo):
... HAPPY = 1
... SAD = 2
...
|
如果定义了成员的枚举也能被子类化,则类型与实例的某些重要不可变规则将会被破坏。另一方面,一组枚举类共享某些操作也是合理的。(请参阅例程 OrderedEnum )
数据类支持
当从 dataclass
继承时,__repr__()
将忽略被继承类的名称。 例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
| >>> from dataclasses import dataclass, field
>>> @dataclass
... class CreatureDataMixin:
... size: str
... legs: int
... tail: bool = field(repr=False, default=True)
...
>>> class Creature(CreatureDataMixin, Enum):
... BEETLE = 'small', 6
... DOG = 'medium', 4
...
>>> Creature.DOG
<Creature.DOG: size='medium', legs=4>
|
使用 dataclass()
参数 repr=False
来使用标准的 repr()
。
在 3.12 版本发生变更: 只有数据类字段会被显示在值区域中,而不会显示数据类的名称。Only the dataclass fields are shown in the value area, not the dataclass’ name.
打包(pickle)
枚举类型可以被打包和解包:
1
2
3
4
| >>> from test.test_enum import Fruit
>>> from pickle import dumps, loads
>>> Fruit.TOMATO is loads(dumps(Fruit.TOMATO))
True
|
打包的常规限制同样适用于枚举类型:必须在模块的最高层级定义,因为解包操作要求可从该模块导入。
用 pickle 协议版本 4 可以轻松地将嵌入其他类中的枚举进行打包。
通过在枚举类中定义 __reduce_ex__()
来修改枚举成员的封存/解封方式是可能的。 默认的方法是基于值的,但具有复杂值的枚举也许会想要基于名称的:
1
2
3
| >>> import enum
>>> class MyEnum(enum.Enum):
... __reduce_ex__ = enum.pickle_by_enum_name
|
不建议为旗标使用基于名称的方式,因为未命名的别名将无法解封。
函数式 API
Enum
类可调用并提供了以下函数式 API:
1
2
3
4
5
6
7
| >>> Animal = Enum('Animal', 'ANT BEE CAT DOG')
>>> Animal
<enum 'Animal'>
>>> Animal.ANT
<Animal.ANT: 1>
>>> list(Animal)
[<Animal.ANT: 1>, <Animal.BEE: 2>, <Animal.CAT: 3>, <Animal.DOG: 4>]
|
该 API 的语义类似于 namedtuple
。调用 Enum
的第一个参数是枚举的名称。
第二个参数是枚举成员名称的 来源。可以是个用空格分隔的名称字符串、名称序列、表示键/值对的二元组的序列,或者名称到值的映射(如字典)。 最后两种可以为枚举赋任意值;其他类型则会自动赋成由 1 开始递增的整数值(利用 start
形参可指定为其他起始值)。返回值是一个派生自 Enum
的新类。换句话说,上述对 Animal
的赋值等价于:
1
2
3
4
5
6
| >>> class Animal(Enum):
... ANT = 1
... BEE = 2
... CAT = 3
... DOG = 4
...
|
默认从 1
开始而非 0
,因为 0
是布尔值 False
,但默认的枚举成员都被视作 True
。
对使用函数式 API 创建的枚举进行封存,可能会很棘手,因为要使用栈帧的实现细节来尝试找出枚举是在哪个模块中创建的(例如当你使用了另一个模块中的实用函数时它就可能失败,在 IronPython 或 Jython 上也可能无效)。解决办法是像下面这样显式地指定模块名称:
>»
1
| >>> Animal = Enum('Animal', 'ANT BEE CAT DOG', module=__name__)
|
警告
如果未提供 module
,且 Enum 无法确定是哪个模块,新的 Enum 成员将不可被解封;为了让错误尽量靠近源头,封存将被禁用。
新的 pickle 协议版本 4 在某些情况下同样依赖于 __qualname__
被设为特定位置以便 pickle 能够找到相应的类。 例如,类是否存在于全局作用域的 SomeData 类中:
>»
1
| >>> Animal = Enum('Animal', 'ANT BEE CAT DOG', qualname='SomeData.Animal')
|
完整的签名为:
1
2
3
4
5
6
7
8
9
| Enum(
value='NewEnumName',
names=<...>,
*,
module='...',
qualname='...',
type=<mixed-in class>,
start=1,
)
|
在 3.5 版本发生变更: 增加了 start 形参。