В прошлом посте я рассказал, как можно изменять порядок полей в форме (ну и в модели). Там я чуть-чуть затронул описание низкоуровневой реализации механизма выставления по порядку полей (атрибутов), соответствующему тому как эти поля объявлены в классе.
Откуда вообще взялась необходимость в каких-то хитростях для определения этого порядка? Да потому что, после того как класс создан, к его атрибутам можно легко обратиться через __dict__, но как известно обычный питонячий dict - unordered. Т.е. из-за своей реализации в языке, ключи в нем упорядочены не по тому как они в него попадали, а случайно (на самом деле не совсем случайно, но для нас это не интересно). А следовательно информация о порядке объявления атрибутов класса безвозвратно потеряна. Существует распространенный способ обхода этой проблемы, который активно используется в нескольких компонентах джанги - в моделях и формах.
Классический способ
Для заданного класса, который будет классом атрибутов, нуждающихся в упорядочивании (для джанги это models.Field и forms.Field), заводится "статический" счетчик, который, обновляясь при каждом создании объекта данного класса, хранит порядковый номер этого самого объекта. В дальнейшем по этому номеру поля сортируются и заносятся в какой-либо упорядоченный контейнер - например в SortedDict.
Причем не важно количественное значение счетчика. Главное, что поля объявленнные раньше имеют меньший порядковый номер. Поэтому счетчик не обнуляется.
Данный способ применяется на данный момент в самой джанге.
Приведу простую реализацию:
from django.utils.datastructures import SortedDict
class Field(object):
# сам счетчик
creation_counter = 0
def __init__(self, label):
# вот тут манипулируем со статическим счетчиком,
# запоминая свой порядоковый номер
self.creation_counter = self.__class__.creation_counter
self.__class__.creation_counter += 1
self.label = label
def __repr__(self):
return "Field: %s" % self.label
class EntityMetaclass(type):
def __new__(cls, name, bases, attrs):
try:
Entity
except NameError:
pass
else:
# фильтруем поля
fields = [(field_name, f) for field_name, f in attrs.iteritems()\
if isinstance(f, Field)]
# самая магия - сортируем список полей по счетчику
fields.sort(
lambda counter_a, counter_b: cmp(counter_a, counter_b),
lambda pair: pair[1]
)
attrs["fields"] = SortedDict(fields)
return super(EntityMetaclass, cls).__new__(cls, name, bases, attrs)
class Entity(object):
__metaclass__ = EntityMetaclass
Ну и используется это всем до боли знакомым образом:
>>> class ConcreteEntity(Entity):
... field1 = Field("foo")
... field2 = Field("bar")
... field3 = Field("foobar")
>>> ConcreteEntity.fields.items()
[('field1', Field: foo), ('field2', Field: bar), ('field3', Field: foobar)]
Но есть ещё один вариант.
Способ 3000
В свете недавнего выхода питона 3.0, появился другой вариант реализации данной функциональности. Как известно, там обновился механизм объявления метаклассов. И у метаклассов появилась новая возможность - явным образом через метод __prepare__ указывать контейнер, в который будут заноситься атрибуты по мере создания класса (т.е. обработкой интерпретатором определения класса).
В таком случае счетчик уже больше не нужен. Атрибуты можно сразу заносить в SortedDict, где они будут в строгом порядке:
Пример такой реализации:
class Field(object):
def __init__(self, label):
self.label = label
def __repr__(self):
return "Field: %s" % self.label
class EntityMetaclass(type):
@classmethod
def __prepare__(metacls, name, bases):
return SortedDict()
def __new__(cls, name, bases, classdict):
"""
classdict - объект SortedDict, созданный в методе __prepare__
"""
try:
Entity
except NameError:
pass
else:
# тут всё равно надо отфильтровать поля, но уже не надо принудительно сортировать
fields = SortedDict([(field_name, f) for field_name, f in classsdict.iteritems()\
if isinstance(f, Field)])
attrs["fields"] = fields
return super(EntityMetaclass, cls).__new__(cls, name, bases, dict(classsdict))
class Entity(metaclass=EntityMetaclass):
pass
Кстати, в сlassdict попадают все атрибуты, включая методы - ещё один небольшой бонус.
Доживем до того момента, что джанга заработает на 3.0, мы ещё не скоро, но уже понятно, что в некоторых местах она может стать еще более лаконичной и стройной.
Если в следующий раз задумаетесь над созданием собственного DSL, не забывайте про эту полезную идиому:-)
Комментирование данного поста закрыто.