Ещё одна реализация 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, с третьим не проверялась.
    Нужны ли альтернативные реализации перечислений для Питона?

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

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 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?
        • +2
          В Python 2.7 мне хватает возможностей модуля flufl.enum. Собственно, его реализация и легла в основу PEP435.
          • 0
            Тогда согласен: удобно теперь будет иметь такой функционал в стандартном комплекте.
            Осталось мне теперь по-тихоньку переносить свой код в третью ветку.
            Вам это удается или ещё не пробовали?
          • +1
            Мне для Enum'а понравилось одно из решений на StackOverflow:
            class Colors:
                class Red: pass
                class Green: pass
                class Blue: pass
            
            • НЛО прилетело и опубликовало эту надпись здесь

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