Перечисления на службе добра

Давно уже сталкиваюсь с одним неудобством в повседневной работе с джангой.

Например у нас есть моделька:

class Entry( models.Model ):
    title = models.CharField( max_length = 150 )
    type = <...>

И каким полем выразить тип (да, знаю, что имя конфликтует со встроенным, но тут это не принципиально)? "Ха!" - скажут некоторые. Да просто взять IntegerField и сделать типы целыми числами от 0 до сколько надо. Легко!

TYPES = ( ( 0, _( "inactive" ) ),
          ( 1, _( "active" ) ) )

class Entry( models.Model ):
    title = models.CharField( max_length = 150 )
    type = models.PositiveIntegerField( choices = TYPES )

Казалось бы, проблема решена. И этим можно пользоваться. Но, тут сразу начинаются неудобства.

Для того чтобы код не превращался в склад "магических чисел" надо добавить соответствующие константы, которые описывают эти два типа.

INACTIVE = 0
ACTIVE   = 1

# или даже
INACTIVE, ACTIVE = range( 0, 2 )

потом можно пойти ещё дальше и выразить choices через них, чтобы было меньше дублирования

TYPES = ( ( INACTIVE, _( "inactive" ) ),
          ( ACTIVE, _( "active" ) ) )

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

e = Entry.objects.get( name = "Спартак чемпион!", type = INACTIVE ) 
e.type = ACTIVE
e.save()

Но, это не удобно! Всё равно от повторений избавиться не удалось. Да и красоты мало. Конечно константы как и choices можно объявить в scope класса, чтобы было хоть какое-то логическая связь, но всё равно - так не интересно.

Потом, что будет если в просмотрите raw базу? Увидите там кучу строк, где в поле "тип" какие-то не понятные числа(ну для вас понятные, если вы этот код сравнительно недавно писали, а если давно?) значения. Что за ними скрывается?

Так, думаем дальше. новая идея приходит так же быстро как и первая - надо использовать строковые литералы для обозначения типа и сменить тип поля в модели на CharField. И пускай это чуть-чуть менее эффективно с точки зрения объема информации.

INACTIVE = "inactive"
ACTIVE   = "active"

Зато мы решаем в миг проблему просмотра базы "из вне". Теперь поля получили более осмысленные значения. Но, не решили проблему "много букв".

Увы в питоне нет такой полезной фичи, как перечисления - enums. Которые есть например в С++.

Но если их нет, то почему бы нам их не сделать. Да, можно найти уже готовые, но мы пойдем своим путем! Будем писать сами и специально для нашего случая(использования для джанго моделей), пусть и немного упрощенно.

За несколько минут вот такой класс получился:

class Enum( object ):
    def __init__( self, **kwargs ):
        self.attrs = kwargs
        for key, value in kwargs.iteritems():
            setattr( self, key, key )

    def __iter__(self):
        return self.attrs.iteritems()

Теперь с его использованием, перепишем исходную модель:

class Entry( models.Model ):
    # сразу занесем в scope
    # создаем enum, в результате имена параметров станут значениями в базе,
    # а значения самих параметров станут ярлыками(labels) для этих значений
    types = Enum( inactive = _( "inactive" ),
                  active   = _( "active" ) )

    title = models.CharField( max_length = 150 )
    type = models.CharField( max_length = 10, choices = types )

И пример использования:

e = Entry.objects.get( name = "Спартак чемпион!", type = Entry.types.inactive ) 
e.type = Entry.types.active
e.save()

Код стал логичнее и прозрачнее.

Что делать, если у нас уже legacy код со значениями в виде чисел? Не проблема - создадим небольшую модификацию исходного класса энума:

class CustomEnum( Enum ):
    def __init__(self, **kwargs ):
        self.attrs = dict( kwargs.itervalues() )

        for key, pair in kwargs.iteritems():
            setattr( self, key, pair[ 0 ] )

И соответственно изменим его создание

types = Enum( inactive = ( 0, _( "inactive" ) ),
              active   = ( 1, _( "active" ) ) )

Я этой идиомой уже некоторое время пользуюсь и пока очень доволен. Альтернативные реализации enum'ов в питоне можно найти например тут и тут. Но они не адаптированы для джанги, хотя более полно реализуют концепцию перечислений.

Надеюсь, вам понравилось:)

Комментарии 14

  1. Александр Кошелев написал:

    Собственно, да, одним из основных(для меня) преимуществ - контроль правильности значений. Плюс меньше головной боли при рефакторинге.

    Оставлен 12 Май 2008 в 07:22
  2. Максим написал:

    Честно говоря я считаю крайне рискованным уносить любые перечисления/справочники сложнее True/False на уровень кода. Минимальная необходимость работать с базой извне или необходимость добавлять статусы создает весьма дурные проблемы.

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

    Максим.

    Оставлен 12 Май 2008 в 07:26
  3. Евгений Сизиков написал:

    Александр, мне кажется в приведенной выше реализации слишком много кода ;) Предстовляется возможным написать функцию __init__() более простой:

    
           class Enum(object):
    
        def __init__(self, **kwargs):
            self.__dict__.update(kwargs)
    
        def __iter__(self):
            return self.__dict__.iteritems()
          
    
    Оставлен 13 Май 2008 в 17:01
  4. Александр Кошелев написал:

    Отличная идея! А теперь потрудись добавить это как сниппет и в django-dev как вещь, которую вполне можно добавить в trunk. Желательно с более красивом примером с тремя-четырьмя значениями, а не с двумя.

    Слушаюсь, комрад!;)

    Александр, мне кажется в приведенной выше реализации слишком много кода ;) Предстовляется возможным написать функцию init() более простой:

    На самом деле немного не так себя ведет ваш код:

    > my_enum = MyEnum( foo = "foo name", bar = "bar name" ) #мой
    > enum = Enum( foo = "foo name", bar = "bar name" ) #ваш
    
    > my_enum.foo
    'foo'
    
    > enum.foo
    'foo name'
    

    Но начинание правильное:) Чуть-чуть подпилить наверно где-то стоит.

    Оставлен 13 Май 2008 в 19:43
  5. anonymous написал:

    Не выгоднее ли использовать шаблон Proxy?

    
           class Enum(object):
        def __init__(self, **kwargs):
            self._attrs = kwargs
    
        def __getattr__(self, name):
            if name not in self._attrs:
                raise AttributeError(name)
            return self._attrs[name]
          
    
    Оставлен 14 Май 2008 в 16:48
  6. Александр Кошелев написал:

    А можно узнать в чем сакральный смысл присваивания ключу ключа, а не его значения?

    setattr( self, key, key )

    Смысл в том, что делая вызов Entry.types.active, я ожидаю получить значение поля, а не его лейбл. Значением в данном случае выступает active.

    Оставлен 15 Май 2008 в 13:24
  7. Александр Кошелев написал:

    Не выгоднее ли использовать шаблон Proxy?

    Да особой разницы нет, только чуть-чуть поменять ваш код:

    def __getattr__(self, name):
        if name not in self._attrs:
            raise AttributeError(name)
        return name
    
    Оставлен 15 Май 2008 в 13:26
  8. udaw написал:

    А как теперь сюда добавить дефолтовый вариант?

    type = models.CharField( max_length = 10, choices = types )

    Оставлен 26 Июнь 2008 в 18:19
  9. Александр Кошелев написал:

    Например так:

    type = models.CharField( max_length = 10, choices = types, default = types.active )
    
    Оставлен 27 Июнь 2008 в 01:01
  10. Sergey Shepelev написал:

    Александру спасибо за статью.

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

    В проекте на работе использовал class CartEntry(models.Model): class TYPE: unused = 0 user = 1 boss = 2 admin = 3

    entry_type = models.IntegerField(default=TYPE.unused)
    

    Именно тот "плохой" промежуточный вариант между хорошим енумом и совсем плохим внешним списком. Особенность в том, что мне не нужны choices.

    В своём проекте сделаю адаптацию Сашиного подхода с числами.

    Есть идея, что нужно как-то утилизировать metaclass, чтобы выбирать имя для создаваемого класса. Я никогда не использовал metaclass в питоне, пинайте ногами.

    P.S.: e-mail notifications до сих пор не работает. (Прошло не меньше 3х месяцев с момента как я это заметил)

    Оставлен 17 Июль 2008 в 18:25
  11. Александр Кошелев написал:

    P.S.: e-mail notifications до сих пор не работает. (Прошло не меньше 3х месяцев с момента как я это заметил)

    Да, да, да. Поддерживаю!

    Оставлен 17 Июль 2008 в 20:11
  12. Nazar Leush написал:

    Очень понравилось решение.

    Но хочется еще лучше :)

    Как на счет описания в виде класса и атрибутов? Я использую Pylint и он ругается на неописанные атрибуты. Вот если б было так:

    class Types(Choices)
      inactive = ChoiceItem(0, _( "inactive" ))
      active = ChoiceItem(1, _( "active" ))
    

    Это уже в стиле моделей Django. И я думаю именно такое описание было бы очень даже кстати даже для транка.

    Пока опыта и смелости не хватает такое реализовать...

    Какие мысли по поводу?

    P.S. не работает справка по markdown http://webnewage.org/pages/markdown/

    Оставлен 06 Сентябрь 2008 в 09:15
  13. Nazar Leush написал:

    Поразмыслил, можно еще проще:

    class Types(Choices) inactive = (0, _( "inactive" )) active = (1, _( "active" ))

    Или для стрингового чойса:

    class Types(Choices) inactive = "inactive" active = "active"

    :) Теоретически в конструкторе эти атрибуты будут видны - можно построить итератор.

    Оставлен 06 Сентябрь 2008 в 13:42
  14. Александр Кошелев написал:

    Nazar Leush, да, эта концепция является логическим продолжением моей. Вариантов реализации перечислений в питоне уйма. Каждый выберет по вкусу:-)

    Оставлен 06 Сентябрь 2008 в 22:32