Александр Кошелев
Александр Кошелев Python-разработчик

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

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

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

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’ов в питоне можно найти например тут и тут. Но они не адаптированы для джанги, хотя более полно реализуют концепцию перечислений.

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

comments powered by Disqus