
А может всё-таки есть способ сделать такой Enum, используя стандартную библиотеку Python?!
Иногда очень хочется, чтобы у констант были дополнительные параметры, хранящие прочие характеристики. Первое, что приходит на ум - это описать Enum, хранящий простые значения, и маппинг.
from dataclasses import dataclass from enum import Enum class Color(Enum): BLACK = 'black' WHITE = 'white' PURPLE = 'purple' @dataclass(frozen=True) class RGB: red: int green: int blue: int COLOR_TO_RGB = { Color.BLACK: RGB(0, 0, 0), Color.WHITE: RGB(255, 255, 255), Color.PURPLE: RGB(128, 0, 128), }
В таком случае получается, что константы и характеристики располагаются сами по себе, к тому же они могут находится в разных частях системы. Это может привести к тому что при появлении новой константы в Color, никто не обновит маппинг, т.к. нет жёсткой и явной связи.
Давайте разбираться как же можно хранить всё необходимое в единой структуре.
Вариант 1.
from enum import Enum from typing import Union class RelatedEnum(str, Enum): related_value: Union[int, str] def __new__( cls, value: Union[int, str], related_value: Union[int, str] ) -> 'RelatedEnum': obj = str.__new__(cls, value) obj._value_ = value obj.related_value = related_value return obj class SomeEnum(RelatedEnum): CONST1 = ('value1', 'related_value1') CONST2 = ('value2', 'related_value2')
>>> SomeEnum.CONST1.value 'value1' >>> SomeEnum.CONST1.related_value 'related_value1' >>> SomeEnum('value1') <SomeEnum.CONST1: 'value1'> >>> SomeEnum('value1').related_value 'related_value1'
Кажется, что выглядит неплохо, но в таком варианте есть ограничение по кол-ву дополнительных параметров. Давайте попробуем еще немного улучшить.
Вариант 2.
Раз в прошлом варианте у нас получилось сделать с использованием tuple, то значит получится и с typing.NamedTuple. К тому же будут именованные параметры, что повысит читабельность.
В качестве члена перечисления будем хранить целиком объект typing.NamedTuple. Теперь чтобы у нас происходило корректное сравнение объектов нам нужно переопределить методы __hash__ и __eq__. Сравниваться объекты будут по одному полю - value.
from enum import Enum from types import DynamicClassAttribute from typing import Any, NamedTuple, Union class RGB(NamedTuple): red: int green: int blue: int class ColorInfo(NamedTuple): value: Union[int, str] rgb: RGB = None ru: str = None def __hash__(self) -> int: return hash(self.value) def __eq__(self, other: Any) -> bool: if isinstance(other, type(self)): return hash(self) == hash(other) return False class Color(Enum): BLACK = ColorInfo('black', rgb=RGB(0, 0, 0), ru='черный') WHITE = ColorInfo('white', rgb=RGB(255, 255, 255), ru='белый') RED = ColorInfo('red', rgb=RGB(255, 0, 0), ru='красный') GREEN = ColorInfo('green', rgb=RGB(0, 255, 0), ru='зеленый') BLUE = ColorInfo('blue', rgb=RGB(0, 0, 255), ru='голубой') PURPLE = ColorInfo('purple', rgb=RGB(128, 0, 128), ru='пурпурный') OLIVE = ColorInfo('olive', rgb=RGB(128, 128, 0), ru='оливковый') TEAL = ColorInfo('teal', rgb=RGB(0, 128, 128), ru='бирюзовый') _value_: ColorInfo @DynamicClassAttribute def value(self) -> str: return self._value_.value @DynamicClassAttribute def info(self) -> ColorInfo: return self._value_ @classmethod def _missing_(cls, value: Any) -> 'Color': if isinstance(value, (str, int)): return cls._value2member_map_[ColorInfo(value)] raise ValueError(f'Unknown color: {value}')
>>> Color.PURPLE.value 'purple' >>> Color.PURPLE.info ColorInfo(value='purple', rgb=RGB(red=128, green=0, blue=128), ru='пурпурный') >>> Color('purple') <Color.PURPLE: ColorInfo(value='purple', rgb=RGB(red=128, green=0, blue=128), ru='пурпурный')>
Получился в принципе рабочий вариант. Конечно у него есть свои ограничения за счет использования typing.NamedTuple. Плюс ко всему решение не универсальное. После некоторых раздумий появился следующий вариант.
Вариант 3.
Что если вместо typing.NamedTuple использовать dataclass? Вроде идея здравая. Появляется возможность наследования классов, хранящих доп. параметры. Плюс вспомогательные функции из dataclasses.
В качестве члена перечисления, как и в прошлый раз, будем хранить объект целиком, только теперь это dataclass.
import enum from dataclasses import dataclass from types import DynamicClassAttribute from typing import Union, TypeVar, Any from uuid import UUID SimpleValueType = Union[UUID, int, str] ExtendedEnumValueType = TypeVar('ExtendedEnumValueType', bound='BaseExtendedEnumValue') ExtendedEnumType = TypeVar('ExtendedEnumType', bound='ExtendedEnum') @dataclass(frozen=True) class BaseExtendedEnumValue: value: SimpleValueType class ExtendedEnum(enum.Enum): value: SimpleValueType _value_: ExtendedEnumValueType @DynamicClassAttribute def value(self) -> SimpleValueType: return self._value_.value @DynamicClassAttribute def extended_value(self) -> ExtendedEnumValueType: return self._value_ @classmethod def _missing_(cls, value: Any) -> ExtendedEnumType: # noqa: WPS120 if isinstance(value, (UUID, int, str)): simple_value2member = {member.value: member for member in cls.__members__.values()} try: return simple_value2member[value] except KeyError: pass # noqa: WPS420 raise ValueError(f'{value!r} is not a valid {cls.__qualname__}')
Теперь чтобы всё красиво заработало нам понадобится функция EnumField, которая упростит инициализацию (вдохновлено Pydantic).
def EnumField(value: Union[SimpleValueType, ExtendedEnumValueType]) -> BaseExtendedEnumValue: if isinstance(value, (UUID, int, str)): return BaseExtendedEnumValue(value=value) return value
Теперь можно приступать к объявлению и проверке работы.
from dataclasses import field @dataclass(frozen=True) class RGB: red: int green: int blue: int @dataclass(frozen=True) class ColorInfo(BaseExtendedEnumValue): rgb: RGB = field(compare=False) ru: str = field(compare=False) class Color(ExtendedEnum): BLACK = EnumField(ColorInfo('black', rgb=RGB(0, 0, 0), ru='черный')) WHITE = EnumField(ColorInfo('white', rgb=RGB(255, 255, 255), ru='белый')) RED = EnumField(ColorInfo('red', rgb=RGB(255, 0, 0), ru='красный')) GREEN = EnumField(ColorInfo('green', rgb=RGB(0, 255, 0), ru='зеленый')) BLUE = EnumField(ColorInfo('blue', rgb=RGB(0, 0, 255), ru='голубой')) PURPLE = EnumField(ColorInfo('purple', rgb=RGB(128, 0, 128), ru='пурпурный')) OLIVE = EnumField(ColorInfo('olive', rgb=RGB(128, 128, 0), ru='оливковый')) TEAL = EnumField(ColorInfo('teal', rgb=RGB(0, 128, 128), ru='бирюзовый'))
>>> Color.PURPLE <Color.PURPLE: ColorInfo(value='purple', rgb=RGB(red=128, green=0, blue=128), ru='пурпурный')> >>> Color.PURPLE.value 'purple' >>> Color.PURPLE.extended_value ColorInfo(value='purple', rgb=RGB(red=128, green=0, blue=128), ru='пурпурный') >>> Color.PURPLE.extended_value.rgb RGB(red=128, green=0, blue=128) >>> Color.PURPLE.extended_value.ru 'пурпурный' >>> Color('purple') <Color.PURPLE: ColorInfo(value='purple', rgb=RGB(red=128, green=0, blue=128), ru='пурпурный')>
Так родился Python пакет extended-enum. В репозитории я описал основные возможности, а также процесс миграции со стандартного Enum на ExtendedEnum.
Надеюсь, что материал был полезен! Успехов вам в любых начинаниях!
P.S. Мне будет очень приятно, если поставите звёздочку на github
