программист серверов и логики
0,2
рейтинг
3 января 2014 в 15:51

Разработка → Ещё одна реализация Enums для Python

В прошлом году сообщество Python наконец-то договорилось о реализации перечислений. Было разработано соответствующее предложение PEP 435, его реализация уже есть в python 3.4.

Наблюдая за горячими спорами, я решил в качестве эксперимента сделать свой велосипед, добавив в него несколько фич, появление которых в официальной реализации было маловероятно.

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

В большинстве случаев, когда мы описываем отношение вида <имя, значение>, у нас имеется много информации, которую желательно привязать к имени: вспомогательный текст для пользовательского интерфейса, связи с родственными перечислениями, ссылки на другие объекты или функции. Приходится городить дополнительные структуры данных, что не есть хорошо — лишние сущности как-никак.

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

Заодно добавил:

  • наследование;
  • несколько вспомогательных методов и проверок;
  • построение индексов по всем столбцам таблицы;
  • формирование обратных ссылок в связанных друг с другом отношениях;

В итоге получилось вот такая вещь (примеры решил не дробить, чтобы не увеличивать и так длинное «полотно»):

########################
# Базовое использование
########################

from rels import Column, Relation

# Enum и EnumWithText уже объявлены в библиотеке
# и доступны как rels.Enum и rels.EnumWithText
# тут их объявления приведены для упрощения понимания

class Enum(Relation):             # объявляем абстраткное перечисление
    name = Column(primary=True)   # столбец с именами
    value = Column(external=True) # столбец со значениями


# наследование — добавляем дополнительный столбец для какого-нибудь текста
#                например, для использования в пользовательском интерфейсе
class EnumWithText(Enum):
    text = Column()


class SOME_CONSTANTS(Enum):       # объявляем конкретное перечисление
    records = ( ('NAME_1', 1),    # и указываем данные для него
                ('NAME_2', 2))


class SOME_CONSTANTS_WITH_TEXT(EnumWithText): # ещё одно конкретное перечисление
    records = ( ('NAME_1', 1, 'constant 1'),
                ('NAME_2', 2, 'constant 2'))


# Работаем с перечислениями

# доступ к данным
SOME_CONSTANTS.NAME_1.name == 'NAME_1'          # True
SOME_CONSTANTS.NAME_1.value == 1                # True

# получение элемента перечисления из «сырых» данных
SOME_CONSTANTS(1) == SOME_CONSTANTS.NAME_1      # True

# сравнения
SOME_CONSTANTS.NAME_2 == SOME_CONSTANTS.NAME_2  # True
SOME_CONSTANTS.NAME_2 != SOME_CONSTANTS.NAME_1  # True

# теперь для проверок не надо всюду тягать импорты перечисления
SOME_CONSTANTS.NAME_2.is_NAME_1                 # False
SOME_CONSTANTS.NAME_2.is_NAME_2                 # True

# каждый элемент перечисления — отдельный объект,
# поэтому даже объекты с одинаковыми данными равны не будут
SOME_CONSTANTS.NAME_2 != SOME_CONSTANTS_WITH_TEXT.NAME_2  # True
SOME_CONSTANTS.NAME_1 != SOME_CONSTANTS_WITH_TEXT.NAME_1  # True

# наследование — добавляем новые элементы
class EXTENDED_CONSTANTS(SOME_CONSTANTS_WITH_TEXT):  # расширяем набор данных в перечислении
    records = ( ('NAME_3', 3, 'constant 3'), )       # добавляем ещё одно значение


########################
# Индексы
########################

class ENUM(Relation):
    name = Column(primary=True)   # для этого столбца имя индекса будет .index_name
    value = Column(external=True) # для этого столбца имя индекса будет .index_value
    text = Column(unique=False, index_name='by_key') # указываем своё имя для индекса

    records = ( ('NAME_1', 0, 'key_1'),
                ('NAME_2', 1, 'key_2'),
                ('NAME_3', 2, 'key_2'), )

# если данные в столбце уникальны, значением в словаре будет элемент перечисления
ENUM.index_name # {'NAME_1': ENUM.NAME_1, 'NAME_2': ENUM.NAME_2,  'NAME_3': ENUM.NAME_3}

# если данные в столбце не уникальны, значением в словаре будет список элементов перечисления
ENUM.by_key     # {'key_1': [ENUM.NAME_1], 'key_2': [ENUM.NAME_2, ENUM.NAME_3]}


########################
# Обратные ссылки
########################

# объявляем отношение, на которое будем ссылаться
class DESTINATION_ENUM(Relation):
    name = Column(primary=True)
    val = Column()

    records = ( ('STATE_1', 'value_1'),
                ('STATE_2', 'value_2') )

# объявляем отношение, которое будет ссылаться
class SOURCE_ENUM(Relation):
    name = Column(primary=True)
    val = Column()
    rel = Column(related_name='rel_source')

    records = ( ('STATE_1', 'value_1', DESTINATION_ENUM.STATE_1),
                ('STATE_2', 'value_2', DESTINATION_ENUM.STATE_2) )

# проверяем работу ссылок
DESTINATION_ENUM.STATE_1.rel_source == SOURCE_ENUM.STATE_1 # True
DESTINATION_ENUM.STATE_2 == SOURCE_ENUM.STATE_2.rel        # True

Отдельно замечу, что не обязательно перечисления объявлять в коде. Во многих случаях, удобнее ограничиться объявлением модели данных, а сами данные грузить из сторонних источников, например, электронных таблиц.

Репозиторий и подробная документация на github

P.S. библиотека разрабатывалась в расчёте на Python 2.7, с третьим не проверялась.
Нужны ли альтернативные реализации перечислений для Питона?

Проголосовало 215 человек. Воздержалось 75 человек.

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

Елецкий Алексей @Tiendil
карма
53,0
рейтинг 0,2
программист серверов и логики
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (17)

  • +4
    Рискну получить минусов и спрошу, что же такое перечисления, для чего они нужны в Пайтоне в том или ином виде, каково их практическое применение и насколько они затратны по производительности?
    • +2
      Интересует чисто пользовательский ответ на эти вопросы, ибо PEP435 я читал, ну а с необходимостью использования такой структуры данных пока не сталкивался. Ну и словари, конечно же, использую для подобных нужд (если я верно сравниваю).
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Разумно, согласен с Вами. Даже теперь есть идеи, где бы я мог использовать их в своем коде.
      • +3
        Перечисление в классическом виде — это просто набор констант, принадлежащих к одному и тому же типу. В Python не принято проверять тип данных, поэтому перечисления были введены, как мне кажется, просто как удобная и привычная абстракция, знакомая многим. По большому счёту в Python легко можно обойтись без использования перечислений, но их присутствие в стандартной библиотеке с версии 3.4 точно не будет лишним. Кому это нужно, тот и будет использовать.

        Мне удобно использовать перечисления в Python и инкапсулировать в них какие-то связанные данные. Ну, вот, например, как-то так:

        from collections import namedtuple
        from flufl import enum
        
        # Данные, связанные с перечислением WorldSide
        _ = namedtuple('WorldSideInfo', ['angle', 'mode'])
        
        class WorldSide(enum.Enum):
            """Класс перечисления определяет стороны света"""
        
            North = _(90, (0, 0, 0))
            NorthEast = _(45, (2, 1, 1))
            East = _(0, (0, 0, 0))
            SouthEast = _(-45, (2, 1, 1))
            South = _(-90, (0, 0, 0))
            SouthWest = _(-135, (2, 1, 1))
            West = _(180, (0, 0, 0))
            NorthWest = _(135, (2, 1, 1))
        
        
        # Используем
        ws_angle = WorldSide.NorthWest.value.angle
        
        
        • +2
          Благодарю Вас за хороший пример. Действительно, качественное применение этого нововведения не будет лишним. А ещё, Вы натолкнули меня на интересные мысли по применению перечислений. В моем коде по работе со свойствами объектов-изображений с одинаковыми числовыми типами значений они могут очень пригодиться для описания свойств этих самых объектов.

          Выходит, мое сравнение перечислений со словарями ошибочно?
    • +1
      википедия: Перечисляемый тип

      Производительность зависит от конкретной реализации. В случае Rels никаких особых затыков нет, чёрной магии под капотом не делается.
      • +1
        То есть, потребление памяти минимальное?
        • 0
          Да.

          Естественно, память линейно зависит от количества элементов перечисления и количества столбцов в таблице.

          В случае работы с перечислениями, скорее критична не память (т.к. их достаточно мало создаётся, по сравнению с другими объектами), а вычислительная сложность выполнения базовых операций с ними (получение значений, сравнения и т.п.). В этом плане всё тоже хорошо.
          • 0
            В таком случае, благодарю Вас за новый инструмент в моей работе с любимым языком :)
            • 0
              Спасибо, надеюсь он окажется полезным.
              Если будут замечания или предложения по развитию библиотеки — пишите.
              • 0
                Кстати, вот я не спец в такого рода делах, однако, эта структура, что называется, thread-safe?
                • 0
                  Да.
  • +2
    В Python 2.7 мне хватает возможностей модуля flufl.enum. Собственно, его реализация и легла в основу PEP435.
    • 0
      Тогда согласен: удобно теперь будет иметь такой функционал в стандартном комплекте.
      Осталось мне теперь по-тихоньку переносить свой код в третью ветку.
      Вам это удается или ещё не пробовали?
  • +1
    Мне для Enum'а понравилось одно из решений на StackOverflow:
    class Colors:
        class Red: pass
        class Green: pass
        class Blue: pass
    
    • НЛО прилетело и опубликовало эту надпись здесь

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