簡體   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