如何在Python中表示“枚举”?

1141

我主要是一名C#开发人员,但现在正在用Python开展项目。

在Python中如何表示与枚举等效的结构?

43个回答

2983

枚举类型已在Python 3.4中添加,如PEP 435所述。它还被后向移植到 pypi 上的 3.3、3.2、3.1、2.7、2.6、2.5 和 2.4

想要更高级的枚举技巧,请尝试使用 aenum 库(2.7, 3.3+, 与 enum34 相同作者)。代码在 py2 和 py3 之间不完全兼容,例如你需要在 python 2 中使用__order__

  • 使用 enum34,请执行 $ pip install enum34
  • 使用 aenum,请执行 $ pip install aenum

安装enum(没有数字)将安装一个完全不同且不兼容的版本。


from enum import Enum     # for enum34, or the stdlib version
# from aenum import Enum  # for the aenum version
Animal = Enum('Animal', 'ant bee cat dog')

Animal.ant  # returns <Animal.ant: 1>
Animal['ant']  # returns <Animal.ant: 1> (string lookup)
Animal.ant.name  # returns 'ant' (inverse lookup)

或者等价地:

class Animal(Enum):
    ant = 1
    bee = 2
    cat = 3
    dog = 4
在早期的版本中,实现枚举的一种方法是:

def enum(**enums):
    return type('Enum', (), enums)

它的使用方法如下:

>>> Numbers = enum(ONE=1, TWO=2, THREE='three')
>>> Numbers.ONE
1
>>> Numbers.TWO
2
>>> Numbers.THREE
'three'

您还可以轻松支持自动枚举,像这样:

def enum(*sequential, **named):
    enums = dict(zip(sequential, range(len(sequential))), **named)
    return type('Enum', (), enums)

并且像这样使用:

>>> Numbers = enum('ZERO', 'ONE', 'TWO')
>>> Numbers.ZERO
0
>>> Numbers.ONE
1

可以通过以下方式添加将值转换回名称的支持:

def enum(*sequential, **named):
    enums = dict(zip(sequential, range(len(sequential))), **named)
    reverse = dict((value, key) for key, value in enums.iteritems())
    enums['reverse_mapping'] = reverse
    return type('Enum', (), enums)

这会覆盖任何同名的内容,但在输出时渲染枚举变量非常有用。如果反向映射不存在,则会抛出KeyError异常。以第一个示例为例:

>>> Numbers.reverse_mapping['three']
'THREE'

如果你正在使用MyPy,另一种表示“枚举”的方式是使用typing.Literal

例如:

from typing import Literal #python >=3.8
from typing_extensions import Literal #python 2.7, 3.4-3.7


Animal = Literal['ant', 'bee', 'cat', 'dog']

def hello_animal(animal: Animal):
    print(f"hello {animal}")

hello_animal('rock') # error
hello_animal('bee') # passes


2
我一直不明白为什么在方法enum(*sequential, **named)中要传递kwargs(**named)参数?请解释一下。即使没有kwargs,它也可以正常工作。我已经验证过了。 - Seenu S
1
将Python 2函数更新为与Python 3的Enum(name, values)函数API兼容,这将是非常不错的。 - bscan
很酷,但这样的枚举在Intellisense中无法识别。 - iperov
默认值怎么样? - Sam
3
这里应该提到auto() - fuenfundachtzig
显示剩余2条评论

970
在 PEP 435 之前,Python 没有相应的内容,但是你可以自己实现。
我个人喜欢保持简单(我见过一些非常复杂的例子),像这样...
class Animal:
    DOG = 1
    CAT = 2

x = Animal.DOG

在Python 3.4中(PEP 435),您可以将Enum作为基类。这会为您提供一些额外的功能,PEP中有描述。例如,枚举成员与整数不同,并且由namevalue组成。
from enum import Enum

class Animal(Enum):
    DOG = 1
    CAT = 2

print(Animal.DOG)
# <Animal.DOG: 1>

print(Animal.DOG.value)
# 1

print(Animal.DOG.name)
# "DOG"

如果您不想手动输入数值,请使用以下快捷方式:

class Animal(Enum):
    DOG, CAT = range(2)

< p > Enum 实现 可以转换为列表并且可迭代。成员的顺序是声明顺序,并与其值无关。例如:

class Animal(Enum):
    DOG = 1
    CAT = 2
    COW = 0

list(Animal)
# [<Animal.DOG: 1>, <Animal.CAT: 2>, <Animal.COW: 0>]

[animal.value for animal in Animal]
# [1, 2, 0]

Animal.CAT in Animal
# True

256
Python默认是动态语言。对于像Python这样的语言来说,强制在编译时保证安全性是没有合理理由的,特别是当根本不存在安全性问题时更是如此。还有一点……一个好的设计模式只有在创建它的上下文中才是好的。根据你正在使用的工具,一个好的模式也可能被取代或完全无用。 - Alexandru Nedelcu
22
如果你有100个值,那么你肯定做错了什么;)我喜欢与我的枚举相关的数字...它们易于编写(与字符串相比),可以很容易地在数据库中持久化,并且与C/C++枚举兼容,这使得编组更加容易。 - Alexandru Nedelcu
51
我会使用这个,将数字替换为object() - Tobu
8
X = object() 不方便,因为它不知道自己是什么(你只能与 namespace.X 进行比较),而且风险很高,因为 copy.deepcopy() 或序列化/反序列化会创建一个新的对象,它与你定义的任何对象都不相等!数字至少是安全的,但通常最好使用字符串。 - Beni Cherniavsky-Paskin
9
原先的 PEP354 不再只是被拒绝,而现在标记为已被取代。PEP435 为 Python 3.4 添加了一个标准的枚举类型(Enum)。详情请参见 http://www.python.org/dev/peps/pep-0435/。 - Peter Hansen
显示剩余9条评论

355

这里是一个实现:

class Enum(set):
    def __getattr__(self, name):
        if name in self:
            return name
        raise AttributeError

以下是它的用法:

Animals = Enum(["DOG", "CAT", "HORSE"])

print(Animals.DOG)

56
非常好。可以通过覆盖__setattr__(self, name, value)和可能的__delattr__(self, name)进一步改进,这样如果您意外写入Animals.DOG = CAT,它将不会悄无声息地成功。 - Joonas Pulakka
17
有趣,但相对较慢:每次访问Animals.DOG都需要进行一次测试;而且常量的值是字符串,因此与这些常量进行比较会比如果允许使用整数值更慢。 - Eric O. Lebigot
3
我认为相比于Alexandru或Mark提出的更短的解决方案,这个解决方案不够易读。尽管如此,它仍是一个有趣的解决方案 :) - Eric O. Lebigot
8
在“try-except”块中如何检查集合成员资格? - bgusach
这种技术对于字符串常量非常有效。您可以使用 "My pet is a "+Animals.DOG。Python3的Enum类需要一个__str__方法来删除类名,并且仍然需要一个str()转换...除非我漏掉了什么。 - shao.lo
显示剩余4条评论

222

如果您需要数字值,这里是最快的方式:

dog, cat, rabbit = range(3)

在Python 3.x中,你还可以在末尾添加一个星号作为占位符,以吸收所有剩余的range值,以防你不介意浪费内存并且无法计数:

dog, cat, rabbit, horse, *_ = range(100)

3
但这可能会占用更多的内存! - M.J
我不明白为什么要使用星号作为占位符,因为 Python 会检查要解包的值的数量(所以它会代替你进行计数)。 - Gabriel Devillers
1
@GabrielDevillers,我认为如果元组中的元素数量不匹配,Python将引发异常。 - Mark Harrison
1
确实,在我的测试中(Python2,3)是这样的,但这意味着程序员的任何计数错误都将在第一次测试时被捕获(并给出正确的计数信息)。 - Gabriel Devillers

141

对于你来说最好的解决方案将取决于你对于你的enum的需求,它是一个假的。

简单枚举:

如果你需要enum仅作为标识不同项目名称列表,那么Mark Harrison的解决方案(上面的)非常好:

Pen, Pencil, Eraser = range(0, 3)

使用 range 还可以设置任何 起始值

Pen, Pencil, Eraser = range(9, 12)

除了上面所述的之外,如果您还要求这些项目属于某种容器,那么请将它们嵌入到一个类中:

class Stationery:
    Pen, Pencil, Eraser = range(0, 3)
要使用枚举项,现在需要使用容器名称和项名称:
stype = Stationery.Pen

复杂的枚举:

对于长列表的枚举或更复杂的枚举用法,这些解决方案将不足以满足需求。您可以参考Will Ware在Python Cookbook中发表的关于模拟Python中的枚举的方法。在线版本可在此处获取。

更多信息:

PEP 354: Python中的枚举详细介绍了有关Python中枚举的提议及其被拒绝的原因。


9
使用 range 函数,当第一个参数为 0 时,可以省略它。 - ToonAlfrink
另一个适用于某些目的的虚拟枚举是 my_enum = dict(map(reversed, enumerate(str.split('Item0 Item1 Item2'))))。然后可以在查找中使用 my_enum,例如,my_enum['Item0'] 可以作为序列的索引。您可能希望将 str.split 的结果包装在一个函数中,如果有任何重复项,则抛出异常。 - Ana Nimbus
不错!对于标志位,你可以使用Flag1, Flag2, Flag3 = [2**i for i in range(3)] - majkelx
1
这是最好的答案。 - Yuri Pozniak

84

在Java JDK 5之前使用的类型安全枚举模式具有许多优点。与Alexandru的答案类似,您创建一个类,类级字段是枚举值;但是,枚举值是类的实例,而不是小整数。这具有优势,即您的枚举值不会意外地与小整数相等,可以控制它们的打印方式,如果有用的话可以添加任意方法,并使用isinstance进行断言。

class Animal:
   def __init__(self, name):
       self.name = name

   def __str__(self):
       return self.name

   def __repr__(self):
       return "<Animal: %s>" % self

Animal.DOG = Animal("dog")
Animal.CAT = Animal("cat")

>>> x = Animal.DOG
>>> x
<Animal: dog>
>>> x == 1
False

最近python-dev论坛上指出有一些枚举库在使用中,包括:


17
我认为这是一种非常糟糕的方法。Animal.DOG = Animal("狗") Animal.DOG2 = Animal("狗") assert Animal.DOG == Animal.DOG2失败了... - Confusion
12
@Confusion 用户不应该调用构造函数,其实构造函数的存在只是一个实现细节,你需要向使用你代码的人传达这样一个信息:创建新的枚举值没有意义,退出代码也无法“做正确的事情”。当然,这并不妨碍你实现 Animal.from_name("dog") --> Animal.DOG。 - Aaron Maenpaa
14
你的枚举值不会意外地与小整数相等,这有什么优势?将枚举与整数进行比较有什么问题吗?特别是如果你将枚举存储在数据库中,通常希望将其存储为整数,因此你必须在某个时候将其与整数进行比较。 - ibz
3
@Aaaron Maenpaa。正确的。这仍然是一种破碎和过于复杂的方法来做到这一点。 - aaronasterling
4
这要看你是从C语言的角度来看"Enums只是几个整数的名称",还是更面向对象的角度,其中枚举值是实际对象并具有方法(这就是Java 1.5中的枚举方式,也是类型安全枚举模式所追求的)。个人而言,我不喜欢switch语句,所以更倾向于将枚举值作为实际对象。 - Aaron Maenpaa
显示剩余5条评论

67

枚举类可以只需要一行代码。

class Enum(tuple): __getattr__ = tuple.index
如何使用它(正向和反向查找、键、值、项等)。
>>> State = Enum(['Unclaimed', 'Claimed'])
>>> State.Claimed
1
>>> State[1]
'Claimed'
>>> State
('Unclaimed', 'Claimed')
>>> range(len(State))
[0, 1]
>>> [(k, State[k]) for k in range(len(State))]
[(0, 'Unclaimed'), (1, 'Claimed')]
>>> [(k, getattr(State, k)) for k in State]
[('Unclaimed', 0), ('Claimed', 1)]

1
我认为这是最简单和最优雅的解决方案。在Python 2.4(是的,旧的遗留服务器)中,元组没有索引。我通过替换为列表来解决了这个问题。 - Massimo
1
我在Jupyter笔记本中尝试了这个,发现它不能作为一行定义工作,但将__getattr__定义放在第二个(缩进的)行上会被接受。 - user5920660
这个解决方案让我可以使用 in 关键字来搜索成员,非常方便。示例用法:'Claimed' in Enum(['Unclaimed', 'Claimed']) - Farzad Abdolhosseini
我不喜欢这种字符串初始化方式,因为IntelliSense无法处理这种类型。因此,内置类型和自定义类型更好。 - iperov
这个与enum.Enum相比有什么优势吗?还是这只是一个答案,因为Python 3.6已经被弃用,Python 3.4引入了内置的枚举包? - Martin Thoma

56

所以,我同意。让我们不在Python中强制类型安全,但我想保护自己不犯傻瓜错误。那么,我们对此有什么看法?

class Animal(object):
    values = ['Horse','Dog','Cat']

    class __metaclass__(type):
        def __getattr__(self, name):
            return self.values.index(name)

使用枚举定义时,它可以避免值冲突。

>>> Animal.Cat
2

还有一个方便的优势:非常快速的反向查找:

def name_of(self, i):
    return self.values[i]

我喜欢这样,但你最好用元组锁定值以提高效率?我已经尝试过一下,从__init__中的args设置了self.values的版本。能够声明“Animal = Enum('horse', 'dog', 'cat')”很不错。我还在__getattr__中捕获了ValueError, 以防self.values中缺少某个项目 - 相反,使用提供的名称字符串引发AttributeError似乎更好。基于我有限的知识面,我无法使元类在Python 2.7 中工作,但我的自定义Enum类可以使用普通的实例方法很好地运行。 - trojjer
我完全不知道你如何使用这个类!?你能解释一下它的作用吗? - Camion
这个有什么优势比enum.Enum吗?还是这只是一个应该被删除的答案,不是因为Python 3.6已经被弃用,Python 3.4引入了内置的枚举包? - Martin Thoma

55

Python没有内置等同于enum的功能,其他答案提供了一些自己实现的想法(您可能也对Python cookbook中的过度版本感兴趣)。

然而,在需要在C中调用enum的情况下,我通常会只使用简单字符串:由于对象/属性的实现方式,(C)Python已经针对短字符串进行了优化,所以使用整数并不会真正带来任何性能优势。为了防止打字错误/无效值,您可以在特定位置插入检查。

ANIMALS = ['cat', 'dog', 'python']

def take_for_a_walk(animal):
    assert animal in ANIMALS
    ...

与使用类相比的一个缺点是失去了自动完成的好处。


2
我更喜欢这个解决方案。在可能的情况下,我喜欢使用内置类型。 - Seun Osewa
那个版本并不是过于复杂的,它只是有很多提供的测试代码。 - Casebash
1
实际上,“正确”的版本在注释中,而且更加复杂 - 主要版本有一个小错误。 - Casebash

43

2013年5月10日,Guido同意将PEP 435 加入Python 3.4标准库。这意味着Python终于内置支持枚举类型了!

对于Python3.3、3.2、3.1、2.7、2.6、2.5和2.4版本,有一个向后兼容的版本可用。它在PyPI上的名称为enum34

声明:

>>> from enum import Enum
>>> class Color(Enum):
...     red = 1
...     green = 2
...     blue = 3

表示:

>>> print(Color.red)
Color.red
>>> print(repr(Color.red))
<Color.red: 1>

迭代:

>>> for color in Color:
...   print(color)
...
Color.red
Color.green
Color.blue

编程访问:

>>> Color(1)
Color.red
>>> Color['blue']
Color.blue

了解更多信息,请参考该提案。官方文档很可能很快就会出现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接