Enum (скорочено від Enumerations) – надають можливість визначати набір пов’язаних іменованих констант, котрі можна використовувати для представлення значень зі змістовними іменами, замість використання так званих магічних чисел, або жорстко закодованих рядків. Це робить код зрозумілішим та легшим в підтримці.

Зміст запису:

  1. Створення enum у Python
  2. Автоматичне встановлення значень членам enum
  3. Ітерація по enum
  4. Порівняння членів enum
  5. Унікальні значення в enum Python
  6. Використання enum в операторах if та match
  7. Розширення enum новою поведінкою
  8. Enum з multiple значеннями
  9. Інші класи enum

Створення enum у Python

Python надає можливість створювати власні перерахування за допомогою вбудованого модуля enum. Для цього використовується клас Enum, де кожний член перерахування визначається як атрибут класу.

Щоб створити перерахування, слід оголосити клас, який успадковує клас enum.Enum. Члени перерахування здаються як атрибути цього класу. Приклад:

from enum import Enum


class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

Нижче розглянуто різноманітні приклади доступу до членів enum у Python:

# Accessing enum members
print(list(Color))
# [<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 3>]

# Accessing enum member by name
print(Color.GREEN)
# Color.GREEN

# Accessing enum member by name
print(Color['GREEN'])
# Color.GREEN

# Accessing enum member by value
print(Color(2))
# Color.GREEN

# Accessing enum member value
print(Color.GREEN.value)
# 2

# Accessing enum member name
print(Color.GREEN.name)
#  GREEN

Атрибути класу Enum конвертуються в його екземпляри, кожен з котрих має власні атрибути name (зберігає ім’я), та value (зберігає встановлене значення).

Зазвичай членам enum в якості значень встановлюють послідовні цілі числа. Однак у Python значення членів enum можуть бути будь-якого типу, включаючи користувацькі типи. Приклад використання рядкових значень:

from enum import Enum


class Size(Enum):
    S = 'small'
    M = 'medium'
    L = 'large'


print(list(Size))
# [<Size.S: 'small'>, <Size.M: 'medium'>, <Size.L: 'large'>]

Ще один спосіб оголошення enum у Python, використання функціонального API, де потрібно просто викликати Enum з відповідними аргументами, так само як при виклику функції:

from enum import Enum

Color = Enum('Color', ['RED', 'GREEN', 'BLUE'])
print(list(Color))
# [<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 3>]

Слід звернути увагу, що в такому випадку значення членів автоматично встановлюються в послідовні цілі числа, починаючи з 1. Також при потребі можна встановити власні значення:

from enum import Enum

Color = Enum('Color', {'RED': 'R', 'GREEN': 'G', 'BLUE': 'B'})
print(list(Color))
# [<Color.RED: 'R'>, <Color.GREEN: 'G'>, <Color.BLUE: 'B'>]

Наостанок слід додати, що оскільки члени enum є константами, Python забороняє присвоювати їм нові значення під час виконання:

from enum import Enum


class Size(Enum):
    S = 'small'
    M = 'medium'
    L = 'large'


Size.S = 's'
# AttributeError: Cannot reassign members.

Автоматичне встановлення значень членам enum

Модуль enum у Python надає функцію auto(), яка дає змогу автоматично встановлювати значення для членів enum:

from enum import Enum, auto


class Priority(Enum):
    LOW = auto()
    MEDIUM = auto()
    HIGH = auto()


print(list(Priority))
# [<Priority.LOW: 1>, <Priority.MEDIUM: 2>, <Priority.HIGH: 3>]

За замовчуванням, функція auto() присвоює членам enum послідовні цілі числа, в якості значень (починаючи з 1). При потребі змінити поведінку за замовчуванням, слід реалізувати в класі перерахування метод _generate_next_value_(), який використовується auto() для генерації значень:

from enum import Enum, auto


class Priority(Enum):
    def _generate_next_value_(name, start, count, last_values):
        return name[0]

    LOW = auto()
    MEDIUM = auto()
    HIGH = auto()


print(list(Priority))
# [<Priority.LOW: 'L'>, <Priority.MEDIUM: 'M'>, <Priority.HIGH: 'H'>]

Ітерація по enum

Є можливість перебору членів enum за допомогою циклу for:

from enum import Enum


class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3


for color in Color:
    print(color)

"""
Color.RED
Color.GREEN
Color.BLUE
"""

Цикл буде йти по атрибутам у тому порядку, в якому вони вказані при оголошенні класу. Назви та значення ніяк не впливають на порядок.

Порівняння членів enum

Члени enum можна порівнювати за допомогою операторів тотожності (is) та рівності (==):

from enum import Enum


class Status(Enum):
    ACTIVE = 1
    INACTIVE = 2
    PENDING = 3


print(Status.ACTIVE == Status.ACTIVE)  # True
print(Status.ACTIVE == Status.INACTIVE)  # False

print(Status.ACTIVE is Status.ACTIVE)  # True
print(Status.ACTIVE is Status.INACTIVE)  # False

Оператори більше та менше не підтримуються, при спробі порівняння буде піднято виняток TypeError:

print(Status.ACTIVE > Status.INACTIVE)
# TypeError: '>' not supported between instances of 'Status' and 'Status'

В такому випадку слід використовувати клас IntEnum, який підтримує порівняння членів перерахування за їх значеннями:

from enum import IntEnum


class Priority(IntEnum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3


print(Priority.HIGH > Priority.MEDIUM)  # True
print(Priority.MEDIUM >= Priority.MEDIUM)  # True
print(Priority.LOW == 1)  # True
print(Priority.HIGH > 2)  # True

Унікальні значення в enum Python

Члени enum з однаковими значеннями, є різними іменами що вказують на один і той самий об’єкт:

from enum import Enum


class Color(Enum):
    RED = 1
    CRIMSON = 1
    BLUE = 2

У цьому прикладі RED та CRIMSON вказують на один об’єкт:

print(Color.RED == Color.CRIMSON)  # True
print(Color.RED is Color.CRIMSON)  # True

При спробі отримати name для CRIMSON, повертається перше ім’я з оголошення класу перерахування (саме воно вважається істинним):

print(Color.CRIMSON.name)  # RED

Також, оскільки CRIMSON є синонімом для RED, його не буде відображено при спробі перебору enum у циклі:

for color in Color:
    print(color)

# Color.RED
# Color.BLUE

Якщо потрібно щоб усі члени enum мали лише унікальні значення, слід додати декоратор @unique перед оголошенням класу:

import enum


@enum.unique
class Color(enum.Enum):
    RED = 1
    CRIMSON = 1
    BLUE = 2

# ValueError: duplicate values found in <enum 'Color'>: CRIMSON -> RED

В такому випадку, члени з повторюваними значеннями будуть піднімати виняток ValueError під час виконання.

Використання enum в операторах if та match

Оператори if...elif та match...case є поширеними місцями для використання enum. Обидві конструкції дають змогу виконувати різні дії в залежності від певних умов. Приклади:

from enum import Enum


class Status(Enum):
    NEW = 1
    IN_PROGRESS = 2
    DONE = 3


status = Status.IN_PROGRESS

if status is Status.NEW:
    print('Task created')
elif status is Status.IN_PROGRESS:
    print('Task performed')
elif status is Status.DONE:
    print('Task completed')

# Task performed

from enum import Enum


class Status(Enum):
    NEW = 1
    IN_PROGRESS = 2
    DONE = 3


status = Status.IN_PROGRESS

match status:
    case Status.NEW:
        print('Task created')
    case Status.IN_PROGRESS:
        print('Task performed')
    case Status.DONE:
        print('Task completed')

# Task performed

Розширення Enum новою поведінкою

До цього ми працювали з enum за замовчуванням (тобто використовували стандартні можливості та поведінку). При потребі розширити стандартну поведінку, класи enum у Python надають можливість додавати методи з реалізацією необхідної функціональності (так само як це робиться з будь-яким звичайним класом Python). Приклад:

from enum import Enum


class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

    def describe(self):
        return f'This is {self.name.lower()}'


color = Color.RED
print(color.describe())
# This is red

Розглянемо цікавіший приклад з реалізацією шаблону Strategy. Уявімо, що нам потрібен клас, який дозволяє застосовувати одну з двох стратегій сортування списку: за зростанням або спаданням. Реалізуємо це використовуючи enum:

from enum import Enum


class Sort(Enum):
    ASCENDING = 1
    DESCENDING = 2

    def __call__(self, values):
        return sorted(values, reverse=self is Sort.DESCENDING)


numbers = [5, 3, 1, 4, 2]
print(Sort.ASCENDING(numbers))  # [1, 2, 3, 4, 5]
print(Sort.DESCENDING(numbers))  # [5, 4, 3, 2, 1]

У цьому прикладі, кожний член enum є callable (тобто викликається як функція), завдяки реалізації методу __call__ у класі. Також, саме в __call__ зашита логіка сортування. Таким чином, маємо enum котрий визначає дві стратегії сортування: Sort.ASCENDING та Sort.DESCENDING.

Enum з multiple значеннями

Є можливість оголошувати enum, де кожен член має більше ніж одне значення:

from enum import Enum


class Fruit(Enum):
    APPLE = ('Red', 52)
    BANANA = ('Yellow', 89)
    ORANGE = ('Orange', 47)

    def __init__(self, color, calories):
        self.color = color
        self.calories = calories


print(Fruit.APPLE.name)  # APPLE
print(Fruit.APPLE.value)  # ('Red', 52)
print(Fruit.APPLE.color)  # Red
print(Fruit.APPLE.calories)  # 52

У цьому прикладі, значенням кожного члена enum є кортеж з двох елементів, котрі завдяки реалізації методу __init__ зберігаються у відповідні атрибути екземпляра: color та calories.

Інші класи enum

Окрім класу Enum, модуль enum надає декілька додаткових класів, котрі дозволяють створювати перерахування зі специфічною поведінкою. Розглянемо деякі з них.

IntEnum

Успадкування від класу IntEnum дозволяє створювати перерахування, яке також є підкласом int (тобто його члени матимуть також властивості цілих чисел). Приклад:

from enum import IntEnum


class Size(IntEnum):
    S = 1
    M = 2
    L = 3


print(Size.S > Size.M)  # False
print(Size.L >= Size.M)  # True
print(Size.M < 1)  # False

IntFlag

Клас IntFlag можна використовувати в якості базового для перерахувань, члени яких повинні підтримувати побітові операції. Приклад:

from enum import IntFlag


class Role(IntFlag):
    OWNER = 8
    POWER_USER = 4
    USER = 2
    ADMIN = OWNER | POWER_USER | USER


person_roles = Role.USER | Role.POWER_USER

print(person_roles)  # Role.POWER_USER|USER
print(type(person_roles))  # <enum 'Role'>
print(Role.USER in person_roles)  # True
print(Role.OWNER in Role.ADMIN)  # True

Екземпляри IntFlag також підтримують операції над цілими числами:

print(Role.USER + 1)  # 3
print(Role.ADMIN + 5)  # 19
print(Role.ADMIN > Role.USER)  # True

Flag

Ще один клас доступний у модулі enum – Flag, подібний до IntFlag, але має деякі обмеження. Основна відмінність від IntFlag полягає в тому, що Flag не успадковує int, тому операції над цілими числами не підтримуються (при спробі буде отримано помилку TypeError):

from enum import Flag, auto


class Permission(Flag):
    READ = auto()
    WRITE = auto()
    EXECUTE = auto()


permission = Permission.READ | Permission.WRITE
print(permission)  
# Permission.WRITE|READ

print(Permission.READ > Permission.WRITE)  
# TypeError: '>' not supported between instances of 'Permission' and 'Permission'


Мітки: