繁体   English   中英

如何扩展 Python 枚举?

[英]How to extend Python Enum?

是否可以扩展使用 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的枚举值并添加更多?

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

允许定义成员的枚举的子类化将导致违反类型和实例的一些重要不变量。

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

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

虽然不常见,但有时从许多模块创建枚举很有用。 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 )库的作者。

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

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

节点.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)])

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

文档

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

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

例子:

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


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

之后,您可以访问 Cats from Animals:

>>> 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(我没有测试过,但我希望它支持我描述的所有特性)。

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

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

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

这里已经有很多很好的答案,但这里有另一个纯粹使用Enum 的 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

我认为你可以这样做:

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可以在运行时改变任何东西
  • 类也是对象

另一种方式 :

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>

您不能扩展枚举,但可以通过合并它们来创建一个新枚举。
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,
)

是的,您可以修改Enum 下面的示例代码有点 hacky,它显然依赖于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的子类的实例)是如何创建的。

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

我想从 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')]

扩展Enum的装饰器

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

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. 扩展Enum类的装饰器

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

从概念上讲,在这种意义上扩展枚举是没有意义的。 问题在于这违反了 Liskov 替换原则:子类的实例应该可以在任何可以使用基类 class 的实例的地方使用,但是BookingStatus的实例不能可靠地用于任何需要EventStatus的地方。 毕竟,如果该实例的值为BookingStatus.duplicateBookingStatus.unknown ,那么这将不是EventStatus的有效枚举值。

我们可以创建一个新的 class,它通过使用功能性 API重用EventStatus设置。一个基本示例:

event_status_codes = [s.name for s in EventStatus]
BookingStatus = Enum(
    'BookingStatus', event_status_codes + ['duplicate', 'unknown']
)

这种方法对枚举值重新编号,忽略它们在EventStatus中的内容。 我们还可以传递名称-值对以指定枚举值; 这让我们可以做更多的分析,以便重用旧值并自动编号新值:

def extend_enum(result_name, base, *new_names):
    base_values = [(v.name, v.value) for v in base]
    next_number = max(v.value for v in base) + 1
    new_values = [(name, i) for i, name in enumerate(new_names, next_number)]
    return Enum(result_name, base_values + new_values)

# Now we can do:
BookingStatus = extend_enum('BookingStatus', EventStatus, 'duplicate', 'unknown')

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM