Spec-Zone.ru › Python 3.14

Руководство по Enum

Enum — это набор символьных имен, связанных с уникальными значениями. Они похожи на глобальные переменные, но предоставляют более полезное представление repr(), группировку, безопасность типов и некоторые другие возможности.

Они особенно полезны, когда переменная может принимать одно из ограниченного набора значений. Например, дни недели:

>>> from enum import Enum
>>> class Weekday(Enum):
...     MONDAY = 1
...     TUESDAY = 2
...     WEDNESDAY = 3
...     THURSDAY = 4
...     FRIDAY = 5
...     SATURDAY = 6
...     SUNDAY = 7

Или, возможно, основные цвета RGB:

>>> from enum import Enum
>>> class Color(Enum):
...     RED = 1
...     GREEN = 2
...     BLUE = 3

Как видите, создать Enum так же просто, как написать класс, наследующий непосредственно от Enum.

Примечание

Регистр имен элементов Enum

Поскольку перечисления используются для представления констант и помогают избежать конфликтов имен между методами/атрибутами классов-миксинов и именами элементов перечисления, мы настоятельно рекомендуем использовать для элементов имена В ВЕРХНЕМ РЕГИСТРЕ и в примерах будем придерживаться этого стиля.

В зависимости от природы перечисления значение элемента может иметь или не иметь значение, но в любом случае по этому значению можно получить соответствующий элемент:

>>> Weekday(3)
<Weekday.WEDNESDAY: 3>

Как видите, repr() элемента показывает имя перечисления, имя элемента и значение. str() элемента показывает только имя перечисления и имя элемента:

>>> print(Weekday.THURSDAY)
Weekday.THURSDAY

Типом элемента перечисления является перечисление, к которому он принадлежит:

>>> type(Weekday.MONDAY)
<enum 'Weekday'>
>>> isinstance(Weekday.FRIDAY, Weekday)
True

У элементов перечисления есть атрибут, содержащий только их name:

>>> print(Weekday.TUESDAY.name)
TUESDAY

Также у них есть атрибут для их value:

>>> Weekday.WEDNESDAY.value
3

В отличие от многих языков, рассматривающих перечисления исключительно как пары «имя/значение», перечисления Python могут иметь дополнительное поведение. Например, у datetime.date есть два метода для получения дня недели: weekday() и isoweekday(). Разница в том, что один из них ведет отсчет от 0 до 6, а другой — от 1 до 7. Вместо того чтобы запоминать это самостоятельно, можно добавить в перечисление Weekday метод, который извлекает день из экземпляра date и возвращает соответствующий элемент перечисления:

@classmethod
def from_date(cls, date):
    return cls(date.isoweekday())

Теперь полное перечисление Weekday выглядит так:

>>> class Weekday(Enum):
...     MONDAY = 1
...     TUESDAY = 2
...     WEDNESDAY = 3
...     THURSDAY = 4
...     FRIDAY = 5
...     SATURDAY = 6
...     SUNDAY = 7
...     #
...     @classmethod
...     def from_date(cls, date):
...         return cls(date.isoweekday())

Теперь мы можем узнать, какой сегодня день! Посмотрите:

>>> import datetime as dt
>>> Weekday.from_date(dt.date.today())
<Weekday.TUESDAY: 2>

Разумеется, если вы читаете это в другой день, вы увидите именно этот день.

Это перечисление Weekday отлично подходит, если нашей переменной нужен только один день, но что, если нужно несколько? Допустим, мы пишем функцию для отображения домашних дел на неделю и не хотим использовать list — можно использовать другой тип Enum:

>>> from enum import Flag
>>> class Weekday(Flag):
...     MONDAY = 1
...     TUESDAY = 2
...     WEDNESDAY = 4
...     THURSDAY = 8
...     FRIDAY = 16
...     SATURDAY = 32
...     SUNDAY = 64

Мы изменили две вещи: унаследовались от Flag, а все значения являются степенями двойки.

Как и в исходном перечислении Weekday выше, можно выбрать один элемент:

>>> first_week_day = Weekday.MONDAY
>>> first_week_day
<Weekday.MONDAY: 1>

Но Flag также позволяет объединить несколько элементов в одной переменной:

>>> weekend = Weekday.SATURDAY | Weekday.SUNDAY
>>> weekend
<Weekday.SATURDAY|SUNDAY: 96>

Можно даже перебирать переменную Flag:

>>> for day in weekend:
...     print(day)
Weekday.SATURDAY
Weekday.SUNDAY

Хорошо, давайте зададим несколько домашних дел:

>>> chores_for_ethan = {
...     'feed the cat': Weekday.MONDAY | Weekday.WEDNESDAY | Weekday.FRIDAY,
...     'do the dishes': Weekday.TUESDAY | Weekday.THURSDAY,
...     'answer SO questions': Weekday.SATURDAY,
...     }

А теперь напишем функцию для отображения домашних дел на заданный день:

>>> def show_chores(chores, day):
...     for chore, days in chores.items():
...         if day in days:
...             print(chore)
...
>>> show_chores(chores_for_ethan, Weekday.SATURDAY)
answer SO questions

Если фактические значения элементов не имеют значения, можно упростить себе работу и использовать auto() в качестве значений:

>>> from enum import auto
>>> class Weekday(Flag):
...     MONDAY = auto()
...     TUESDAY = auto()
...     WEDNESDAY = auto()
...     THURSDAY = auto()
...     FRIDAY = auto()
...     SATURDAY = auto()
...     SUNDAY = auto()
...     WEEKEND = SATURDAY | SUNDAY

Программный доступ к элементам перечисления и их атрибутам

Иногда бывает полезно получать доступ к элементам перечисления программно (то есть в ситуациях, когда Color.RED не подойдет, поскольку точный цвет неизвестен на момент написания программы). Enum обеспечивает такой доступ:

>>> Color(1)
<Color.RED: 1>
>>> Color(3)
<Color.BLUE: 3>

Чтобы получить доступ к элементам перечисления по имени, используйте обращение по индексу:

>>> Color['RED']
<Color.RED: 1>
>>> Color['GREEN']
<Color.GREEN: 2>

Если у вас есть элемент перечисления и вам нужно его name или value:

>>> member = Color.RED
>>> member.name
'RED'
>>> member.value
1

Дублирование элементов и значений перечисления

Наличие двух элементов перечисления с одинаковым именем недопустимо:

>>> class Shape(Enum):
...     SQUARE = 2
...     SQUARE = 3
...
Traceback (most recent call last):
...
TypeError: 'SQUARE' already defined as 2

Однако с элементом перечисления могут быть связаны другие имена. Если двум записям A и B присвоено одинаковое значение (и первой определена A), то B является псевдонимом элемента A. Поиск по значению A вернет элемент A. Поиск по имени A вернет элемент A. Поиск по имени B также вернет элемент A:

>>> class Shape(Enum):
...     SQUARE = 2
...     DIAMOND = 1
...     CIRCLE = 3
...     ALIAS_FOR_SQUARE = 2
...
>>> Shape.SQUARE
<Shape.SQUARE: 2>
>>> Shape.ALIAS_FOR_SQUARE
<Shape.SQUARE: 2>
>>> Shape(2)
<Shape.SQUARE: 2>

Примечание

Нельзя создавать элемент с тем же именем, что и у уже определенного атрибута (другого элемента, метода и т. д.), а также создавать атрибут с тем же именем, что и у элемента.

Обеспечение уникальности значений перечисления

По умолчанию в перечислениях несколько имен могут служить псевдонимами одного и того же значения. Если такое поведение нежелательно, можно использовать декоратор @unique:

>>> from enum import Enum, unique
>>> @unique
... class Mistake(Enum):
...     ONE = 1
...     TWO = 2
...     THREE = 3
...     FOUR = 3
...
Traceback (most recent call last):
...
ValueError: duplicate values found in <enum 'Mistake'>: FOUR -> THREE

Использование автоматических значений

Если точное значение не имеет значения, можно использовать auto:

>>> from enum import Enum, auto
>>> class Color(Enum):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> [member.value for member in Color]
[1, 2, 3]

Значения выбираются методом _generate_next_value_(), который можно переопределить:

>>> class AutoName(Enum):
...     @staticmethod
...     def _generate_next_value_(name, start, count, last_values):
...         return name
...
>>> class Ordinal(AutoName):
...     NORTH = auto()
...     SOUTH = auto()
...     EAST = auto()
...     WEST = auto()
...
>>> [member.value for member in Ordinal]
['NORTH', 'SOUTH', 'EAST', 'WEST']

Примечание

Метод _generate_next_value_() необходимо определить до любых элементов.

Итерация

При переборе элементов перечисления псевдонимы не предоставляются:

>>> list(Shape)
[<Shape.SQUARE: 2>, <Shape.DIAMOND: 1>, <Shape.CIRCLE: 3>]
>>> list(Weekday)
[<Weekday.MONDAY: 1>, <Weekday.TUESDAY: 2>, <Weekday.WEDNESDAY: 4>, <Weekday.THURSDAY: 8>, <Weekday.FRIDAY: 16>, <Weekday.SATURDAY: 32>, <Weekday.SUNDAY: 64>]

Обратите внимание, что псевдонимы Shape.ALIAS_FOR_SQUARE и Weekday.WEEKEND не отображаются.

Специальный атрибут __members__ — это доступное только для чтения упорядоченное отображение имен на элементы. Оно включает все имена, определенные в перечислении, в том числе псевдонимы:

>>> for name, member in Shape.__members__.items():
...     name, member
...
('SQUARE', <Shape.SQUARE: 2>)
('DIAMOND', <Shape.DIAMOND: 1>)
('CIRCLE', <Shape.CIRCLE: 3>)
('ALIAS_FOR_SQUARE', <Shape.SQUARE: 2>)

Атрибут __members__ можно использовать для детального программного доступа к элементам перечисления. Например, чтобы найти все псевдонимы:

>>> [name for name, member in Shape.__members__.items() if member.name != name]
['ALIAS_FOR_SQUARE']

Примечание

Псевдонимы флагов включают значения с несколькими установленными флагами, например 3, а также значения без установленных флагов, то есть 0.

Сравнения

Элементы перечисления сравниваются по идентичности:

>>> Color.RED is Color.RED
True
>>> Color.RED is Color.BLUE
False
>>> Color.RED is not Color.BLUE
True

Упорядоченные сравнения значений перечисления не поддерживаются. Элементы Enum не являются целыми числами (однако см. раздел IntEnum ниже):

>>> Color.RED < Color.BLUE
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'Color' and 'Color'

Однако сравнения на равенство определены:

>>> Color.BLUE == Color.RED
False
>>> Color.BLUE != Color.RED
True
>>> Color.BLUE == Color.BLUE
True

При сравнении со значениями, не принадлежащими перечислению, результат всегда будет «не равно» (исключение — IntEnum, которое было специально разработано для другого поведения; см. ниже):

>>> Color.BLUE == 2
False

Предупреждение

Модули можно перезагружать. Если перезагруженный модуль содержит перечисления, они будут созданы заново, и новые элементы могут не быть идентичны/равны исходным элементам.

Допустимые элементы и атрибуты перечислений

В большинстве приведенных выше примеров в качестве значений перечисления используются целые числа. Целые числа — краткий и удобный вариант (и они используются по умолчанию в функциональном API), но их использование не является обязательным. В подавляющем большинстве случаев неважно, каково фактическое значение перечисления. Но если значение важно, у перечислений могут быть произвольные значения.

Перечисления — это классы Python, поэтому они могут иметь методы и специальные методы, как обычно. Пусть у нас есть такое перечисление:

>>> class Mood(Enum):
...     FUNKY = 1
...     HAPPY = 3
...
...     def describe(self):
...         # self is the member here
...         return self.name, self.value
...
...     def __str__(self):
...         return 'my custom str! {0}'.format(self.value)
...
...     @classmethod
...     def favorite_mood(cls):
...         # cls here is the enumeration
...         return cls.HAPPY
...

Тогда:

>>> Mood.favorite_mood()
<Mood.HAPPY: 3>
>>> Mood.HAPPY.describe()
('HAPPY', 3)
>>> str(Mood.FUNKY)
'my custom str! 1'

Правила допустимых имен таковы: имена, начинающиеся и заканчивающиеся одним подчеркиванием, зарезервированы для enum и использоваться не могут; все остальные атрибуты, определенные в перечислении, становятся его элементами, за исключением специальных методов (__str__(), __add__() и т. д.), дескрипторов (методы также являются дескрипторами) и имен переменных, перечисленных в _ignore_.

Примечание: если в перечислении определены __new__() и/или __init__(), все значения, переданные элементу перечисления, будут переданы этим методам. Пример см. в разделе Planet.

Примечание

Метод __new__(), если он определен, используется при создании элементов Enum; после этого он заменяется методом __new__() из Enum, который используется после создания класса для поиска существующих элементов. Подробнее см. в разделе Когда использовать __new__(), а когда __init__().

Ограниченное наследование от Enum

У нового класса Enum должен быть один базовый класс-перечисление, не более одного конкретного типа данных и столько классов-миксинов на основе object, сколько необходимо. Порядок этих базовых классов следующий:

class EnumName([mix-in, ...,] [data-type,] base-enum):
    pass

Кроме того, наследование от перечисления разрешено только в том случае, если в перечислении не определены элементы. Поэтому следующий вариант запрещен:

>>> class MoreColor(Color):
...     PINK = 17
...
Traceback (most recent call last):
...
TypeError: <enum 'MoreColor'> cannot extend <enum 'Color'>

А следующий вариант разрешен:

>>> class Foo(Enum):
...     def some_behavior(self):
...         pass
...
>>> class Bar(Foo):
...     HAPPY = 1
...     SAD = 2
...

Разрешение наследования от перечислений, определяющих элементы, привело бы к нарушению важных инвариантов типов и экземпляров. С другой стороны, возможность использовать общее поведение для группы перечислений полезна. (Пример см. в разделе OrderedEnum.)

Поддержка dataclass

При наследовании от dataclass метод __repr__() не включает имя унаследованного класса. Например:

>>> from dataclasses import dataclass, field
>>> @dataclass
... class CreatureDataMixin:
...     size: str
...     legs: int
...     tail: bool = field(repr=False, default=True)
...
>>> class Creature(CreatureDataMixin, Enum):
...     BEETLE = 'small', 6
...     DOG = 'medium', 4
...
>>> Creature.DOG
<Creature.DOG: size='medium', legs=4>

Чтобы использовать стандартный repr(), передайте аргумент repr=False декоратору dataclass().

Изменено в версии 3.12: В области значения отображаются только поля dataclass, но не имя dataclass.

Примечание

Добавление декоратора @~dataclasses.dataclass к Enum и его подклассам не поддерживается. Это не вызовет ошибок, но приведет к очень странным результатам во время выполнения, например элементы будут равны друг другу:

>>> @dataclass               # don't do this: it does not make any sense
... class Color(Enum):
...    RED = 1
...    BLUE = 2
...
>>> Color.RED is Color.BLUE
False
>>> Color.RED == Color.BLUE  # problem is here: they should not be equal
True

Сериализация с помощью pickle

Перечисления можно сериализовать и десериализовать с помощью pickle:

>>> from test.test_enum import Fruit
>>> from pickle import dumps, loads
>>> Fruit.TOMATO is loads(dumps(Fruit.TOMATO))
True

Применяются обычные ограничения pickle: сериализуемые перечисления должны быть определены на верхнем уровне модуля, поскольку для десериализации их необходимо импортировать из этого модуля.

Примечание

Протокол pickle версии 4 позволяет без труда сериализовать перечисления, вложенные в другие классы.

Изменить способ сериализации/десериализации элементов перечисления можно, определив __reduce_ex__() в классе перечисления. По умолчанию используется значение, но для перечислений со сложными значениями может подойти имя:

>>> import enum
>>> class MyEnum(enum.Enum):
...     __reduce_ex__ = enum.pickle_by_enum_name

Примечание

Для флагов не рекомендуется использовать имя, поскольку псевдонимы без имен не будут десериализованы.

Функциональный API

Класс Enum можно вызывать, используя следующий функциональный API:

>>> Animal = Enum('Animal', 'ANT BEE CAT DOG')
>>> Animal
<enum 'Animal'>
>>> Animal.ANT
<Animal.ANT: 1>
>>> list(Animal)
[<Animal.ANT: 1>, <Animal.BEE: 2>, <Animal.CAT: 3>, <Animal.DOG: 4>]

Семантика этого API похожа на семантику namedtuple. Первый аргумент вызова Enum — это имя перечисления.

Второй аргумент — это источник имен элементов перечисления. Им может быть строка с именами, разделенными пробелами, последовательность имен, последовательность пар «ключ/значение» или отображение (например, словарь) имен на значения. Последние два варианта позволяют назначать перечислениям произвольные значения; в остальных случаях автоматически присваиваются возрастающие целые числа, начиная с 1 (используйте параметр start, чтобы указать другое начальное значение). Возвращается новый класс, производный от Enum. Иными словами, приведенное выше присваивание Animal эквивалентно следующему:

>>> class Animal(Enum):
...     ANT = 1
...     BEE = 2
...     CAT = 3
...     DOG = 4
...

Начальное число по умолчанию — 1, а не 0, потому что 0 в булевом контексте имеет значение False, тогда как по умолчанию все элементы перечисления считаются True.

Сериализация перечислений, созданных с помощью функционального API, может быть непростой: для определения модуля, в котором создается перечисление, используются детали реализации стека вызовов (например, это не сработает, если использовать вспомогательную функцию из отдельного модуля, а также может не работать в IronPython или Jython). Решение — явно указать имя модуля следующим образом:

>>> Animal = Enum('Animal', 'ANT BEE CAT DOG', module=__name__)

Предупреждение

Если module не указан и Enum не удается определить его значение, новые элементы Enum нельзя будет десериализовать; чтобы ошибки возникали ближе к месту их причины, сериализация с помощью pickle будет отключена.

Новый протокол pickle 4 также в некоторых случаях требует, чтобы __qualname__ указывал на место, где pickle сможет найти класс. Например, если класс доступен в классе SomeData в глобальной области видимости:

>>> Animal = Enum('Animal', 'ANT BEE CAT DOG', qualname='SomeData.Animal')

Полная сигнатура:

Enum(
    value='NewEnumName',
    names=<...>,
    *,
    module='...',
    qualname='...',
    type=<mixed-in class>,
    start=1,
    )
  • value: имя, которое будет присвоено новому классу перечисления.
  • names: элементы перечисления. Это может быть строка с именами, разделенными пробелами или запятыми (если не указано иное, значения начинаются с 1):

    'RED GREEN BLUE' | 'RED,GREEN,BLUE' | 'RED, GREEN, BLUE'
    

    или итератор имен:

    ['RED', 'GREEN', 'BLUE']
    

    или итератор пар (имя, значение):

    [('CYAN', 4), ('MAGENTA', 5), ('YELLOW', 6)]
    

    или отображение:

    {'CHARTREUSE': 7, 'SEA_GREEN': 11, 'ROSEMARY': 42}
    
  • module: имя модуля, в котором можно найти новый класс перечисления.
  • qualname: место в модуле, где можно найти новый класс перечисления.
  • type: тип, добавляемый в новый класс перечисления как миксин.
  • start: начальное число, если переданы только имена.

Изменено в версии 3.5: Добавлен параметр start.

Производные перечисления

IntEnum

Первый из предоставляемых вариантов Enum также является подклассом int. Элементы IntEnum можно сравнивать с целыми числами; следовательно, целочисленные перечисления разных типов также можно сравнивать друг с другом:

>>> from enum import IntEnum
>>> class Shape(IntEnum):
...     CIRCLE = 1
...     SQUARE = 2
...
>>> class Request(IntEnum):
...     POST = 1
...     GET = 2
...
>>> Shape == 1
False
>>> Shape.CIRCLE == 1
True
>>> Shape.CIRCLE == Request.POST
True

Однако их по-прежнему нельзя сравнивать со стандартными перечислениями Enum:

>>> class Shape(IntEnum):
...     CIRCLE = 1
...     SQUARE = 2
...
>>> class Color(Enum):
...     RED = 1
...     GREEN = 2
...
>>> Shape.CIRCLE == Color.RED
False

Значения IntEnum ведут себя как целые числа и в других ожидаемых случаях:

>>> int(Shape.CIRCLE)
1
>>> ['a', 'b', 'c'][Shape.CIRCLE]
'b'
>>> [i for i in range(Shape.SQUARE)]
[0, 1]

StrEnum

Второй из предоставляемых вариантов Enum также является подклассом str. Элементы StrEnum можно сравнивать со строками; следовательно, строковые перечисления разных типов также можно сравнивать друг с другом.

Добавлено в версии 3.11.

IntFlag

Следующий предоставляемый вариант Enum, IntFlag, также основан на int. Отличие в том, что элементы IntFlag можно объединять с помощью побитовых операторов (&, |, ^, ~), и результат, если это возможно, по-прежнему будет элементом IntFlag. Как и элементы IntEnum, элементы IntFlag также являются целыми числами и могут использоваться везде, где используется int.

Примечание

Любая операция над элементом IntFlag, кроме побитовых операций, приводит к потере принадлежности к IntFlag.

Побитовые операции, результатом которых являются недопустимые значения IntFlag, приводят к потере принадлежности к IntFlag. Подробности см. в описании FlagBoundary.

Добавлено в версии 3.6.

Изменено в версии 3.11.

Пример класса IntFlag:

>>> from enum import IntFlag
>>> class Perm(IntFlag):
...     R = 4
...     W = 2
...     X = 1
...
>>> Perm.R | Perm.W
<Perm.R|W: 6>
>>> Perm.R + Perm.W
6
>>> RW = Perm.R | Perm.W
>>> Perm.R in RW
True

Комбинациям также можно присваивать имена:

>>> class Perm(IntFlag):
...     R = 4
...     W = 2
...     X = 1
...     RWX = 7
...
>>> Perm.RWX
<Perm.RWX: 7>
>>> ~Perm.RWX
<Perm: 0>
>>> Perm(7)
<Perm.RWX: 7>

Примечание

Именованные комбинации считаются псевдонимами. Псевдонимы не отображаются при итерации, но могут возвращаться при поиске по значению.

Изменено в версии 3.11.

Ещё одно важное отличие между IntFlag и Enum состоит в том, что при отсутствии установленных флагов (значение равно 0) результат проверки логического значения будет False:

>>> Perm.R & Perm.X
<Perm: 0>
>>> bool(Perm.R & Perm.X)
False

Поскольку элементы IntFlag также являются подклассами int, их можно объединять с целыми числами (но принадлежность к IntFlag может быть утрачена):

>>> Perm.X | 4
<Perm.R|X: 5>

>>> Perm.X + 8
9

Примечание

Оператор отрицания ~ всегда возвращает элемент IntFlag с положительным значением:

>>> (~Perm.X).value == (Perm.R|Perm.W).value == 6
True

Элементы IntFlag также можно перебирать:

>>> list(RW)
[<Perm.R: 4>, <Perm.W: 2>]

Добавлено в версии 3.11.

Flag

Последний вариант — Flag. Как и элементы IntFlag, элементы Flag можно объединять с помощью побитовых операторов (&, |, ^, ~). В отличие от IntFlag, их нельзя объединять с другими перечислениями Flag или сравнивать с ними, а также нельзя объединять с int или сравнивать с ним. Хотя значения можно задавать напрямую, рекомендуется использовать в качестве значения auto и позволить Flag выбрать подходящее значение.

Добавлено в версии 3.6.

Как и для IntFlag, если в результате объединения элементов Flag не установлено ни одного флага, результат проверки логического значения будет False:

>>> from enum import Flag, auto
>>> class Color(Flag):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> Color.RED & Color.GREEN
<Color: 0>
>>> bool(Color.RED & Color.GREEN)
False

Отдельные флаги должны иметь значения, являющиеся степенями двойки (1, 2, 4, 8, …), а комбинации флагов — нет:

>>> class Color(Flag):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...     WHITE = RED | BLUE | GREEN
...
>>> Color.WHITE
<Color.WHITE: 7>

Присвоение имени условию «ни один флаг не установлен» не меняет его логическое значение:

>>> class Color(Flag):
...     BLACK = 0
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> Color.BLACK
<Color.BLACK: 0>
>>> bool(Color.BLACK)
False

Элементы Flag также можно перебирать:

>>> purple = Color.RED | Color.BLUE
>>> list(purple)
[<Color.RED: 1>, <Color.BLUE: 2>]

Добавлено в версии 3.11.

Примечание

Для большинства нового кода настоятельно рекомендуется использовать Enum и Flag, поскольку IntEnum и IntFlag нарушают некоторые семантические гарантии перечисления (их можно сравнивать с целыми числами, а значит, в силу транзитивности — и с другими несвязанными перечислениями). IntEnum и IntFlag следует использовать только в тех случаях, когда Enum и Flag не подходят; например, когда целочисленные константы заменяются перечислениями или требуется взаимодействие с другими системами.

Прочее

Хотя IntEnum входит в модуль enum, его было бы очень просто реализовать самостоятельно:

class IntEnum(int, ReprEnum):   # or Enum instead of ReprEnum
    pass

Это показывает, насколько просто определять похожие производные перечисления; например, FloatEnum, в который вместо float добавлен int.

Несколько правил:

  1. При создании подкласса Enum типы-миксины должны располагаться в последовательности базовых классов перед самим классом Enum, как в приведённом выше примере с IntEnum.
  2. Типы-миксины должны допускать создание подклассов. Например, для bool и range нельзя создавать подклассы; если использовать их как тип-миксин, при создании Enum возникнет ошибка.
  3. Хотя у Enum элементы могут быть любого типа, после добавления дополнительного типа-миксина все элементы должны иметь значения этого типа, например int выше. Это ограничение не относится к миксинам, которые добавляют только методы и не задают другой тип.
  4. При добавлении другого типа данных атрибут value не совпадает с самим элементом перечисления, хотя эквивалентен ему и при сравнении считается равным.
  5. data type — это миксин, который определяет __new__() или dataclass
  6. Форматирование в стиле %-форматирования: %s и %r вызывают соответственно методы __str__() и __repr__() класса Enum; другие спецификаторы (например, %i или %h для IntEnum) обрабатывают элемент перечисления как тип, добавленный через миксин.
  7. Форматированные строковые литералы, str.format() и format() используют метод __str__() перечисления.

Примечание

Поскольку IntEnum, IntFlag и StrEnum предназначены для непосредственной замены существующих констант, их метод __str__() заменён методом __str__() соответствующего типа данных.

Когда использовать __new__(), а когда __init__()

__new__() необходимо использовать, если вы хотите настроить фактическое значение элемента Enum. Любые другие изменения можно внести либо в __new__(), либо в __init__(), причём предпочтительнее __init__().

Например, если конструктору нужно передать несколько элементов, но только один из них должен стать значением:

>>> class Coordinate(bytes, Enum):
...     """
...     Coordinate with binary codes that can be indexed by the int code.
...     """
...     def __new__(cls, value, label, unit):
...         obj = bytes.__new__(cls, [value])
...         obj._value_ = value
...         obj.label = label
...         obj.unit = unit
...         return obj
...     PX = (0, 'P.X', 'km')
...     PY = (1, 'P.Y', 'km')
...     VX = (2, 'V.X', 'km/s')
...     VY = (3, 'V.Y', 'km/s')
...

>>> print(Coordinate['PY'])
Coordinate.PY

>>> print(Coordinate(3))
Coordinate.VY

Предупреждение

Не вызывайте super().__new__(): будет найден __new__, предназначенный только для поиска; вместо этого используйте непосредственно тип данных.

Тонкости

Поддерживаемые имена __dunder__ и _sunder_

Поддерживаемые имена __dunder__ и _sunder_ перечислены в документации по API Enum.

_Private__names

Приватные имена не преобразуются в элементы перечисления, а остаются обычными атрибутами.

Изменено в версии 3.11.

Тип элемента Enum

Элементы перечисления являются экземплярами своего класса перечисления, и обычно к ним обращаются как к EnumClass.member. В некоторых ситуациях, например при написании пользовательского поведения перечисления, полезно иметь возможность напрямую обратиться к одному элементу из другого; это поддерживается. Однако во избежание конфликтов имён между именами элементов и атрибутами/методами классов, добавленных через миксин, настоятельно рекомендуется использовать имена в верхнем регистре.

Изменено в версии 3.5.

Создание элементов с примесью других типов данных

При создании подкласса другого типа данных, например int или str, вместе с Enum все значения после = передаются конструктору этого типа данных. Например:

>>> class MyEnum(IntEnum):      # help(int) -> int(x, base=10) -> integer
...     example = '11', 16      # so x='11' and base=16
...
>>> MyEnum.example.value        # and hex(11) is...
17

Логическое значение классов и элементов Enum

Логическое значение классов перечислений, в которые добавлены типы, не являющиеся Enum (например, int, str и т. д.), определяется правилами добавленного типа; в остальных случаях все элементы считаются True. Чтобы логическое значение вашего перечисления зависело от значения элемента, добавьте в класс следующий код:

def __bool__(self):
    return bool(self.value)

Обычные классы Enum всегда считаются True.

Классы Enum с методами

Если добавить в подкласс перечисления дополнительные методы, как в приведённом ниже классе Planet, эти методы будут отображаться при вызове dir() для элемента, но не для класса:

>>> dir(Planet)
['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__members__', '__module__']
>>> dir(Planet.EARTH)
['__class__', '__doc__', '__module__', 'mass', 'name', 'radius', 'surface_gravity', 'value']

Объединение элементов Flag

При переборе комбинации элементов Flag будут возвращены только элементы, состоящие из одного бита:

>>> class Color(Flag):
...     RED = auto()
...     GREEN = auto()
...     BLUE = auto()
...     MAGENTA = RED | BLUE
...     YELLOW = RED | GREEN
...     CYAN = GREEN | BLUE
...
>>> Color(3)  # named combination
<Color.YELLOW: 3>
>>> Color(7)      # not named combination
<Color.RED|GREEN|BLUE: 7>

Нюансы Flag и IntFlag

Используя следующий фрагмент в примерах:

>>> class Color(IntFlag):
...     BLACK = 0
...     RED = 1
...     GREEN = 2
...     BLUE = 4
...     PURPLE = RED | BLUE
...     WHITE = RED | GREEN | BLUE
...

получаем следующие утверждения:

  • флаги с одним битом являются каноническими
  • флаги с несколькими битами и нулём битов являются псевдонимами
  • при итерации возвращаются только канонические флаги:

    >>> list(Color.WHITE)
    [<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 4>]
    
  • отрицание флага или набора флагов возвращает новый флаг/набор флагов с соответствующим положительным целочисленным значением:

    >>> Color.BLUE
    <Color.BLUE: 4>
    
    >>> ~Color.BLUE
    <Color.RED|GREEN: 3>
    
  • имена псевдофлагов формируются из имён их элементов:

    >>> (Color.RED | Color.GREEN).name
    'RED|GREEN'
    
    >>> class Perm(IntFlag):
    ...     R = 4
    ...     W = 2
    ...     X = 1
    ...
    >>> (Perm.R & Perm.W).name is None  # effectively Perm(0)
    True
    
  • многоразрядные флаги, то есть псевдонимы, могут возвращаться в результате операций:

    >>> Color.RED | Color.BLUE
    <Color.PURPLE: 5>
    
    >>> Color(7)  # or Color(-1)
    <Color.WHITE: 7>
    
    >>> Color(0)
    <Color.BLACK: 0>
    
  • проверка членства / вхождения: флаги с нулевым значением всегда считаются входящими:

    >>> Color.BLACK in Color.WHITE
    True
    

    в остальных случаях True возвращается, только если все биты одного флага присутствуют в другом:

    >>> Color.PURPLE in Color.WHITE
    True
    
    >>> Color.GREEN in Color.PURPLE
    False
    

Появился новый механизм граничных значений, который определяет обработку битов вне допустимого диапазона / недопустимых битов: STRICT, CONFORM, EJECT и KEEP:

  • STRICT –> вызывает исключение при получении недопустимых значений
  • CONFORM –> отбрасывает все недопустимые биты
  • EJECT –> теряет статус Flag и становится обычным int с заданным значением
  • KEEP –> сохраняет дополнительные биты

    • сохраняет статус Flag и дополнительные биты
    • дополнительные биты не отображаются при итерации
    • дополнительные биты отображаются в repr() и str()

По умолчанию для Flag используется STRICT, для IntFlag — EJECT, а для _convert_ — KEEP (пример ситуации, когда требуется KEEP, см. в описании ssl.Options).

Чем отличаются Enums и Flags?

У перечислений есть собственный метакласс, который влияет на многие аспекты как производных классов Enum, так и их экземпляров (элементов).

Классы Enum

Метакласс EnumType отвечает за предоставление методов __contains__(), __dir__(), __iter__() и других методов, позволяющих выполнять с классом Enum действия, которые не сработали бы с обычным классом, например list(Color) или some_enum_var in Color. EnumType отвечает за корректную работу различных других методов итогового класса Enum (например, __new__(), __getnewargs__(), __str__() и __repr__()).

Классы Flag

В Flags понятие псевдонимов расширено: чтобы флаг был каноническим, его значение должно быть степенью двойки, а имя не должно дублироваться. Поэтому помимо определения псевдонима для Enum, псевдонимом считается флаг без значения (то есть 0) или со значением, содержащим несколько степеней двойки (например, 3).

Элементы Enum (то есть экземпляры)

Самое интересное в элементах перечисления — то, что они являются синглтонами. Метакласс EnumType создаёт их все во время создания самого класса перечисления, а затем устанавливает специальный метод __new__(), который гарантирует, что новые экземпляры не будут созданы, а будут возвращаться только уже существующие элементы.

Элементы Flag

Элементы Flag можно перебирать так же, как класс Flag; при этом будут возвращены только канонические элементы. Например:

>>> list(Color)
[<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 4>]

(Обратите внимание, что BLACK, PURPLE и WHITE не отображаются.)

Инвертирование элемента Flag возвращает соответствующее положительное значение, а не отрицательное. Например:

>>> ~Color.RED
<Color.GREEN|BLUE: 6>

Длина элемента Flag соответствует количеству содержащихся в нём значений, являющихся степенями двойки. Например:

>>> len(Color.PURPLE)
2

Сборник рецептов Enum

Хотя Enum, IntEnum, StrEnum, Flag и IntFlag подходят для большинства сценариев использования, они не могут охватить их все. Ниже приведены рецепты для некоторых других типов перечислений, которые можно использовать напрямую или взять за основу для создания собственных.

Пропуск значений

Во многих сценариях использования фактическое значение перечисления не имеет значения. Существует несколько способов определить такой простой тип перечисления:

  • использовать экземпляры auto в качестве значений
  • использовать экземпляры object в качестве значений
  • использовать описательную строку в качестве значения
  • использовать кортеж в качестве значения и пользовательский метод __new__(), заменяющий кортеж значением типа int

Использование любого из этих способов даёт понять пользователю, что значения не имеют значения, а также позволяет добавлять, удалять элементы или менять их порядок, не перенумеровывая остальные.

Использование auto

Использование auto будет выглядеть так:

>>> class Color(Enum):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> Color.GREEN
<Color.GREEN: 3>

Использование object

Использование object будет выглядеть так:

>>> class Color(Enum):
...     RED = object()
...     GREEN = object()
...     BLUE = object()
...
>>> Color.GREEN
<Color.GREEN: <object object at 0x...>>

Это также хороший пример того, почему может понадобиться написать собственный метод __repr__():

>>> class Color(Enum):
...     RED = object()
...     GREEN = object()
...     BLUE = object()
...     def __repr__(self):
...         return "<%s.%s>" % (self.__class__.__name__, self._name_)
...
>>> Color.GREEN
<Color.GREEN>

Использование описательной строки

Использование строки в качестве значения будет выглядеть так:

>>> class Color(Enum):
...     RED = 'stop'
...     GREEN = 'go'
...     BLUE = 'too fast!'
...
>>> Color.GREEN
<Color.GREEN: 'go'>

Использование пользовательского метода __new__()

Использование автоматически нумерующего метода __new__() будет выглядеть так:

>>> class AutoNumber(Enum):
...     def __new__(cls):
...         value = len(cls.__members__) + 1
...         obj = object.__new__(cls)
...         obj._value_ = value
...         return obj
...
>>> class Color(AutoNumber):
...     RED = ()
...     GREEN = ()
...     BLUE = ()
...
>>> Color.GREEN
<Color.GREEN: 2>

Чтобы сделать AutoNumber более универсальным, добавьте *args в сигнатуру:

>>> class AutoNumber(Enum):
...     def __new__(cls, *args):      # this is the only change from above
...         value = len(cls.__members__) + 1
...         obj = object.__new__(cls)
...         obj._value_ = value
...         return obj
...

Затем, наследуясь от AutoNumber, можно написать собственный __init__ для обработки любых дополнительных аргументов:

>>> class Swatch(AutoNumber):
...     def __init__(self, pantone='unknown'):
...         self.pantone = pantone
...     AUBURN = '3497'
...     SEA_GREEN = '1246'
...     BLEACHED_CORAL = () # New color, no Pantone code yet!
...
>>> Swatch.SEA_GREEN
<Swatch.SEA_GREEN: 2>
>>> Swatch.SEA_GREEN.pantone
'1246'
>>> Swatch.BLEACHED_CORAL.pantone
'unknown'

Примечание

Метод __new__(), если он определён, используется при создании элементов Enum; после этого он заменяется методом __new__() класса Enum, который после создания класса используется для поиска существующих элементов.

Предупреждение

Не вызывайте super().__new__(): будет найден __new__, предназначенный только для поиска; вместо этого используйте непосредственно тип данных, например:

obj = int.__new__(cls, value)

OrderedEnum

Упорядоченное перечисление, не основанное на IntEnum, благодаря чему оно сохраняет обычные инварианты Enum (например, невозможность сравнения с другими перечислениями):

>>> class OrderedEnum(Enum):
...     def __ge__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value >= other.value
...         return NotImplemented
...     def __gt__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value > other.value
...         return NotImplemented
...     def __le__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value <= other.value
...         return NotImplemented
...     def __lt__(self, other):
...         if self.__class__ is other.__class__:
...             return self.value < other.value
...         return NotImplemented
...
>>> class Grade(OrderedEnum):
...     A = 5
...     B = 4
...     C = 3
...     D = 2
...     F = 1
...
>>> Grade.C < Grade.A
True

DuplicateFreeEnum

При обнаружении повторяющегося значения элемента вызывает ошибку вместо создания псевдонима:

>>> class DuplicateFreeEnum(Enum):
...     def __init__(self, *args):
...         cls = self.__class__
...         if any(self.value == e.value for e in cls):
...             a = self.name
...             e = cls(self.value).name
...             raise ValueError(
...                 "aliases not allowed in DuplicateFreeEnum:  %r --> %r"
...                 % (a, e))
...
>>> class Color(DuplicateFreeEnum):
...     RED = 1
...     GREEN = 2
...     BLUE = 3
...     GRENE = 2
...
Traceback (most recent call last):
  ...
ValueError: aliases not allowed in DuplicateFreeEnum:  'GRENE' --> 'GREEN'

Примечание

Это полезный пример создания подкласса Enum для добавления или изменения других вариантов поведения, а также для запрета псевдонимов. Если требуется только запретить псевдонимы, вместо этого можно использовать декоратор unique().

MultiValueEnum

Поддерживает несколько значений для одного элемента:

>>> class MultiValueEnum(Enum):
...     def __new__(cls, value, *values):
...         self = object.__new__(cls)
...         self._value_ = value
...         for v in values:
...             self._add_value_alias_(v)
...         return self
...
>>> class DType(MultiValueEnum):
...     float32 = 'f', 8
...     double64 = 'd', 9
...
>>> DType('f')
<DType.float32: 'f'>
>>> DType(9)
<DType.double64: 'd'>

Planet

Если определён метод __new__() или __init__(), значение элемента перечисления передаётся этим методам:

>>> class Planet(Enum):
...     MERCURY = (3.303e+23, 2.4397e6)
...     VENUS   = (4.869e+24, 6.0518e6)
...     EARTH   = (5.976e+24, 6.37814e6)
...     MARS    = (6.421e+23, 3.3972e6)
...     JUPITER = (1.9e+27,   7.1492e7)
...     SATURN  = (5.688e+26, 6.0268e7)
...     URANUS  = (8.686e+25, 2.5559e7)
...     NEPTUNE = (1.024e+26, 2.4746e7)
...     def __init__(self, mass, radius):
...         self.mass = mass       # in kilograms
...         self.radius = radius   # in meters
...     @property
...     def surface_gravity(self):
...         # universal gravitational constant  (m3 kg-1 s-2)
...         G = 6.67300E-11
...         return G * self.mass / (self.radius * self.radius)
...
>>> Planet.EARTH.value
(5.976e+24, 6378140.0)
>>> Planet.EARTH.surface_gravity
9.802652743337129

TimePeriod

Пример использования атрибута _ignore_:

>>> import datetime as dt
>>> class Period(dt.timedelta, Enum):
...     "different lengths of time"
...     _ignore_ = 'Period i'
...     Period = vars()
...     for i in range(367):
...         Period['day_%d' % i] = i
...
>>> list(Period)[:2]
[<Period.day_0: datetime.timedelta(0)>, <Period.day_1: datetime.timedelta(days=1)>]
>>> list(Period)[-2:]
[<Period.day_365: datetime.timedelta(days=365)>, <Period.day_366: datetime.timedelta(days=366)>]

Создание подкласса EnumType

Хотя большинство потребностей в перечислениях можно удовлетворить, настроив подклассы Enum с помощью декораторов классов или пользовательских функций, для изменения поведения Enum можно создать подкласс EnumType.

© 2001 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/howto/enum.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API