Создание перечисления, состоящего ровно из n элементов, является тривиальной задачей, если я определил его сам:
class Compass(enum.Enum):
NORTH = enum.auto()
EAST = enum.auto()
SOUTH = enum.auto()
WEST = enum.auto()
## or ##
Coin = enum.Enum('Coin', 'HEADS TAILS')
Но что, если это перечисление будет выпущено в открытый доступ, чтобы другие пользователи могли его разделить на подклассы? Давайте предположим, что некоторые из его дополнительных действий зависят от наличия нужного количества членов, поэтому нам нужно обеспечить, чтобы пользователи определяли их правильно.
Вот мое желаемое поведение:
class Threenum(enum.Enum):
"""An enum with exactly 3 members, a 'Holy Enum of Antioch' if you will.
First shalt thou inherit from it. Then shalt though define members three,
no more, no less. Three shall be the number thou shalt define, and the
number of the members shall be three. Four shalt thou not define, neither
define thou two, excepting that thou then proceed to three. Five is right
out. Once member three, being the third member, be defined, then employest
thou thy Threenum of Antioch towards thy problem, which, being intractible
in My sight, shall be solved.
"""
...
class Triumvirate(Threenum): # success
CEASAR = enum.auto()
POMPEY = enum.auto()
CRASSUS = enum.auto()
class TeenageMutantNinjaTurtles(Threenum): # TypeError
LEONARDO = 'blue'
DONATELLO = 'purple'
RAPHAEL = 'red'
MICHELANGELO = 'orange'
Trinity = Threenum('Trinity', 'FATHER SON SPIRIT') # success
Schwartz = Threenum('Schwartz', 'UPSIDE DOWNSIDE') # TypeError
Переопределение _generate_next_value_()
позволяет принудительно использовать максимальное, но не минимальное количество участников.
Это работа для метаклассов. Модуль enum
предоставляет метакласс, который вы можете расширить, добавив желаемое поведение. Самый простой способ — определить n в метаклассе.
class ThreenumMeta(enum.EnumMeta):
"""Meta class for making enums with exactly 3 members."""
n = 3
def __new__(meta, *args, **kwargs):
cls = super().__new__(meta, *args, **kwargs)
if len(cls) not in [0, meta.n]:
raise TypeError(f'{cls} must have exactly {meta.n} members.')
return cls
class Threenum(enum.Enum, metaclass=ThreenumMeta):
"""An enum with exactly 3 members."""
...
Для этого вам понадобится новый метакласс для каждого значения n. Если вам нужно сделать это с несколькими значениями n или если n неизвестно и должно быть динамическим, тогда вам нужно что-то более гибкое, один метакласс, который позволит вам указать значение n.
class EnumMeta_NMany(enum.EnumMeta):
"""Meta class for making enums with exactly n many members, specified by kwarg."""
def __new__(meta, *args, n=None, **kwargs):
cls = super().__new__(meta, *args, **kwargs)
cls.n = getattr(cls, 'n', None) or n
if cls.n is None:
raise TypeError(f'{cls} must specify required number of members with `n` kwarg.')
if len(cls) not in [0, cls.n]:
raise TypeError(f'{cls} must have exactly {cls.n} members.')
return cls
class TwoEnum(enum.Enum, metaclass=EnumMeta_NMany, n=2):
"""An enum with exactly 2 members."""
...
class Threenum(enum.Enum, metaclass=EnumMeta_NMany, n=3):
"""An enum with exactly 3 members."""
...
@EthanFurman Спасибо. Я думаю, что это первый раз, когда у меня возник вопрос, на который еще не было ответа по SO. А поскольку решение оказалось значительно сложнее, чем я предполагал, и на его разработку и полное понимание у меня ушло несколько часов, я подумал, что им стоит поделиться здесь.
@EthanFurman Черт возьми, ты действительно разработал модуль перечисления ?! Очень круто. ( ̄^ ̄ )ゞ Думаю, вы бы знали, если бы существовал способ получше. Поскольку вы не предоставили ответ, могу ли я предположить, что его не существует?
__init_subclass__
было бы проще, но его можно использовать только на Python 3.11+. Ваш ответ метакласса является наиболее широко используемым.
Более простой подход — проверить количество Enum
членов подкласса в методе __init_subclass__:
class Threenum(enum.Enum):
def __init_subclass__(cls):
if len(cls) != 3:
raise TypeError('Subclass of Threenum must have exactly 3 members.')
Демо здесь
Вы также можете создать такой класс с фабричной функцией:
def exact_enum(number):
class _ExactEnum(enum.Enum):
def __init_subclass__(cls):
if len(cls) != number:
raise TypeError(
f'This Enum subclass must have exactly {number} members.')
return _ExactEnum
Использование:
class TeenageMutantNinjaTurtles(exact_enum(3)): # TypeError
LEONARDO = 'blue'
DONATELLO = 'purple'
RAPHAEL = 'red'
MICHELANGELO = 'orange'
Демо здесь
Среда, которую вы используете, неисправна. Если вы запустите свой код в настоящем интерпретаторе Python, он завершится неудачей.
Но спасибо за усилия. __init_subclass__
— отличное предложение, и оно было одним из первых, что я попробовал. Но на самом деле это не работает. Причина в том, что он запускается при создании класса, то есть до создания экземпляров членов, поэтому все перечисления будут иметь длину 0 при запуске. Вы можете увидеть это, напечатав len(cls)
перед проверкой длины. И, насколько я могу судить, другого способа узнать, сколько членов определено внутри __init_subclass__
, нет. Вам придется подождать, пока будут созданы экземпляры членов, а затем подсчитать их.
@ibonyun: Ошибка __init_subclass__
была исправлена в Python 3.11.
@EthanFurman Ага, сейчас я использую 3.9. Спасибо за разъяснение.
Хороший вопрос/ответ.