source

Python Enum을 확장하는 방법은 무엇입니까?

manycodes 2023. 5. 26. 21:12
반응형

Python Enum을 확장하는 방법은 무엇입니까?

새 클래스를 사용하여 만든 클래스를 확장할 수 있습니까?EnumPython 3.4의 기능? 어떻게?

단순한 하위 분류는 작동하지 않는 것 같습니다.예를 들어

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/howto/enum.html#restricted-enum-subclassing

그래서, 그것은 직접적으로 가능하지 않습니다.

드물지만 여러 모듈에서 열거형을 만드는 것이 유용할 수도 있습니다.라이브러리는 이 기능을 지원합니다.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, backport, Advanced Enumeration aenum() 라이브러리의 작성자입니다.

Enum 클래스를 직접 호출하고 체인을 사용하면 기존 열거형을 확장(결합)할 수 있습니다.

CANopen 구현 작업 중에 열거형을 확장하는 문제를 발견했습니다.0x1000 ~ 0x2000 범위의 파라미터 인덱스는 모든 CAN 개방 노드에 대해 일반적인 반면, 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

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

저는 3.8에서 그런 식으로 테스트했습니다.기존 열거형을 상속할 수도 있지만 기본 클래스(마지막 위치)에서도 상속해야 합니다.

문서:

새 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"

그런 다음 Cats from Animals에 액세스할 수 있습니다.

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

따라서 간단한 상속, 데코레이터를 사용한 상속 에뮬레이션(사실은 레크리에이션) 또는 에넘과 같은 새로운 종속성을 추가하는 방법을 선택합니다(테스트하지는 않았지만, 제가 설명한 모든 기능을 지원할 것으로 예상됨).

올바른 유형 지정을 위해 연산자를 사용할 수 있습니다.

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

여기에는 이미 많은 좋은 답변들이 있지만 여기 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 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는, 그나결과는러결는▁s과▁the.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
>>>

네임스페이스 충돌이 발생할 경우 다음과 같이 처리됩니다.

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

이는베때니다입문스이▁the에 의한 입니다.enum.EnumMeta.__prepare__한 판반환 enum._EnumDict인 전적인것대 dict키 할당 시 다르게 동작하는 개체입니다.이 오류 메시지를 다음과 같이 둘러싸서 표시하지 않을 수 있습니다.try-except TypeError또는 호출하기 전에 네임스페이스를 수정하는 방법이 있을 수 있습니다.super().__prepare__(...).

다른 방법:

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>

저는 당신이 다음과 같은 방법으로 할 수 있다고 생각합니다:

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

요점은 다음과 같습니다.

  • 파이썬은 런타임에 무엇이든 변경할 수 있습니다.
  • 클래스도 객체입니다.

열거형을 확장할 수 없지만 병합하여 새 열거형을 만들 수 있습니다.
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

미하일 불긴의 대답을 확장하기 위해, 장식가는 다음을 확장하기 위해 사용될 수 있습니다.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

할 데코레이터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>]

예, 다음을 수정할 수 있습니다.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)이 생성됩니다.

이것을 생산 코드에 사용하지 않는 것이 좋습니다.필요하다면 중복된 값이나 이름에 대한 가드를 추가해야 합니다.

나는 장고의 유산을 상속받고 싶었습니다.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)

파이썬 3.8main.py:

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

개념적으로 이러한 의미에서 열거형을 확장하는 것은 의미가 없습니다.문제는 이것이 Liskov 대체 원칙을 위반한다는 것입니다: 하위 클래스의 인스턴스는 기본 클래스의 인스턴스가 사용될 수 있는 모든 곳에서 사용할 수 있어야 하지만,BookingStatus 신뢰할 수 있는 곳에서 사용할 수 없었습니다.EventStatus예니다됩이면 만그인입니다.BookingStatus.duplicate또는BookingStatus.unknown은 "" " " " " 에 한 열거 이 아닙니다.EventStatus.

우리는 새로운 클래스를 만들 수 있습니다.EventStatus기능 API를 사용하여 설정합니다.기본적인 예:

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

언급URL : https://stackoverflow.com/questions/33679930/how-to-extend-python-enum

반응형