Перечисления (enum). Класс Enum

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Смотреть материал на YouTube | RuTube

Стандартная библиотека языка Python содержит класс Enum, с помощью которого можно создавать перечисления. Что это такое? Часто в программах требуется определять различные константы. Например, для набора цветов:

red = (127, 0, 0) # красный
green = (127, 127, 255) # зеленый
blue = (0, 255, 0) # синий

Конечно, прямо так их и можно объявить. Но было бы лучше сгруппировать и определить в рамках одного какого-либо класса. Первое что приходит в голову – это воспользоваться обычным классом и определить цвета в виде набора атрибутов следующим образом:

class Color:
    red = (127, 0, 0)  # красный
    green = (127, 127, 255)  # зеленый
    blue = (0, 255, 0)  # синий

А, затем, обращаться к любой переменной по синтаксису:

cl = Color.red

и даже менять их значения:

Color.green = 0

Однако часто полагается, что переменные с константными значениями не должны менять своего значения на всем протяжении работы программы. Чтобы обеспечить этот и некоторый дополнительный функционал, класс Color достаточно унаследовать от класса Enum, который позволит превратить его в перечисление, то есть, в класс с набором констант:

from enum import Enum
 
 
class Color(Enum):
    red = (127, 0, 0)  # красный
    green = (127, 127, 255)  # зеленый
    blue = (0, 255, 0)  # синий

Теперь при попытке изменить значение какого-либо атрибута:

Color.green = 0

получим сообщение об ошибке типа AttributeError. Мало того, сами атрибуты red, green, blue стали объектами класса Color:

type(Color.red) # <enum 'Color'>

И у этих атрибутов появились два свойства:

Color.blue.value # (0, 255, 0) 
Color.blue.name # 'blue'

Кроме того, мы теперь можем перебрать все элементы перечисления Color с помощью цикла for следующим образом:

for p in Color:
    print(f"{p.name} = {p.value}")

Атрибуты отображаются в том же порядке, в котором были прописаны в классе Color:

red = (127, 0, 0)
green = (127, 127, 255)
blue = (0, 255, 0)

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

class Color(Enum):
    red = (127, 0, 0)  # красный
    green = (127, 127, 255)  # зеленый
    blue = (0, 255, 0)  # синий
 
    gray = 'gray'
    white = 255
    black = [0, 0, 0]

То есть, это могут быть и строки, и числа, и кортежи, и списки и вообще любые типы данных. Но, чаще всего используют неизменяемые типы, так как, например, изменяемый список формально можно модифицировать после объявления класса Color:

Color.black.value[0] = 1

Для перечислений такая операция выглядит несколько нестандартно.

Если же атрибуту black присвоить значение 255, которое уже используется для атрибута white, то black будет восприниматься, как псевдоним (алиас) атрибута white:

class Color(Enum):
    red = (127, 0, 0)  # красный
    green = (127, 127, 255)  # зеленый
    blue = (0, 255, 0)  # синий
 
    gray = 'gray'
    white = 255
    black = 255

И по этой причине не будет выводиться при переборе в цикле for:

for p in Color:
    print(f"{p.name} = {p.value}")

Увидим:

red = (127, 0, 0)
green = (127, 127, 255)
blue = (0, 255, 0)
gray = gray
white = 255

Однако мы можем напрямую указать имя ‘black’ для доступа к соответствующему атрибуту:

p = Color.black # <Color.white: 255>

или по ключу 'black':

p = Color['black'] # <Color.white: 255>

В обоих случаях переменная p будет являться объектом класса Color, содержащим значение 255, но иметь имя 'white'.

Если же мы не хотим, чтобы перечисление содержало дублирующие значения (псевдонимы), то можно воспользоваться декоратором unique из модуля enum:

from enum import Enum, unique
 
 
@unique
class Color(Enum):
    red = (127, 0, 0)  # красный
    green = (127, 127, 255)  # зеленый
    blue = (0, 255, 0)  # синий
 
    gray = 'gray'
    white = 255
    black = 255

В этом случае возникнет ошибка, т.к. присутствует псевдоним black. Но, если прописать у этого атрибута другое значение, например, 0:

    black = 0

то программа выполнится без ошибок.

Пример использования перечисления

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

class Figure(Enum):
    RECT = 0
    TRIANGLE = 1
    CIRCLE = 2

А функция будет иметь вид:

def fig_per(*args, fig:Figure=Figure.RECT):
    if fig == Figure.RECT:
        return 2 * (args[0] + args[1]) # периметр прямоугольника
    elif fig == Figure.TRIANGLE:
        return args[0] + args[1] + args[2] # периметр треугольника
    elif fig == Figure.CIRCLE:
        return 2 * 3.1415 * args[0] # длина круга
 
    return -1 # для неизвестной фигуры

По умолчанию она вычисляет периметр прямоугольника, т.к. именованный параметр fig принимает значение Figure.RECT. Соответственно, ожидается, что в функцию fig_per будут переданы два первых позиционных аргумента с высотой и длиной прямоугольника. Если же параметр fig равен Figure.TRIANGLE, то берутся первые три позиционных аргумента. Наконец, для Figure.CIRCLE достаточно указать только радиус окружности в первом аргументе.

Вызывать эту функцию можно следующим образом:

per1 = fig_per(10, 20)
per2 = fig_per(1, 2, 3, fig=Figure.TRIANGLE)
per3 = fig_per(7, fig=Figure.CIRCLE)

Видите, как удобно вместо числовых значений 0, 1, 2 использовать имена RECT, TRIANGLE, CIRCLE. Любой сторонний программист, читая этот код, сразу поймет, о чем здесь идет речь и к чему относятся эти константы. В этом основное назначение перечислений – повышение читаемости текста программы.

Если же, при вызове функции fig_per в параметре fig указать числовое значение, например, 0, что соответствует прямоугольнику:

per = fig_per(10, 20, fig=0)

то получим значение -1, а не 60. Почему так произошло? Для этого нужно детально разобраться, как работают операции сравнения с атрибутами перечисления.

Операции сравнения с атрибутами перечисления

Чтобы все лучше понять, давайте вернемся к ранее объявленному перечислению Color с набором атрибутов:

class Color(Enum):
    red = (127, 0, 0)  # красный
    green = (127, 127, 255)  # зеленый
    blue = (0, 255, 0)  # синий
    …

И сформируем еще одно аналогичное перечисление с именем ColorRGB:

class ColorRGB(Enum):
    red = (127, 0, 0)
    green = (127, 127, 255)
    blue = (0, 255, 0)

В переменных p1, p2 сохраним значение атрибута red из этих классов:

p1 = Color.red
p2 = ColorRGB.red

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

p1 == p2 # False
p1 != p2 # True
p1 == Color.red # True
p1 == ColorRGB.red # False
p2 == ColorRGB.red # True
p1 == (127, 0, 0) # False

То есть, значение True возникает только при равенстве объекта p1 или p2 соответствующему объекту из строго определенного класса. Даже если прописать конкретное числовое значение, то операция сравнения на равенство все равно возвращает False. Именно поэтому, при указании в функции fig_per значения fig=0, получаем значение периметра -1.

Конечно, конкретные значения на равенство можно проверять так:

p1.value == (127, 0, 0)

но, обычно, для перечислений не обращаются к конкретным величинам в операциях сравнения.

Методы внутри класса перечисления

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

class Figure(Enum):
    RECT = 0
    TRIANGLE = 1
    CIRCLE = 2
 
    def describe(self):
        if self == Figure.RECT:
            return 'Rectangle'
        elif self == Figure.TRIANGLE:
            return 'Triangle'
        elif self == Figure.CIRCLE:
            return 'Circle'
 
        return 'Unknown'

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

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

p = Figure.TRIANGLE
p.describe() # 'Triangle'

Метод describe() в данном случае вернет строку 'Triangle', т.к. переменная p ссылается на объект класса Figure со значением Figure.TRIANGLE. Соответственно, параметр self – это ссылка на текущий объект, для которого вызывается метод describe(). Через этот параметр определяем строку, которую следует вернуть.

На следующем занятии мы продолжим эту тему и рассмотрим специальные типы перечислений IntEnum и StrEnum.

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Видео по теме