如何扩展Python枚举?

129

是否可能扩展使用Python 3.4中新Enum功能创建的类?如何实现?

简单的子类化似乎不起作用。例如:

from enum import Enum

class EventStatus(Enum):
   success = 0
   failure = 1

class BookingStatus(EventStatus):
   duplicate = 2
   unknown = 3

会出现像TypeError: Cannot extend enumerations或(在更近的版本中)TypeError: BookingStatus: cannot extend enumeration 'EventStatus'这样的异常。

我该如何让BookingStatus重复使用EventStatus的枚举值并添加更多?


15
枚举的概念是你拥有该类型所有值的完整列表。如果你扩展它并添加更多值,你就会破坏枚举最基本的属性。 - user2357112
@user2357112:谢谢,这是我的问题的答案。 - falek.marcin
11
我不明白这个 - 上面的代码没有给 EventStatus 添加更多值,它试图创建一个新类型,该类型继承了 EventStatus 的值并且还有一些额外的值。在我看来,EventStatus 是干净的。为什么这会破坏基本属性? - Cai
2
@Cai:如果允许这样做,新值将成为“EventStatus”的实例,因为新类将是“EventStatus”的子类。(这也是您无法创建“bool”的子类的原因。) - user2357112
@user2357112supportsMonica 我不相信你所说的是正确的。多态应该允许子类继承父类并添加自己的内容,而不影响父类。例如,Cat和Dog类扩展了Animal类。如果Dog实现了def bark():,这并不意味着所有Animal子类现在都有bark()。同样,EventStatus不应该在BookingStatus中具有新值。 - sleepystar96
2
@sleepystar96:你对枚举和isinstance关系的理解有误。枚举类的核心属性之一是不允许创建更多实例。子类的实例将会是更多实例。一个子类中的方法重写不会在其他子类中显示,这完全无关紧要。 - user2357112
15个回答

78

1
有没有其他内置方法可以做到这一点? - falek.marcin
1
@falek.marcin:可能不是最好的选择。我认为你可以使用另一个更简单的Enum实现。有很多选择 - GingerPlusPlus
1
记录一下,它没有意义的原因是Enum类型使用基于标识的比较,并且枚举的值是不可变的。当确定相对顺序、成员资格测试等时,值的type很重要。当您定义success时,它的type被不可变地设置为EventStatus;如果允许从EventStatus继承,那么success也需要成为BookingStatus(这样做将违反不可变性和/或导致基于type的测试出现问题)。 - ShadowRanger
1
似乎枚举文档中的某个部分链接已更改。由于建议编辑队列已满,我无法自己编辑答案。 - Frank
请注意,“类型的重要不变量”是Liskov替换原则(https://en.wikipedia.org/wiki/Liskov_substitution_principle)。为了理解,请考虑`def func(val: BaseEnum)。现在用func(DerivedEnum.some-value-not-in-base)`调用。在这种情况下应该发生什么,考虑到Python的BaseEnum不允许未定义的值?因此,子类化枚举与正确的面向对象设计不兼容。 - undefined
显示剩余3条评论

59
有时候,虽然不常见,但从多个模块中创建枚举是非常有用的。aenum1库提供了一个extend_enum函数来支持这个功能。
from aenum import Enum, extend_enum

class Index(Enum):
    DeviceType    = 0x1000
    ErrorRegister = 0x1001

for name, value in (
        ('ControlWord', 0x6040),
        ('StatusWord', 0x6041),
        ('OperationMode', 0x6060),
        ):
    extend_enum(Index, name, value)

assert len(Index) == 5
assert list(Index) == [Index.DeviceType, Index.ErrorRegister, Index.ControlWord, Index.StatusWord, Index.OperationMode]
assert Index.DeviceType.value == 0x1000
assert Index.StatusWord.value == 0x6041

1声明:我是Python stdlib Enumenum34 backport高级枚举(aenum库的作者。


“extend_Strenum” 是否在路线图上? - eklektek

31

直接调用Enum类并使用chain可以扩展(连接)现有的枚举。

我在开发CANopen实现时遇到了扩展枚举的问题。参数索引从0x1000到0x2000的范围对所有CANopen节点都是通用的,而从0x6000开始的范围则取决于节点是驱动器、io模块等。

nodes.py:

from enum import IntEnum

class IndexGeneric(IntEnum):
    """ This enum holds the index value of genric object entrys
    """
    DeviceType    = 0x1000
    ErrorRegister = 0x1001

Idx = IndexGeneric

驱动程序.py:

from itertools import chain
from enum import IntEnum
from nodes import IndexGeneric

class IndexDrives(IntEnum):
    """ This enum holds the index value of drive object entrys
    """
    ControlWord   = 0x6040
    StatusWord    = 0x6041
    OperationMode = 0x6060

Idx= IntEnum('Idx', [(i.name, i.value) for i in chain(IndexGeneric,IndexDrives)])

7
我希望使用你最后一段代码中的代码片段,并将其用于MIT许可的typhon库。你愿意将其授权给MIT兼容的许可证吗?我会完整地标明出处。 - gerrit
2
@gerrit 是的,当然可以自由使用它!抱歉回复晚了。 - Jul3k

21

我在3.8上测试了这种方法。我们可以继承现有的枚举,但也需要从基类进行继承(放在最后一个位置)。

文档:

一个新的枚举类必须有一个基本枚举类、最多一个具体数据类型,以及需要的任意数量的基于对象的混合类。它们的顺序是:

class EnumName([mix-in, ...,] [data-type,] base-enum):
    pass

示例:

class Cats(Enum):
    SIBERIAN = "siberian"
    SPHINX = "sphinx"


class Animals(Cats, Enum):
    LABRADOR = "labrador"
    CORGI = "corgi"

之后,您可以从动物中访问猫:

>>> Animals.SIBERIAN
<Cats.SIBERIAN: 'siberian'>

但是如果你想迭代这个枚举类型,只有新成员可访问:

>>> list(Animals)
[<Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]

实际上,这种方法是为了从基类继承方法,但您也可以将其用于具有这些限制的成员。

另一种方法(有点hacky)

如上所述,编写某些函数以将两个枚举合并为一个。我已经写了一个例子:

def extend_enum(inherited_enum):
    def wrapper(added_enum):
        joined = {}
        for item in inherited_enum:
            joined[item.name] = item.value
        for item in added_enum:
            joined[item.name] = item.value
        return Enum(added_enum.__name__, joined)
    return wrapper


class Cats(Enum):
    SIBERIAN = "siberian"
    SPHINX = "sphinx"


@extend_enum(Cats)
class Animals(Enum):
    LABRADOR = "labrador"
    CORGI = "corgi"

但是这里我们遇到了另一个问题。如果我们想要比较成员,它将失败:

>>> Animals.SIBERIAN == Cats.SIBERIAN
False

在这里,我们只需要比较新创建成员的名称和值:

>>> Animals.SIBERIAN.value == Cats.SIBERIAN.value
True

但是,如果我们需要对新枚举进行迭代,它可以正常工作:

>>> list(Animals)
[<Animals.SIBERIAN: 'siberian'>, <Animals.SPHINX: 'sphinx'>, <Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]
所以,选择你的方式:简单继承,使用装饰器进行继承模拟(实际上是重新创建),或添加新的依赖项,例如aenum(我还没有测试过,但我希望它支持我描述的所有功能)。

6
Python 3.8.6出了问题。之前在Python 3.8.5上是可以工作的。 - Hemil
1
@MikhaliBulygin,您错了。在之前的版本中存在一个错误,他们在3.8.6中有意修复了这个问题。请查看我发布的链接。 - Hemil
好答案!确实,强制从基类继承是有意义的。Python太棒了! - ScotchAndSoda
这似乎在3.9.5版本中又出现了问题。 - Jon
2
好的,我猜这是一个不应该起作用的解决方法。这就是“fixed/broke”从8.3.6开始出现的问题:https://bugs.python.org/issue41517 - CGFoX
显示剩余3条评论

13
为了正确地指定类型,您可以使用Union运算符:Union
from enum import Enum
from typing import Union

class EventStatus(Enum):
   success = 0
   failure = 1

class BookingSpecificStatus(Enum):
   duplicate = 2
   unknown = 3

BookingStatus = Union[EventStatus, BookingSpecificStatus]

example_status: BookingStatus
example_status = BookingSpecificStatus.duplicate
example_status = EventStatus.success

值得注意的是:typing 模块需要 Python 3.5+ 版本。 - John Crawford
10
这似乎没有提供任何功能。 - liang
1
类型注释仅仅是元数据,可以被第三方静态检查工具使用。Union 注释并不会创建一个新的类型;它只是记录了一个特定名称仅用于两个仍然无关的类的实例之一。特别地,这不允许我们编写例如 BookingStatus.successBookingStatus.duplicate - Karl Knechtel
这实际上是“解决”初始问题的正确方式。扩展枚举违反了里氏替换原则(https://en.wikipedia.org/wiki/Liskov_substitution_principle),这是面向对象编程的一个基本原则。因此,扩展枚举(类似于类的继承)是糟糕软件设计的一个迹象。在这里,联合确实是正确的语义映射。即使这不是人们可能想到的,出于充分的理由,也不应该像扩展类一样扩展枚举。 - undefined

11
这里已经有很多好的答案了,但我还有另一个纯粹使用Enum's Functional API的解决方案。
可能不是最美观的解决方案,但它避免了代码重复,可以直接使用,无需额外的包/库,并且应该足够满足大多数使用情况。
from enum import Enum

class EventStatus(Enum):
   success = 0
   failure = 1

BookingStatus = Enum(
    "BookingStatus",
    [es.name for es in EventStatus] + ["duplicate", "unknown"],
    start=0,
)

for bs in BookingStatus:
    print(bs.name, bs.value)

# success 0
# failure 1
# duplicate 2
# unknown 3

如果你想明确指定“值”,你可以使用以下方法:
BookingStatus = Enum(
    "BookingStatus",
    [(es.name, es.value) for es in EventStatus] + [("duplicate", 6), ("unknown", 7)],
)

for bs in BookingStatus:
    print(bs.name, bs.value)

# success 0
# failure 1
# duplicate 6
# unknown 7

1
不必使用 start=0list,你可以使用一个字典:{attr.name: attr.value for attr in EventStatus} | {"duplicate": 555, "unknown": -2}| 只适用于 Python > 3.9,对于 Python < 3.9,请使用 dict.update 方法或 dict(**d1, **d2))。 - Давид Шико
好到一定程度。不幸的是,EventStatus.successBookingStatus.success是两个不同的对象,所以不能相等比较。这使得如果将一个EventStatus传递给一个期望BookingStatus的方法,就不合适了。 - Ian Goldby
那么,如果你想向BookingStatus添加方法,你该如何做呢?我想使用这个方法,但是还要添加辅助方法。 - JGFMK
1
@JGFMK - 不算很漂亮,但你可以这样做:BookingStatus.__class__.my_method = my_method,其中my_method由你定义,并以self作为第一个参数 def my_method(self, arg_a, arg_b, ...): ...。然后使用BookingStatus.my_method(arg_a, arg_b) - Paul P

11

我选择使用元类方法解决这个问题。

from enum import EnumMeta

class MetaClsEnumJoin(EnumMeta):
    """
    Metaclass that creates a new `enum.Enum` from multiple existing Enums.

    @code
        from enum import Enum

        ENUMA = Enum('ENUMA', {'a': 1, 'b': 2})
        ENUMB = Enum('ENUMB', {'c': 3, 'd': 4})
        class ENUMJOINED(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMA, ENUMB)):
            pass

        print(ENUMJOINED.a)
        print(ENUMJOINED.b)
        print(ENUMJOINED.c)
        print(ENUMJOINED.d)
    @endcode
    """

    @classmethod
    def __prepare__(metacls, name, bases, enums=None, **kargs):
        """
        Generates the class's namespace.
        @param enums Iterable of `enum.Enum` classes to include in the new class.  Conflicts will
            be resolved by overriding existing values defined by Enums earlier in the iterable with
            values defined by Enums later in the iterable.
        """
        #kargs = {"myArg1": 1, "myArg2": 2}
        if enums is None:
            raise ValueError('Class keyword argument `enums` must be defined to use this metaclass.')
        ret = super().__prepare__(name, bases, **kargs)
        for enm in enums:
            for item in enm:
                ret[item.name] = item.value  #Throws `TypeError` if conflict.
        return ret

    def __new__(metacls, name, bases, namespace, **kargs):
        return super().__new__(metacls, name, bases, namespace)
        #DO NOT send "**kargs" to "type.__new__".  It won't catch them and
        #you'll get a "TypeError: type() takes 1 or 3 arguments" exception.

    def __init__(cls, name, bases, namespace, **kargs):
        super().__init__(name, bases, namespace)
        #DO NOT send "**kargs" to "type.__init__" in Python 3.5 and older.  You'll get a
        #"TypeError: type.__init__() takes no keyword arguments" exception.

这个元类可以像这样使用:
>>> from enum import Enum
>>>
>>> ENUMA = Enum('ENUMA', {'a': 1, 'b': 2})
>>> ENUMB = Enum('ENUMB', {'c': 3, 'd': 4})
>>> class ENUMJOINED(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMA, ENUMB)):
...     e = 5
...     f = 6
...
>>> print(repr(ENUMJOINED.a))
<ENUMJOINED.a: 1>
>>> print(repr(ENUMJOINED.b))
<ENUMJOINED.b: 2>
>>> print(repr(ENUMJOINED.c))
<ENUMJOINED.c: 3>
>>> print(repr(ENUMJOINED.d))
<ENUMJOINED.d: 4>
>>> print(repr(ENUMJOINED.e))
<ENUMJOINED.e: 5>
>>> print(repr(ENUMJOINED.f))
<ENUMJOINED.f: 6>

这种方法创建一个新的Enum,使用与源Enum相同的名称-值对,但生成的Enum成员仍然是唯一的。名称和值将相同,但它们将在直接比较其来源时失败,遵循Python中Enum类设计的精神:
>>> ENUMA.b.name == ENUMJOINED.b.name
True
>>> ENUMA.b.value == ENUMJOINED.b.value
True
>>> ENUMA.b == ENUMJOINED.b
False
>>> ENUMA.b is ENUMJOINED.b
False
>>>

注意命名空间冲突时会发生什么:

>>> ENUMC = Enum('ENUMA', {'a': 1, 'b': 2})
>>> ENUMD = Enum('ENUMB', {'a': 3})
>>> class ENUMJOINEDCONFLICT(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMC, ENUMD)):
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 19, in __prepare__
  File "C:\Users\jcrwfrd\AppData\Local\Programs\Python\Python37\lib\enum.py", line 100, in __setitem__
    raise TypeError('Attempted to reuse key: %r' % key)
TypeError: Attempted to reuse key: 'a'
>>>

这是由于基础的enum.EnumMeta.__prepare__返回了一个特殊的enum._EnumDict对象,而不是典型的dict对象,在键赋值时表现不同。你可以使用try-except TypeError将此错误信息屏蔽,或在调用super().__prepare__(...)之前修改命名空间的方法可能也存在。

2

扩展Enum的装饰器

为了扩展Enum(并通过使用自定义Enum基类支持相等性),可以使用装饰器来扩展Mikhail Bulygin的答案

1. 基于值的相等性Enum基类

from enum import Enum
from typing import Any


class EnumBase(Enum):
    def __eq__(self, other: Any) -> bool:
        if isinstance(other, Enum):
            return self.value == other.value
        return False

2. Decorator to extend Enum class

from typing import Callable

def extend_enum(parent_enum: EnumBase) -> Callable[[EnumBase], EnumBase]:
    """Decorator function that extends an enum class with values from another enum class."""
    def wrapper(extended_enum: EnumBase) -> EnumBase:
        joined = {}
        for item in parent_enum:
            joined[item.name] = item.value
        for item in extended_enum:
            joined[item.name] = item.value
        return EnumBase(extended_enum.__name__, joined)
    return wrapper

例子

>>> from enum import Enum
>>> from typing import Any, Callable
>>> class EnumBase(Enum):
        def __eq__(self, other: Any) -> bool:
            if isinstance(other, Enum):
                return self.value == other.value
            return False
>>> def extend_enum(parent_enum: EnumBase) -> Callable[[EnumBase], EnumBase]:
        def wrapper(extended_enum: EnumBase) -> EnumBase:
            joined = {}
            for item in parent_enum:
                joined[item.name] = item.value
            for item in extended_enum:
                joined[item.name] = item.value
            return EnumBase(extended_enum.__name__, joined)
        return wrapper
>>> class Parent(EnumBase):
        A = 1
        B = 2
>>> @extend_enum(Parent)
    class ExtendedEnum(EnumBase):
        C = 3
>>> Parent.A == ExtendedEnum.A
True
>>> list(ExtendedEnum)
[<ExtendedEnum.A: 1>, <ExtendedEnum.B: 2>, <ExtendedEnum.C: 3>]

2
我认为你可以这样做:

最初的回答。

from typing import List
from enum import Enum

def extend_enum(current_enum, names: List[str], values: List = None):
    if not values:
        values = names

    for item in current_enum:
        names.append(item.name)
        values.append(item.value)

    return Enum(current_enum.__name__, dict(zip(names, values)))

class EventStatus(Enum):
   success = 0
   failure = 1

class BookingStatus(object):
   duplicate = 2
   unknown = 3

BookingStatus = extend_enum(EventStatus, ['duplicate','unknown'],[2,3])

关键点如下:

  • Python可以在运行时更改任何内容
  • 类也是对象
注:Original Answer翻译成“最初的回答”。

2
另一种方式:
Letter = Enum(value="Letter", names={"A": 0, "B": 1})
LetterExtended = Enum(value="Letter", names=dict({"C": 2, "D": 3}, **{i.name: i.value for i in Letter}))

或者:
LetterDict = {"A": 0, "B": 1}
Letter = Enum(value="Letter", names=LetterDict)

LetterExtendedDict = dict({"C": 2, "D": 3}, **LetterDict)
LetterExtended = Enum(value="Letter", names=LetterExtendedDict)

输出:

>>> Letter.A
<Letter.A: 0>
>>> Letter.C
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "D:\jhpx\AppData\Local\Programs\Python\Python36\lib\enum.py", line 324, in __getattr__
    raise AttributeError(name) from None
AttributeError: C
>>> LetterExtended.A
<Letter.A: 0>
>>> LetterExtended.C
<Letter.C: 2>

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