Давно уже сталкиваюсь с одним неудобством в повседневной работе с джангой.
Например у нас есть моделька:
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
Собственно, да, одним из основных(для меня) преимуществ - контроль правильности значений. Плюс меньше головной боли при рефакторинге.
Оставлен 12 Май 2008 в 07:22 ¶Честно говоря я считаю крайне рискованным уносить любые перечисления/справочники сложнее True/False на уровень кода. Минимальная необходимость работать с базой извне или необходимость добавлять статусы создает весьма дурные проблемы.
То есть, либо я чего-то сильно не догоняю в силу недавнего знакомства с питоном и джангой, либо для данного случая огород вообще городить не стоило. Достаточно было поля is_active: BooleanField(). Для более сложного (с подозрением на необходимость дополнения типов по ходу жизни) стоило создать дополнительную таблицу типов.
Максим.
Александр, мне кажется в приведенной выше реализации слишком много кода ;) Предстовляется возможным написать функцию
__init__()более простой:Слушаюсь, комрад!;)
На самом деле немного не так себя ведет ваш код:
Но начинание правильное:) Чуть-чуть подпилить наверно где-то стоит.
Оставлен 13 Май 2008 в 19:43 ¶Не выгоднее ли использовать шаблон Proxy?
Смысл в том, что делая вызов
Оставлен 15 Май 2008 в 13:24 ¶Entry.types.active, я ожидаю получить значение поля, а не его лейбл. Значением в данном случае выступаетactive.Да особой разницы нет, только чуть-чуть поменять ваш код:
Оставлен 15 Май 2008 в 13:26 ¶А как теперь сюда добавить дефолтовый вариант?
type = models.CharField( max_length = 10, choices = types )Например так:
Оставлен 27 Июнь 2008 в 01:01 ¶Александру спасибо за статью.
Заинтересовало, насколько же велика разница между хранением перечислений в базе числами и строками. Намутил маленький убогий бенчмарк. Сравнение скорости баз данных с численными и строковыми перечислениями
В проекте на работе использовал
class CartEntry(models.Model): class TYPE: unused = 0 user = 1 boss = 2 admin = 3Именно тот "плохой" промежуточный вариант между хорошим енумом и совсем плохим внешним списком. Особенность в том, что мне не нужны choices.
В своём проекте сделаю адаптацию Сашиного подхода с числами.
Есть идея, что нужно как-то утилизировать metaclass, чтобы выбирать имя для создаваемого класса. Я никогда не использовал metaclass в питоне, пинайте ногами.
P.S.: e-mail notifications до сих пор не работает. (Прошло не меньше 3х месяцев с момента как я это заметил)
Оставлен 17 Июль 2008 в 18:25 ¶Да, да, да. Поддерживаю!
Оставлен 17 Июль 2008 в 20:11 ¶Очень понравилось решение.
Но хочется еще лучше :)
Как на счет описания в виде класса и атрибутов? Я использую Pylint и он ругается на неописанные атрибуты. Вот если б было так:
Это уже в стиле моделей Django. И я думаю именно такое описание было бы очень даже кстати даже для транка.
Пока опыта и смелости не хватает такое реализовать...
Какие мысли по поводу?
P.S. не работает справка по markdown http://webnewage.org/pages/markdown/
Оставлен 06 Сентябрь 2008 в 09:15 ¶Поразмыслил, можно еще проще:
class Types(Choices) inactive = (0, _( "inactive" )) active = (1, _( "active" ))
Или для стрингового чойса:
class Types(Choices) inactive = "inactive" active = "active"
:) Теоретически в конструкторе эти атрибуты будут видны - можно построить итератор.
Оставлен 06 Сентябрь 2008 в 13:42 ¶Nazar Leush, да, эта концепция является логическим продолжением моей. Вариантов реализации перечислений в питоне уйма. Каждый выберет по вкусу:-)
Оставлен 06 Сентябрь 2008 в 22:32 ¶Комментирование данного поста закрыто.