PEP 435 – 向 Python 标准库添加 Enum 类型
- 作者:
- Barry Warsaw <barry at python.org>, Eli Bendersky <eliben at gmail.com>, Ethan Furman <ethan at stoneleaf.us>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2013年2月23日
- Python 版本:
- 3.4
- 发布历史:
- 2013年2月23日, 2013年5月2日
- 取代:
- 354
- 决议:
- Python-Dev 消息
摘要
本 PEP 提议向 Python 标准库添加一个枚举类型。
枚举是一组绑定到唯一常量值的符号名称。在枚举中,值可以通过身份进行比较,枚举本身可以被迭代。
讨论状态
向 Python 添加枚举类型的想法并不新鲜——PEP 354 是 2005 年被拒绝的先前尝试。最近,在 python-ideas 邮件列表上又启动了一组新的讨论[3]。在几个主题中提出了许多新想法;经过长时间讨论后,Guido 提议将 flufl.enum 添加到标准库中[4]。在 PyCon 2013 语言峰会上,这个问题得到了进一步讨论。很明显,许多开发者希望看到一个继承自 int 的枚举,这可以让我们用带有友好字符串表示的枚举替换标准库中的许多整数常量,而不会放弃向后兼容性。在几个感兴趣的核心开发者之间进行的额外讨论促成了将 IntEnum 作为 Enum 的特殊情况的提议。
Enum 和 IntEnum 之间关键的分歧点在于与整数进行比较在语义上是否有意义。对于大多数枚举用法来说,拒绝与整数进行比较是一个**特性**;与整数进行比较的枚举,通过传递性,会导致不相关类型枚举之间的比较,这在大多数情况下是不希望的。然而,对于某些用途,需要与整数有更大的互操作性。例如,用枚举替换现有的标准库常量(如 socket.AF_INET)就是这种情况。
2013年4月下旬的进一步讨论得出的结论是,枚举成员应该属于其枚举的类型:type(Color.red) == Color。Guido 已经对这个问题[5]以及不允许子类化枚举[6](除非它们没有定义枚举成员[7])的相关问题做出了决定。
PEP 于 2013 年 5 月 10 日被 Guido 接受[1]。
动机
[部分基于 PEP 354 中阐述的动机]
枚举的属性对于定义一组不可变、相关联的常量值非常有用,这些常量值可能具有也可能不具有语义含义。经典示例是星期几(周日至周六)和学校评估等级('A'到'D',以及'F')。其他示例包括错误状态值和定义过程中的状态。
可以简单地定义一个其他基本类型(如 int 或 str)的值序列来表示离散的任意值。然而,枚举确保这些值与其他任何值(包括重要的是其他枚举中的值)是不同的,并且对于这些值,没有意义的操作(“星期三乘以二”)是不被定义的。它还提供了枚举值的便捷可打印表示,而无需在定义它们时进行繁琐的重复(即不需要 GREEN = 'green')。
模块和类型名称
我们提议向标准库添加一个名为 enum 的模块。此模块公开的主要类型是 Enum。因此,要导入 Enum 类型,用户代码将运行
>>> from enum import Enum
新枚举类型的建议语义
创建枚举
枚举是使用类语法创建的,这使得它们易于阅读和编写。在函数式 API中描述了另一种创建方法。要定义枚举,请按以下方式子类化 Enum
>>> from enum import Enum
>>> class Color(Enum):
... red = 1
... green = 2
... blue = 3
关于命名法的说明:我们称 Color 为**枚举**(或**enum**),称 Color.red、Color.green 为**枚举成员**(或**enum 成员**)。枚举成员也具有**值**(Color.red 的值为 1,等等)
枚举成员具有人类可读的字符串表示形式
>>> print(Color.red)
Color.red
...而它们的 repr 包含更多信息
>>> print(repr(Color.red))
<Color.red: 1>
枚举成员的**类型**是它所属的枚举
>>> type(Color.red)
<Enum 'Color'>
>>> isinstance(Color.green, Color)
True
>>>
枚举还具有一个仅包含其项目名称的属性
>>> print(Color.red.name)
red
枚举支持迭代,按定义顺序
>>> class Shake(Enum):
... vanilla = 7
... chocolate = 4
... cookies = 9
... mint = 3
...
>>> for shake in Shake:
... print(shake)
...
Shake.vanilla
Shake.chocolate
Shake.cookies
Shake.mint
枚举成员是可哈希的,因此它们可以在字典和集合中使用
>>> apples = {}
>>> apples[Color.red] = 'red delicious'
>>> apples[Color.green] = 'granny smith'
>>> apples
{<Color.red: 1>: 'red delicious', <Color.green: 2>: 'granny smith'}
程序化访问枚举成员
有时,以编程方式访问枚举成员很有用(例如,在程序编写时不知道确切颜色时,Color.red 不适用)。Enum 允许这种访问
>>> Color(1)
<Color.red: 1>
>>> Color(3)
<Color.blue: 3>
如果要按**名称**访问枚举成员,请使用项目访问
>>> Color['red']
<Color.red: 1>
>>> Color['green']
<Color.green: 2>
复制枚举成员和值
拥有两个同名的枚举成员是无效的
>>> class Shape(Enum):
... square = 2
... square = 3
...
Traceback (most recent call last):
...
TypeError: Attempted to reuse key: square
然而,允许两个枚举成员拥有相同的值。给定两个具有相同值(且 A 先定义)的成员 A 和 B,B 是 A 的别名。按值查找 A 和 B 的值将返回 A。按名称查找 B 也将返回 A
>>> 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>
遍历枚举成员不会提供别名
>>> list(Shape)
[<Shape.square: 2>, <Shape.diamond: 1>, <Shape.circle: 3>]
特殊属性 __members__ 是一个有序字典,将名称映射到成员。它包含枚举中定义的所有名称,包括别名
>>> 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__ 属性可用于对枚举成员进行详细的程序化访问。例如,查找所有别名
>>> [name for name, member in Shape.__members__.items() if member.name != name]
['alias_for_square']
比较
枚举成员通过身份进行比较
>>> Color.red is Color.red
True
>>> Color.red is Color.blue
False
>>> Color.red is not Color.blue
True
枚举值之间**不**支持有序比较。枚举不是整数(但请参阅下面的IntEnum)
>>> Color.red < Color.blue
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: Color() < Color()
但定义了相等性比较
>>> Color.blue == Color.red
False
>>> Color.blue != Color.red
True
>>> Color.blue == Color.blue
True
与非枚举值进行比较将始终比较为不相等(再次强调,IntEnum 明确设计为表现不同,见下文)
>>> Color.blue == 2
False
枚举允许的成员和属性
上面的例子使用整数作为枚举值。使用整数简洁方便(并由函数式 API默认提供),但并非严格强制。在绝大多数用例中,人们并不关心枚举的实际值。但如果值**确实**很重要,枚举可以具有任意值。
枚举是 Python 类,可以像往常一样拥有方法和特殊方法。如果我们有这个枚举
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
那么
>>> Mood.favorite_mood()
<Mood.happy: 3>
>>> Mood.happy.describe()
('happy', 3)
>>> str(Mood.funky)
'my custom str! 1'
允许的规则如下:在枚举中定义的所有属性都将成为此枚举的成员,但 **__dunder__** 名称和描述符[9]除外;方法也是描述符。
受限制的枚举子类化
只有当枚举未定义任何成员时,才允许对枚举进行子类化。所以这是禁止的
>>> class MoreColor(Color):
... pink = 17
...
TypeError: Cannot extend enumerations
但这是允许的
>>> class Foo(Enum):
... def some_behavior(self):
... pass
...
>>> class Bar(Foo):
... happy = 1
... sad = 2
...
Guido 在[6]中给出了此决定的理由。允许对定义了成员的枚举进行子类化会导致违反类型和实例的一些重要不变性。另一方面,允许在枚举组之间共享一些共同行为是有意义的,并且子类化空枚举也用于实现IntEnum。
IntEnum
提议 Enum 的一个变体,它也是 int 的子类。IntEnum 的成员可以与整数进行比较;推而广之,不同类型的整数枚举也可以相互比较
>>> from enum import IntEnum
>>> class Shape(IntEnum):
... circle = 1
... square = 2
...
>>> class Request(IntEnum):
... post = 1
... get = 2
...
>>> Shape == 1
False
>>> Shape.circle == 1
True
>>> Shape.circle == Request.post
True
然而,它们仍然不能与 Enum 进行比较
>>> class Shape(IntEnum):
... circle = 1
... square = 2
...
>>> class Color(Enum):
... red = 1
... green = 2
...
>>> Shape.circle == Color.red
False
IntEnum 值以其他你期望的方式表现得像整数
>>> int(Shape.circle)
1
>>> ['a', 'b', 'c'][Shape.circle]
'b'
>>> [i for i in range(Shape.square)]
[0, 1]
对于绝大多数代码,强烈推荐使用 Enum,因为 IntEnum 破坏了枚举的一些语义承诺(因为它可与整数进行比较,从而通过传递性可与其他不相关的枚举进行比较)。它只应在没有其他选择的特殊情况下使用;例如,当整数常量被枚举替换,并且需要与仍期望整数的代码进行向后兼容时。
其他派生枚举
IntEnum 将成为 enum 模块的一部分。然而,独立实现它会非常简单
class IntEnum(int, Enum):
pass
这展示了如何定义类似的派生枚举,例如,一个混入了 str 而不是 int 的 StrEnum。
一些规则
- 当子类化 Enum 时,混合类型必须在基类序列中出现在 Enum 本身之前,如上面的
IntEnum示例所示。 - 虽然 Enum 可以拥有任何类型的成员,但一旦你混合了额外的类型,所有成员都必须拥有该类型的值,例如上面的
int。此限制不适用于仅添加方法而不指定其他数据类型(如int或str)的混合类。
序列化
枚举可以被序列化和反序列化。
>>> from enum.tests.fruit import Fruit
>>> from pickle import dumps, loads
>>> Fruit.tomato is loads(dumps(Fruit.tomato))
True
通常的序列化限制适用:可序列化的枚举必须在模块的顶层定义,因为反序列化需要它们可以从该模块导入。
函数式 API
Enum 类是可调用的,提供以下函数式 API
>>> Animal = Enum('Animal', 'ant bee cat dog')
>>> Animal
<Enum 'Animal'>
>>> Animal.ant
<Animal.ant: 1>
>>> Animal.ant.value
1
>>> list(Animal)
[<Animal.ant: 1>, <Animal.bee: 2>, <Animal.cat: 3>, <Animal.dog: 4>]
此 API 的语义类似于 namedtuple。Enum 调用的第一个参数是枚举的名称。使用函数式 API 创建的枚举的序列化在 CPython 和 PyPy 上会起作用,但对于 IronPython 和 Jython,您可能需要明确指定模块名称,如下所示
>>> Animals = Enum('Animals', 'ant bee cat dog', module=__name__)
第二个参数是枚举成员名称的*源*。它可以是空格分隔的名称字符串、名称序列、包含键/值对的 2 元组序列,或将名称映射到值的映射(例如字典)。最后两个选项允许为枚举分配任意值;其他选项会自动分配从 1 开始递增的整数。将返回一个从 Enum 派生出的新类。换句话说,上面对 Animal 的赋值等同于
>>> class Animals(Enum):
... ant = 1
... bee = 2
... cat = 3
... dog = 4
默认以 1 作为起始数字而不是 0 的原因是 0 在布尔意义上是 False,但枚举成员都评估为 True。
建议的变体
在邮件列表讨论中提出了一些变体。以下是一些比较受欢迎的。
flufl.enum
flufl.enum 是本 PEP 最初基于的参考实现。最终,决定不将 flufl.enum 包含在内,因为其设计将枚举成员与枚举本身分离,因此前者不是后者的实例。其设计还明确允许通过子类化枚举来扩展其成员(由于成员/枚举分离,在这种方案下,flufl.enum 中没有违反类型不变量)。
无需为枚举指定值
Michael Foord 提出(Tim Delaney 提供了概念验证实现)使用元类魔法使其成为可能
class Color(Enum):
red, green, blue
这些值只在第一次查找时才被实际分配。
优点:更简洁的语法,对于非常常见的任务(只需列出枚举名称而无需关心值)所需的输入更少。
缺点:实现中涉及大量魔法,这使得此类枚举的定义在初次看到时令人困惑。此外,显式优于隐式。
使用特殊名称或形式自动分配枚举值
避免指定枚举值的另一种方法是使用特殊名称或形式自动分配它们。例如
class Color(Enum):
red = None # auto-assigned to 0
green = None # auto-assigned to 1
blue = None # auto-assigned to 2
更灵活地
class Color(Enum):
red = 7
green = None # auto-assigned to 8
blue = 19
purple = None # auto-assigned to 20
此主题的一些变体
- 从枚举包中导入一个特殊名称
auto。 - Georg Brandl 提议使用省略号 (
...) 而不是None来达到同样的效果。
优点:无需手动输入值。使得更改和扩展枚举更容易,特别是对于大型枚举。
缺点:在许多简单情况下实际上输入更长。显式与隐式的争论也适用于此。
标准库中的用例
Python 标准库中有很多地方,使用枚举将有利于替换当前用于表示它们的其他习语。此类用法可分为两类:面向用户代码的常量和内部常量。
面向用户代码的常量,例如 os.SEEK_*、socket 模块常量、十进制舍入模式和 HTML 错误代码,可能需要向后兼容性,因为用户代码可能期望整数。IntEnum 如上所述提供了所需的语义;作为 int 的子类,它不影响期望整数的用户代码,同时允许枚举值的可打印表示
>>> import socket
>>> family = socket.AF_INET
>>> family == 2
True
>>> print(family)
SocketFamily.AF_INET
内部常量不被用户代码看到,但由 stdlib 模块内部使用。这些可以使用 Enum 实现。通过非常不完全的 skim stdlib 发现的一些例子: binhex, imaplib, http/client, urllib/robotparser, idlelib, concurrent.futures, turtledemo。
此外,查看 Twisted 库的代码,有许多用例可以将内部状态常量替换为枚举。对于许多网络代码(特别是协议的实现)也可以这样说,并且在用 Tulip 库编写的测试协议中也可以看到。
致谢
本 PEP 最初提议将 Barry Warsaw 的 flufl.enum 包[8]纳入标准库,并在很大程度上受其启发。Ben Finney 是早期枚举 PEP 354 的作者。
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0435.rst
上次修改时间: 2025-02-01 08:59:27 GMT