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]。在 2013 年的 PyCon 语言峰会上,这个问题得到了进一步的讨论。很明显,许多开发人员希望看到一个继承自 int
的枚举,这可以让我们用具有友好字符串表示的枚举替换标准库中的许多整型常量,而不会放弃向后兼容性。几个感兴趣的核心开发人员之间的额外讨论导致了将 IntEnum
作为 Enum
的特例的提议。
Enum
和 IntEnum
之间的主要分歧点在于,与整数进行比较在语义上是否有意义。对于大多数枚举的使用,拒绝与整数的比较是一个**特性**;与整数进行比较的枚举通过传递性导致不同类型枚举之间的比较,这在大多数情况下是不希望的。然而,对于某些用途,需要与整数有更大的互操作性。例如,当用枚举替换现有的标准库常量(例如 socket.AF_INET
)时,情况就是这样。
2013 年 4 月底的进一步讨论得出结论,枚举成员应该属于其枚举的类型:type(Color.red) == Color
。Guido 已经对这个问题做出了决定 [5],以及不允许子类化枚举的相关问题 [6],除非它们没有定义枚举成员 [7]。
Guido 于 2013 年 5 月 10 日接受了该 PEP [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
)的 mix-in。
腌制
枚举可以被序列化和反序列化。
>>> 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
从 enum 包中导入。 - Georg Brandl 建议使用省略号 (
...
) 而不是None
来达到相同的效果。
优点:无需手动输入值。使枚举更容易更改和扩展,特别是对于大型枚举。
缺点:在许多简单情况下实际上键入更长。显式与隐式的争论也适用于此。
标准库中的用例
Python 标准库中有许多地方可以使用枚举来替代当前用于表示它们的其它习惯用法。这些用法可以分为两类:面向用户代码的常量和内部常量。
面向用户代码的常量,例如 os.SEEK_*
、socket
模块常量、十进制舍入模式和 HTML 错误代码,可能需要向后兼容性,因为用户代码可能期望整数。如上所述,IntEnum
提供了所需的语义;作为 int
的子类,它不会影响期望整数的用户代码,同时允许枚举值的打印表示形式。
>>> import socket
>>> family = socket.AF_INET
>>> family == 2
True
>>> print(family)
SocketFamily.AF_INET
内部常量不会被用户代码看到,而是由标准库模块内部使用。这些可以使用 Enum
实现。通过非常粗略地浏览标准库发现的一些示例: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
上次修改: 2023-09-09 17:39:29 GMT