如何在Python中比较枚举类型?

66

自 Python 3.4 起,Enum 类已经存在。

我正在编写一个程序,在其中一些常量具有特定的顺序,我想知道比较它们的最佳 Pythonic 方法是什么:

class Information(Enum):
    ValueOnly = 0
    FirstDerivative = 1
    SecondDerivative = 2

现在有一种方法,需要将给定的Informationinformation与不同的枚举值进行比较:

information = Information.FirstDerivative
print(value)
if information >= Information.FirstDerivative:
    print(jacobian)
if information >= Information.SecondDerivative:
    print(hessian)

直接比较 Enums 是行不通的,因此有三种方法可供选择,我想知道哪一种更好:

方法 1:使用 values:

if information.value >= Information.FirstDerivative.value:
     ...

方法2:使用IntEnum:

class Information(IntEnum):
    ...

方法三:完全不使用枚举:

class Information:
    ValueOnly = 0
    FirstDerivative = 1
    SecondDerivative = 2

每种方法都有效,方法1稍微冗长一些,而方法2使用了不推荐的IntEnum类,而方法3似乎是在枚举添加之前完成此操作的方式。

我倾向于使用方法1,但我不确定。

谢谢任何建议!


请问您能否引用一下“不推荐使用 IntEnum 类”?3.7.1 版本的文档中并没有将其标记为弃用。 - Patrizio Bertoni
2
当然,从文档中可以看出:“对于大多数新代码,强烈建议使用Enum和Flag,因为IntEnum和IntFlag会破坏枚举的某些语义承诺(通过与整数进行比较,因此通过传递性与其他不相关的枚举进行比较)。只有在Enum和Flag无法胜任的情况下才应使用IntEnum和IntFlag;例如,当整数常量被替换为枚举或用于与其他系统的互操作性。” - Sebastian Werk
第一种方法对我有用,谢谢! - LCoelho
6个回答

72

如果您想在Enum中使用丰富的比较运算符,则应始终实现它们。使用 functools.total_ordering 类装饰器,您只需要实现一个 __eq__ 方法以及一个单一的排序,例如 __lt__ 。由于 enum.Enum 已经实现了 __eq__ ,因此这变得更加容易:

>>> import enum
>>> from functools import total_ordering
>>> @total_ordering
... class Grade(enum.Enum):
...   A = 5
...   B = 4
...   C = 3
...   D = 2
...   F = 1
...   def __lt__(self, other):
...     if self.__class__ is other.__class__:
...       return self.value < other.value
...     return NotImplemented
... 
>>> Grade.A >= Grade.B
True
>>> Grade.A >= 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: Grade() >= int()
可怕、可怕的事情可能会发生在IntEnum中。这主要是为了向后兼容而包含的,枚举类型以前是通过子类化int实现的。来自文档
对于绝大多数代码,强烈建议使用Enum,因为IntEnum违反了枚举的一些语义承诺(它可以与整数进行比较,因此可以与其他不相关的枚举类型通过传递性进行比较)。只有在特殊情况下才应该使用它;例如,当整数常量被替换为枚举并且需要与仍然期望整数的代码进行向后兼容时。
下面是一个示例,说明为什么不希望这样做:
>>> class GradeNum(enum.IntEnum):
...   A = 5
...   B = 4
...   C = 3
...   D = 2
...   F = 1
... 
>>> class Suit(enum.IntEnum):
...   spade = 4
...   heart = 3
...   diamond = 2
...   club = 1
... 
>>> GradeNum.A >= GradeNum.B
True
>>> GradeNum.A >= 3
True
>>> GradeNum.B == Suit.spade
True
>>> 

2
非常好的描述,非常感谢。只有一个问题:您使用return NotImplemented而不是raise NotImplemented。是否有一般规则,何时使用return和raise? - Sebastian Werk
4
@SebastianWerk 嗯,你不能使用 raise NotImplemented,因为它不是一个异常。它是一个内置的单例。请参见文档,它用于丰富比较运算符的特殊情况。根据文档NotImplementedError 用于当“抽象方法需要派生类重写该方法时应引发此异常。” - juanpa.arrivillaga
2
@SebastianWerk 还有,请参考这个问题:https://dev59.com/bXNA5IYBdhLWcg3wpvpg - juanpa.arrivillaga
6
非常好的回答,先生。这种方法是一种简洁但不太高效的替代方案,用来代替官方Python文档中详细介绍的OrderedEnum类。虽然人工实现所有比较运算符的OrderedEnum解决方案确实略微更快,但上述给出的@total_ordering解决方案也有其优点。简洁是一种无足轻重的美德。相关的是,有没有人知道为什么OrderedEnum只是被记录在文档中而没有被添加到enum模块中呢? - Cecil Curry
2
是否有类似的Python Enum结构或类似的东西,它实际上有点… _好用_?添加@total_ordering并实现比较运算符不是我们应该自己编写的内容。这太繁琐了。 - WestCoastProjects
显示剩余4条评论

24

我之前没有遇到过枚举,所以我浏览了文档(https://docs.python.org/3/library/enum.html)...并找到了OrderedEnum(第8.13.13.2节)。这不是你想要的吗?从文档中可以看到:

>>> class Grade(OrderedEnum):
...     A = 5
...     B = 4
...     C = 3
...     D = 2
...     F = 1
...
>>> Grade.C < Grade.A
True

由于未修复的Python bug,这种方法不起作用:https://bugs.python.org/issue30545 您应该始终覆盖比较器方法,如__eq__和__lt__。 - asu
1
@asu,没有未解决的错误,在那个线程中的问题并不是Enum本身的问题,而且无论如何,这个例子确实重写了比较运算符。 - juanpa.arrivillaga
7
在Python 3.6中似乎无法导入,出现了“ImportError: cannot import name 'OrderedEnum'”错误?编辑:看起来这是一个“有趣的例子”,实际上不在标准Python库中。您需要从文档中复制片段以使用它。 - Cobertos
2
OrderedEnum 的文档已经移动到这里:https://docs.python.org/3/howto/enum.html#orderedenum - Nermin

2
你可以创建一个简单的装饰器来解决这个问题:
from enum import Enum
from functools import total_ordering

def enum_ordering(cls):
    def __lt__(self, other):
        if type(other) == type(self):
            return self.value < other.value

        raise ValueError("Cannot compare different Enums")

    setattr(cls, '__lt__', __lt__)
    return total_ordering(cls)


@enum_ordering
class Foos(Enum):
    a = 1
    b = 3
    c = 2

assert Names.a < Names.c
assert Names.c < Names.b
assert Names.a != Foos.a
assert Names.a < Foos.c # Will raise a ValueError

如果您愿意,可以实现@VoteCoffee上面答案中的其他方法,以获取额外积分。


1

结合以上一些想法,您可以子类化enum.Enum,使其与字符串/数字可比较,然后基于此类构建枚举:

import numbers
import enum


class EnumComparable(enum.Enum):
    def __gt__(self, other):
        try:
            return self.value > other.value
        except:
            pass
        try:
            if isinstance(other, numbers.Real):
                return self.value > other
        except:
            pass
        return NotImplemented

    def __lt__(self, other):
        try:
            return self.value < other.value
        except:
            pass
        try:
            if isinstance(other, numbers.Real):
                return self.value < other
        except:
            pass
        return NotImplemented

    def __ge__(self, other):
        try:
            return self.value >= other.value
        except:
            pass
        try:
            if isinstance(other, numbers.Real):
                return self.value >= other
            if isinstance(other, str):
                return self.name == other
        except:
            pass
        return NotImplemented

    def __le__(self, other):
        try:
            return self.value <= other.value
        except:
            pass
        try:
            if isinstance(other, numbers.Real):
                return self.value <= other
            if isinstance(other, str):
                return self.name == other
        except:
            pass
        return NotImplemented

    def __eq__(self, other):
        if self.__class__ is other.__class__:
            return self == other
        try:
            return self.value == other.value
        except:
            pass
        try:
            if isinstance(other, numbers.Real):
                return self.value == other
            if isinstance(other, str):
                return self.name == other
        except:
            pass
        return NotImplemented

0
对于那些想要像这样使用 == 与两个枚举实例的人:enum_instance_1 == enum_instance_2

只需将__eq__方法添加到您的Enum类中,如下所示:

def __eq__(self, other):
    return self.__class__ is other.__class__ and other.value == self.value

1
这完全是不必要的...这种==行为对于继承自enum.Enum或使用enum.EnumMeta作为元类的类的实例已经存在。注意,你也可以直接使用is来比较枚举类型! - undefined
这完全是不必要的...对于继承自enum.Enum或使用enum.EnumMeta作为元类的类的实例,已经存在这种==行为。注意,你也可以直接使用is来比较枚举类型! - juanpa.arrivillaga

0
我正在使用Python 3.9。https://pypi.org/project/ordered-enum/
pip install ordered_enum

那么

from ordered_enum import OrderedEnum

class Information(OrderedEnum):
    ValueOnly = 0
    FirstDerivative = 1
    SecondDerivative = 2

print(Information.ValueOnly < Information.FirstDerivative)

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