Чуть-чуть «извращений» над моделями django

    Лень двигатель прогресса


    Иногда, создавая модели в django, я себя начинаю чувствовать мартышкой. Постоянно создаю атрибут enable, который принимает по умолчанию то значение True, то False. Меняю менеджер objects на свой простой EnableManager. А хочется иметь механизм, который эти монотонные операции делал за меня. Раз хочется, то можно сделать.


    Представляем, что мы хотим получить


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

    Следом я подумал про функцию, которая будет создавать мне нужные классы. Это избавит от огромного количества повторяющегося кода, и в целом выглядит не плохо. «А что если, где-то не стоит использовать, скажем, твой менеджер, и это единственный случай?» — крутилось у меня в голове. Ради эксклюзивного случая создавать класс не очень хочется. Если использовать декоратор, то и этого можно избежать.

    Итого: нам нужно создать функцию, которая при определенных параметрах:
    • Возвращает абстрактный класс
    • Декорирует класс, добавляя в него нужное поле и нужный менеджер


    Везде есть подводные камни


    В данном случае, это было декорирования класса. Дело в том, что в декораторе, используя '@foo' мы можем передать только, над которым будет совершенно действие. Но как сделать так, что бы мы могли передать параметры декоратору?
    Дело в том, что синтаксическая конструкция '@' используется как композиция двух функций(g•f(x)). В python'e и функция и класс является обьектом, и мы можем вернуть функцию синтаксической конструкции '@', т.е. примерно так (l(y))•(f(x)), где l(y) — возвращает функцию. Код декоратора, будет примерно таким:

    def foo(cls=None, param="DefaultValue"):
        def decorator(cls):
            # do something with class
            return cls
        if cls is None:
            return decorator
        else:
            return decorator(cls)
    


    Что то похожее, только с функциями, используется в django для декоратора login_required.

    Теперь можно и покодить


    Вроде все подводные камни разобраны, теперь можно приступить к написанию кода:

    def enable(cls=None, status=True, set_manager=True, mixin=False):
        '''
        Adds enable field into cls or return mixin
        :param cls: class that would updates
        :param status: default value for enable field
        :type status: bool
        :param set_manager: sets if EnableManager is required
        :type set_manager: bool
        :param mixin: sets should be returned model mixin
        :type mixin: bool
        '''
        def decorator(cls):
            '''
            Adds field and manager if manager is required
            '''
            cls.add_to_class('enabled',
                             models.BooleanField(_('Enabled'), default=status))
            if set_manager:
                cls.add_to_class('objects', EnableManager())
            return cls
    
        class Class(models.Model):
            '''
            Enable AbstractModels
            '''
            enbaled = models.BooleanField(_('Enabled'), default=status)
    
            class Meta:
                abstract = True
    
        if cls and mixin:
            raise DecoratorMixinException
        elif mixin:
            if set_manager:
                Class.add_to_class('objects', EnableManager())
            return Class
        elif cls:
            return decorator(cls)
        else:
            return decorator
    


    DecoratorMixinException — это исключение, которое говорит о том что функция не может быть вызвана как декоратор с параметром mixin = True.

    Для добавления менеджеров используется метод add_to_class(), особенность django-моделей, если писать в классе objects = YourManager() или cls.objects = YourManager(), то работать не будет. Через этот же метод мы и добавляем нашей модели необходимое поле.

    Примеры использования


    EnableFalseMixin = enable(status=false, mixin=True)
    
    class SimpleModel(EnableFalseMixin,  models.Model):
        '''
        Simple model
        '''
        # some field here
    
    @enable
    class TestFalseEnable(models.Model):
        '''
        Test enable
        '''
        # some fields here
    
    @enable(status=false)
    class TestFalseEnable(models.Model):
        '''
        Test enable
        '''
        # some fields here
    


    Данная статья задумывалась, как пример создания удобного механизма, которым можно пользоваться повседневно.
    Надеюсь кому-нибудь она будет полезна. Приятной вам разработки на фреймворке django.

    P.S.
    Кажется, некоторые внимательные люди будут ругаться, что имя функции «Enable» написано с большой буквы, что не соответствует PEP8. Я это сделал, потому что данная функция порождает новый класс. Пожалуйста, мастера python'а и django, скажите на сколько правильно это сделано? Встречал такое в некоторых проектах, но никогда не задумывался о том на сколько это правильно.
    Share post

    Similar posts

    Comments 12

      +10
      Мне кажется, имеет смысл ставить вопрос об уместности решения в принципе.

      Что делает это штука? Она привязывает к классу модели декоратор, который создает поле enable со значением по умолчанию, распространяющимся на все объекты модели. Тут я придерусь вовсе не к оформлению синтаксиса по PEP а к тому, что член класса не стоит называть как действие, также как и метод класс не надо называть как объект данных. a = zzz.enabled — это ок. zzz.enable(), a = zzz.is_enabled() — тоже. Наоборот — некорректно. У тела есть атрибут нога и есть метод ходить, но не наоборот.

      По сути это конструктор класса, но база данных не будет знать что конструктор поменялся, а основная задача ОРМ — обеспечивать репрезентацию данных в виде объектов и наоборот.

      Как это решение поведет себя при использовании системы миграций схемы или данных? Как будет работать при интроспекции моделей? Как оно будет вести себя в queryset-ах? Пока в голове нет точного ответа на эти вопросы, такие решения — мина замедленного действия.

      Джанговский ОРМ это большой сложный продукт с которым напрямую связывается много 3-rd party и который имеет много сложных нюансов в поведении. Чем дальше я с ним работаю, тем реже у меня возникает желание хоть как-то вмешиваться в его работу, переносить туда логику, использовать наследование или атипичные члены и так в нем присутствует немало сюрпризов.

      И я вообще не вижу проблемы. То есть действительно сложно создать несколько или несколько десятков классов с одинаковым полем и разным значением по умолчанию? Код должен быть понятным и предсказуемым в поведении, и только после этого можно говорить о DRY и прочих необязательных вещах, а также попытках выстрелить себе в левую пятку через правое плечо. (путем объединения mixin и декораторов в одну структуру, например)

      Декораторы для вьюшек вроде login_required уместны, так как вьюшка — это метод, прямая палка с определенным входом и выходом, в перехвате которых нет ничего сложного или непонятного.

      К классу модели в принципе можно цеплять декораторы но с методами, которые полезут в данные и что-то на основании их ответят, или что-то расскажут о самом классе, ну например геттер is_enabled() который проверит поле enable и выдаст его значение для конкретного объекта. Хотя смысла в этом маловато.
        0
        Про название атрибута абсолютно с вами согласен. Дальше текст пытаюсь осмыслить. Простите, сегодня мало спал может из-за этого проблемы в понимании вашей мысли. Но в любом случае за критику спасибо.
          0
          Поправил название атрибута.
          0
          Вычленил 4 мысли, поправьте если чего недопонял:
          • Атрибут не надо называть глаголом
            +1. Я бы еще не стал называть параметр, у которого назначение — принимать значение по-умолчанию «status». Без чтения коментариев не понять. Другое дело если назвать «default» (не зря же дальше параметр передается именно с таким именем).
          • Создатели джанго — из особенной касты — им можно использовать метаклассы и прочие нетривиальные конструкции. А самим лучше туда не соваться. На худой конец чуть-чуть можно, если ты разработчик «3-rd party».
            Утрировал конечно, но все же: чем пользователь фреймворка хуже?
          • Чувствую возможные сайд-эффекты. В чем именно проблема не наю, но чувствую что добром не кончится.
            Ну можно конечно всю жизнь себя ограничивать в использовании конструкций сложнее @login_required, но зачем? :)
            Проверить что новая моедль ведет себя нормально на самом деле не очень трудно.
          • Что сложно N-цать раз прописать одинаковые атрибуты и менеджеры?
            Если из 200 моделей в проекте найдется парочка вот таких — да, лучше дописать явно.
            А вот если из 200 моделей в 150 встречается добавление этого enabled, и что еще хуже для каждого queryset дописывается filter(enabled=True) — извините, но это уже плохой код. И попытка его улучшить не взирая сложность джанговского ORM-а — по-моему весьма похвальна.
            0
            > Создатели джанго — из особенной касты — им можно использовать метаклассы и прочие нетривиальные конструкции. А самим лучше туда не соваться. На худой конец чуть-чуть можно, если ты разработчик «3-rd party».
            > Утрировал конечно, но все же: чем пользователь фреймворка хуже?

            Метаклассы в принципе ок. Это штатный инструмент, правда с ними связана одна неприятная особенность — они не поддерживаются south, обещают поправить в неопределенном будущем, когда south будет включен в джанговский пекадж, но работу без него представить сложно.

            Но не надо путать метаклассы с подобными конструкциями. Разработчики 3rd-party как раз будут рассчитывать на то, что ваши модели используются достаточно стандартно.

            Сейчас быстро попробовал это решение на тестовом проекте с заглушкой вместо кастом-менеджера — в качестве декоратора ведет себя достаточно адекватно и интроспекция отрабатывает адекватно. Так что автору плюсов в карму, на практике декораторы оказались хорошей заменой абстрактным классам.

            Попытка использования подобного паттерна в качестве примеси — откровенный фейл, фабрика нежизнеспособна и честно говоря даже неинтересно разбираться как сделать ее таковой, сущность которая пытается вести себя и как класс и как функция — это нездоровый паттерн.

            Я бы вообще выкинул вариант с поведением в качестве mixin, так как практика использования декораторов для класса модели сама по себе интересна. В крайнем случае нет особой сложности написать дополнительно нормальный абстрактный класс для таких целей.

            Фиксы:
            enbaled >> enabled
            false (в примерах) >> False
            0
            база будет знать про новое поле, при интроспекции тоже всё будет ок.
            но я бы такое засунул в __metaclass__
            +1
            Я правильно понимаю логику порождения объекта (как-то уж слишком много условий у Вас)?
            if cls and mixin:
                    raise DecoratorMixinException
            
            if mixin:
                    return Class
            elif cls:
                    decorator(cls)
            else:
                    decorator
            

            P.S.
            Раз уж Вы спросили. Каноническое именование декоратов классов, как и фабрики mixin-ов — с маленькой буквы.

            Ну и пару комментарев бы поправил:
            :param cls: class that would updates — мне кажется тут нужен пассивный залог
            :param set_manager: sets is EnableManager is required — опечатка, первый «is» -> «if»

              0
              Вы всё правильно понимаете. Писал не выспавшимся и в состоянии эйфории. Огромное спасибо за замечания. Вечером поправлю.
                0
                *Писал не выспавшимся и в состоянии эйфории, поэтом не думал над оптимизацией кода, хотя, признаю, так писать ни в каком состоянии нельзя.
                0
                Поправил. спасибо вам большое.
                0
                У модели можно задать __metaclass__ вкотором добавить поля и т.п.
                  0
                  Погодите, а почему нельзя было просто написать модель, от которой унаследоваться позже?

                  Only users with full accounts can post comments. Log in, please.