Как расширить Python Enum?

Какова наилучшая практика для расширения типа 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

В настоящее время нет возможности создать базовый класс enum с членами и использовать его в других классах enum (как в примере выше). Есть ли другой способ реализовать наследование для перечислений Python?

Ответы

Ответ 1

Подклассификация перечисления разрешается только в том случае, если перечисление не определяет каких-либо членов.

Разрешение подклассов перечислений, определяющих членов, приведет к нарушению некоторых важных инвариантов типов и экземпляров.

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

Значит нет, это невозможно.

Ответ 2

Вызов класса Enum непосредственно и использование цепочки позволяет расширение (объединение) существующего перечисления.

Я столкнулся с проблемой расширения перечислений при работе над CANopen реализация. Индексы параметров в диапазоне от 0x1000 до 0x2000 являются общими для всех узлов CANopen, например, диапазон от 0x6000 далее зависит от того, является ли node диском, 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

Хотя это и редкость, иногда полезно создать перечисление из многих модулей. Библиотека aenum1 поддерживает это с помощью функции 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 Enum, enum34 backport и Расширенного перечисления (aenum) библиотека.

Ответ 4

Я решил использовать метаклассический подход к этой проблеме.

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

Я думаю, что вы могли бы сделать это следующим образом:

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

Другой способ:

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>