如何扩展Python Enum?

fal*_*cin 42 python enums python-3.x

什么是Enum在Python 3.4中扩展类型的最佳实践,是否有可能这样做?

例如:

from enum import Enum

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

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

Traceback (most recent call last):
...
TypeError: Cannot extend enumerations
Run Code Online (Sandbox Code Playgroud)

目前没有可能的方法来创建一个带有成员的基本枚举类,并在其他枚举类中使用它(如上例所示).有没有其他方法来实现Python枚举的继承?

Gin*_*lus 24

仅当枚举未定义任何成员时,才允许对枚举进行子类化.

允许对定义成员的枚举进行子类化会导致违反某些类型和实例的重要不变量.

https://docs.python.org/3/library/enum.html#restricted-subclassing-of-enumerations

所以,这不是直接可能的.


Jul*_*l3k 14

直接调用Enum类并使用链允许扩展(连接)现有枚举.

我在处理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
Run Code Online (Sandbox Code Playgroud)

drives.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)])
Run Code Online (Sandbox Code Playgroud)

  • 我想使用从 MIT 许可的 [typhon 库](https://pypi.python.org/pypi/typhon) 中的最终片段派生的代码。您是否愿意在与 MIT 兼容的许可证下重新授权?我会保留完整的归属。 (4认同)
  • @gerrit 是的,当然可以随意使用它!这么晚才回复很抱歉 (2认同)

Eth*_*man 12

虽然不常见,但从许多模块创建枚举有时很有用.在1个库支持这与功能:aenumextend_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
Run Code Online (Sandbox Code Playgroud)

1披露:我是Python stdlibEnum,enum34backportAdvanced Enumeration(aenum) 库的作者.


Pau*_*l P 12

这里已经有很多好的答案,但这里有另一个纯粹使用Enum 的函数式 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
Run Code Online (Sandbox Code Playgroud)

如果您想明确指定的,可以使用:

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
Run Code Online (Sandbox Code Playgroud)


小智 10

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

文档

一个新的 Enum 类必须有一个基本的 Enum 类,最多一种具体的数据类型,以及根据需要尽可能多的基于对象的 mixin 类。这些基类的顺序是:

class EnumName([mix-in, ...,] [data-type,] base-enum):
    pass
Run Code Online (Sandbox Code Playgroud)

例子:

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


class Animals(Cats, Enum):
    LABRADOR = "labrador"
    CORGI = "corgi"
Run Code Online (Sandbox Code Playgroud)

之后,您可以访问来自动物的猫:

>>> Animals.SIBERIAN
<Cats.SIBERIAN: 'siberian'>
Run Code Online (Sandbox Code Playgroud)

但是,如果您想遍历此枚举,则只能访问新成员:

>>> list(Animals)
[<Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]
Run Code Online (Sandbox Code Playgroud)

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

另一种方式(有点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"
Run Code Online (Sandbox Code Playgroud)

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

>>> Animals.SIBERIAN == Cats.SIBERIAN
False
Run Code Online (Sandbox Code Playgroud)

这里我们可能只比较新创建的成员的名称和值:

>>> Animals.SIBERIAN.value == Cats.SIBERIAN.value
True
Run Code Online (Sandbox Code Playgroud)

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

>>> list(Animals)
[<Animals.SIBERIAN: 'siberian'>, <Animals.SPHINX: 'sphinx'>, <Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]
Run Code Online (Sandbox Code Playgroud)

所以选择你的方式:简单继承,用装饰器模拟继承(实际上是娱乐),或者添加一个像 aenum 这样的新依赖项(我没有测试过,但我希望它支持我描述的所有功能)。

  • Python 3.8.6 打破了这一点。正在研究 python 3.8.5 (7认同)
  • 好吧,我想这是一个不应该起作用的解决方法。这是从 8.3.6 开始“修复/破坏”的问题:https://bugs.python.org/issue41517 (2认同)

Joh*_*ord 6

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

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.
Run Code Online (Sandbox Code Playgroud)

这个元类可以这样使用:

>>> 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>
Run Code Online (Sandbox Code Playgroud)

这种方法Enum使用与源Enums相同的名称-值对创建一个新的,但结果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
>>>
Run Code Online (Sandbox Code Playgroud)

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

>>> 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'
>>>
Run Code Online (Sandbox Code Playgroud)

这是因为基enum.EnumMeta.__prepare__返回一个特殊的enum._EnumDict而不是dict在键分配时表现不同的典型对象。您可能希望通过用try-包围它来抑制此错误消息except TypeError,或者可能有一种方法可以在调用之前修改命名空间super().__prepare__(...)


小智 5

对于正确的类型规范,您可以使用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
Run Code Online (Sandbox Code Playgroud)

  • 但这似乎没有提供任何功能 (12认同)