Принуждение к порядку

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

Откуда вообще взялась необходимость в каких-то хитростях для определения этого порядка? Да потому что, после того как класс создан, к его атрибутам можно легко обратиться через __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, не забывайте про эту полезную идиому:-)