如何扩展Python Enum?

时间:2015-11-12 19:42:08

标签: python python-3.x enums

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

例如:

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

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

11 个答案:

答案 0 :(得分:20)

  

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

     

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

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

所以没有,这不是直接可能的。

答案 1 :(得分:12)

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

  

我遇到了在使用CANopen时扩展枚举的问题   实现。参数索引的范围是0x1000到0x2000   对所有CANopen节点都是通用的,例如,范围从0x6000   向前取决于节点是否是驱动器,io-module等。

nodes.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)])

drives.py:

select CASE
    WHEN Type = 'Reserved' THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
  END AS isReserved
 from tablename
 where CASE
    WHEN Type = 'Reserved' THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
  END=CAST(0 AS bit)

答案 2 :(得分:8)

虽然不常见,但从许多模块创建枚举有时很有用。 aenum 1 库通过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 backportAdvanced Enumeration (aenum)图书馆的作者。

答案 3 :(得分:6)

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

Docs

新的Enum类必须具有一个基本Enum类,最多一个具体 数据类型以及所需的基于对象的混合类。命令 这些基类是:

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'>]

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

另一种方式(有点古怪)

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

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

但是,如果我们需要在新的Enum上进行迭代,则可以正常运行:

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

因此,请选择您的方式:简单继承,使用decorator进行继承仿真(实际上是重新创建),或添加新的依赖项(如aenum)(我尚未对其进行测试,但我希望它支持我描述的所有功能)。

答案 4 :(得分:1)

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

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>

请注意在命名空间冲突的情况下会发生什么:

>>> 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__(...)之前修改命名空间的方法。

此方法使用与源Enum相同的名称/值对来创建新的Enum,但是生成的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
>>>

答案 5 :(得分:0)

我认为您可以通过以下方式做到这一点:

import enum
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.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可以在运行时更改任何内容
  • 类也是对象

答案 6 :(得分:0)

另一种方式:

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>

答案 7 :(得分:0)

对于正确的类型说明,可以使用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

答案 8 :(得分:0)

是的,您可以修改 Enum。下面的示例代码有点笨拙,它显然依赖于 Enum 的内部结构,它没有任何业务可依赖。另一方面,它有效。

class ExtIntEnum(IntEnum):
    @classmethod
    def _add(cls, value, name):
        obj = int.__new__(cls, value)
        obj._value_ = value
        obj._name_ = name  
        obj.__objclass__ = cls

        cls._member_map_[name] = obj
        cls._value2member_map_[value] = obj
        cls._member_names_.append(name)    

class Fubar(ExtIntEnum):
    foo = 1
    bar = 2

Fubar._add(3,"baz")
Fubar._add(4,"quux")

特别注意 obj = int.__new__() 行。 enum 模块跳过了几个环节,为应该枚举的类找到正确的 __new__ 方法。我们在这里忽略这些圈是因为我们已经知道整数(或者更确切地说,int 的子类的实例)是如何创建的。

最好不要在生产代码中使用它。如果必须,您确实应该添加防止重复值或名称的保护措施。

答案 9 :(得分:0)

我想从 Django 的 IntegerChoices 继承,由于“无法扩展枚举”的限制,这是不可能的。我认为它可以通过一个相对简单的元类来完成。

CustomMetaEnum.py

class CustomMetaEnum(type):
    def __new__(self, name, bases, namespace):
        # Create empty dict to hold constants (ex. A = 1)
        fields = {}

        # Copy constants from the namespace to the fields dict.
        fields = {key:value for key, value in namespace.items() if isinstance(value, int)}
    
        # In case we're about to create a subclass, copy all constants from the base classes' _fields.
        for base in bases:
            fields.update(base._fields)

        # Save constants as _fields in the new class' namespace.
        namespace['_fields'] = fields
        return super().__new__(self, name, bases, namespace)

    # The choices property is often used in Django.
    # If other methods such as values(), labels() etc. are needed
    # they can be implemented below (for inspiration [Django IntegerChoice source][1])
    @property
    def choices(self):
        return [(value,key) for key,value in self._fields.items()]

main.py

from CustomMetaEnum import CustomMetaEnum

class States(metaclass=CustomMetaEnum):
    A = 1
    B = 2
    C = 3

print("States: ")
print(States.A)
print(States.B)
print(States.C)
print(States.choices)


print("MoreStates: ")
class MoreStates(States):
    D = 22
    pass

print(MoreStates.A)
print(MoreStates.B)
print(MoreStates.C)
print(MoreStates.D)
print(MoreStates.choices)

python3.8 main.py

States: 
1
2
3
[(1, 'A'), (2, 'B'), (3, 'C')]
MoreStates: 
1
2
3
22
[(22, 'D'), (1, 'A'), (2, 'B'), (3, 'C')]

答案 10 :(得分:0)

你不能扩展枚举,但你可以通过合并它们来创建一个新的。
Tested for Python 3.6

from enum import Enum


class DummyEnum(Enum):
    a = 1


class AnotherDummyEnum(Enum):
    b = 2


def merge_enums(class_name: str, enum1, enum2, result_type=Enum):
    if not (issubclass(enum1, Enum) and issubclass(enum2, Enum)):
        raise TypeError(
            f'{enum1} and {enum2} must be derived from Enum class'
        )

    attrs = {attr.name: attr.value for attr in set(chain(enum1, enum2))}
    return result_type(class_name, attrs, module=__name__)


result_enum = merge_enums(
    class_name='DummyResultEnum',
    enum1=DummyEnum,
    enum2=AnotherDummyEnum,
)