Python Enums
Изучите Python enums: Enum, IntEnum, Flag, auto(), методы, безопасное сравнение и замена магических чисел в коде.
Enum (сокращение от enumeration — перечисление) — это набор именованных константных значений, объединённых под одним типом. Вместо того чтобы разбрасывать по коду голые целые числа или строки вроде 1, 2, "pending", "active", вы даёте каждому из них описательное имя — Status.PENDING, Color.RED — и Python гарантирует, что это имя всегда соответствует одному и тому же значению.
Эта глава охватывает:
- Зачем нужны enums и какие проблемы они решают
- Создание enum с помощью класса
Enum - Доступ к членам по имени и по значению
- Итерация по enum
auto()— автоматическое присвоение значений PythonIntEnum— enums, ведущие себя как целые числаFlag— комбинируемые битовые флаги- Добавление методов и свойств к enum
- Псевдонимы,
@uniqueи_missing_ - Когда использовать enums, а когда другие подходы
Перед чтением этой главы убедитесь, что вы знакомы с классами и объектами Python и типами данных Python.
Зачем использовать Enums?
Рассмотрим функцию, которая обрабатывает статус заказа, переданный в виде простого целого числа:
def handle_order(status):
if status == 1:
print("Order is pending")
elif status == 2:
print("Order is active")
elif status == 3:
print("Order is complete")Это работает, но имеет реальные проблемы:
- Магические числа. Что означает
2само по себе? Нужно возвращаться к определению функции. - Нет валидации.
handle_order(99)молча ничего не делает — ни ошибки, ни предупреждения. - Опечатки незаметны.
handle_order(2)иhandle_order(20)— оба валидный Python. - Рефакторинг рискован. Если вы решите, что
1должна означать что-то другое, придётся найти каждую1в кодовой базе.
Enums решают все эти проблемы. Та же логика, написанная с enum, самодокументируема, безопасна и удобна для рефакторинга:
from enum import Enum
class OrderStatus(Enum):
PENDING = 1
ACTIVE = 2
COMPLETE = 3
def handle_order(status: OrderStatus):
if status == OrderStatus.PENDING:
print("Order is pending")
elif status == OrderStatus.ACTIVE:
print("Order is active")
elif status == OrderStatus.COMPLETE:
print("Order is complete")
handle_order(OrderStatus.ACTIVE) # Order is activeНамерение понятно, и Python не позволяет handle_order(99) случайно совпасть с каким-либо ветвлением.
Создание Enum
Импортируйте Enum из модуля enum (часть стандартной библиотеки Python — установка не требуется) и создайте подкласс:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3Каждый атрибут класса (RED, GREEN, BLUE) становится членом enum. Значения справа (1, 2, 3) могут быть целыми числами, строками или любым другим типом — выбор за вами.
Доступ к членам
Есть три способа обратиться к члену enum:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
# Attribute access (most common)
print(Color.RED) # Color.RED
# By name (square bracket notation)
print(Color['GREEN']) # Color.GREEN
# By value (call the class with the value)
print(Color(3)) # Color.BLUEКаждый член предоставляет два атрибута:
print(Color.RED.name) # RED
print(Color.RED.value) # 1Используйте .name, когда нужна человекочитаемая метка (для логирования или отображения), и .value, когда нужно передать базовое значение во внешнюю систему (базу данных, API).
repr и type
print(repr(Color.RED)) # <Color.RED: 1>
print(type(Color.RED)) # <enum 'Color'>Член enum является экземпляром своего класса enum, а не int или str.
Итерация по Enum
Enums поддерживают итерацию. Итерация возвращает члены в порядке определения:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
for color in Color:
print(color.name, color.value)
# RED 1
# GREEN 2
# BLUE 3Также можно проверить принадлежность:
print(Color.RED in Color) # TrueЭто делает enums удобными для заполнения выпадающих списков, создания таблиц диспетчеризации или построения списков вариантов для пользовательского ввода.
auto() — Автоматические значения
Если конкретные значения не важны — важна лишь уникальность каждого члена — используйте auto(). Python присваивает последовательные целые числа, начиная с 1:
from enum import Enum, auto
class Direction(Enum):
NORTH = auto()
SOUTH = auto()
EAST = auto()
WEST = auto()
for d in Direction:
print(d.name, d.value)
# NORTH 1
# SOUTH 2
# EAST 3
# WEST 4auto() особенно полезен, когда enum будет расти со временем и вы не хотите вручную перенумеровывать члены.
Сравнение членов Enum
Для сравнения членов используйте is или ==. Оба варианта работают, но is немного быстрее, так как члены enum являются синглтонами — каждое имя соответствует ровно одному объекту:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
print(Color.RED is Color.RED) # True
print(Color.RED == Color.RED) # True
print(Color.RED == Color.GREEN) # FalseЧлен обычного Enum не равен своему исходному значению:
print(Color.RED == 1) # FalseЭто сделано намеренно. Это предотвращает случайное равенство между разными enums, которые используют одно и то же целое число:
class Size(Enum):
SMALL = 1
print(Color.RED == Size.SMALL) # False — different typesЕсли вам нужно сравнение по значению (например, member > 1), используйте IntEnum (см. ниже).
IntEnum — Enums, ведущие себя как целые числа
Члены IntEnum являются также обычными целыми числами Python. Это означает, что вы можете использовать арифметику, операторы сравнения и передавать их везде, где ожидается int:
from enum import IntEnum
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
print(Priority.HIGH > Priority.LOW) # True
print(Priority.MEDIUM + 10) # 12
print(Priority.HIGH == 3) # TrueРаспространённый вариант использования — сортировка списка членов enum:
from enum import IntEnum
class Level(IntEnum):
LOW = 1
MED = 2
HIGH = 3
levels = [Level.HIGH, Level.LOW, Level.MED]
print([l.name for l in sorted(levels)]) # ['LOW', 'MED', 'HIGH']Когда предпочесть Enum вместо IntEnum
Прозрачность целочисленности IntEnum — одновременно его слабость: Priority.HIGH == 3 равно True, поэтому случайный литерал 3 будет молча сравниваться как равный Priority.HIGH. Используйте обычный Enum там, где нужна строгая типобезопасность, и IntEnum только тогда, когда вам действительно нужна целочисленная арифметика или необходимо взаимодействовать с API, работающим с сырыми числами.
Flag — Комбинируемые битовые флаги
Flag предназначен для сценариев, где несколько опций могут быть активны одновременно. Его члены являются степенями двойки, и вы комбинируете их с оператором | (побитовое ИЛИ):
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
ALL = READ | WRITE | EXECUTE
user = Permission.READ | Permission.WRITE
print(user) # Permission.WRITE|READ
print(Permission.READ in user) # True
print(Permission.EXECUTE in user) # Falseauto() внутри Flag присваивает последовательные степени двойки (1, 2, 4, 8, …), так что комбинирование членов через | никогда не даёт неоднозначных результатов.
Используйте Flag для систем разрешений, переключателей функций и любых ситуаций, где нужен компактный набор булевых переключателей.
Добавление методов и свойств
Поскольку enum — это класс, к нему можно добавлять методы и свойства. Это позволяет хранить связанную логику внутри типа, а не разбрасывать её по цепочкам if/elif:
from enum import Enum
class HttpStatus(Enum):
OK = 200
CREATED = 201
NOT_FOUND = 404
INTERNAL_ERROR = 500
@property
def is_success(self):
return 200 <= self.value < 300
@property
def is_error(self):
return self.value >= 400
def handle_response(status: HttpStatus):
if status.is_success:
print(f"{status.value} {status.name}: request succeeded")
elif status.is_error:
print(f"{status.value} {status.name}: request failed")
handle_response(HttpStatus.OK) # 200 OK: request succeeded
handle_response(HttpStatus.NOT_FOUND) # 404 NOT_FOUND: request failedТакже можно задать enum пользовательский __init__ для хранения дополнительных данных на каждый член. Значения передаются в виде кортежей:
from enum import Enum
class Planet(Enum):
MERCURY = (3.303e+23, 2.4397e6)
VENUS = (4.869e+24, 6.0518e6)
EARTH = (5.976e+24, 6.37814e6)
def __init__(self, mass, radius):
self.mass = mass
self.radius = radius
@property
def surface_gravity(self):
G = 6.67430e-11
return G * self.mass / (self.radius ** 2)
print(round(Planet.EARTH.surface_gravity, 2)) # 9.8
print(round(Planet.MERCURY.surface_gravity, 2)) # 3.7Кортеж (mass, radius) становится аргументами конструктора; self.value по-прежнему хранит полный кортеж.
Псевдонимы и @unique
Если два члена имеют одинаковое значение, второй становится псевдонимом — он разрешается в первый член. Псевдонимы не возвращаются при итерации:
from enum import Enum
class Status(Enum):
ACTIVE = 1
RUNNING = 1 # alias for ACTIVE
print(Status.ACTIVE is Status.RUNNING) # True
print(list(Status)) # [<Status.ACTIVE: 1>]Псевдонимы иногда полезны (например, устаревшее имя, указывающее на новое), но могут скрывать опечатки. Примените декоратор @unique, чтобы полностью запретить дублирование значений:
from enum import Enum, unique
@unique
class Status(Enum):
PENDING = 1
ACTIVE = 2
INACTIVE = 3
# Trying to add a duplicate value to a @unique enum raises ValueError:
# ValueError: duplicate values found in <enum 'Bad'>: B -> A@unique — хорошее значение по умолчанию для любого enum, где случайное создание псевдонима было бы ошибкой.
Пользовательский поиск с _missing_
По умолчанию вызов Color('unknown') вызывает ValueError. Вы можете переопределить метод класса _missing_ для обработки нераспознанных значений — например, для поиска без учёта регистра:
from enum import Enum
class Color(Enum):
RED = 'red'
GREEN = 'green'
BLUE = 'blue'
@classmethod
def _missing_(cls, value):
if isinstance(value, str):
for member in cls:
if member.value == value.lower():
return member
return None
print(Color('RED')) # Color.RED
print(Color('Green')) # Color.GREEN_missing_ получает значение, которое не было найдено. Верните соответствующий член или None (что позволяет Python вызвать стандартный ValueError).
Когда использовать Enums
Enums — правильный выбор, когда:
- Переменная может принимать только одно из фиксированного набора именованных состояний (статус заказа, HTTP-метод, масть карты).
- Вы хотите предотвратить молчаливое прохождение недопустимых значений.
- Одна и та же концепция сравнивается в нескольких местах и вам нужен единый источник истины.
- Вам нужно итерировать по всем допустимым значениям (заполнение формы, документирование API).
Вам, вероятно, не нужен enum, когда:
- Набор значений открытый или изменяется во время выполнения (используйте словарь или таблицу поиска в базе данных).
- Вам нужны только два состояния —
True/Falseс чётким булевым смыслом проще. - Значения поступают из пользовательского ввода, который должен быть проверен по схеме — рассмотрите такую библиотеку, как Pydantic, которая хорошо интегрируется с Python enums.
Для связанных паттернов см. датаклассы Python (для структурированных данных со значениями по умолчанию) и абстрактные классы Python (для обеспечения контрактов интерфейса в подклассах). Если вам нужны именованные контейнеры констант без полного механизма enum, модуль collections Python предлагает namedtuple в качестве альтернативы.