Препарируем Django: Модели

Препарируем Django: Модели

Владимир Филонов (Labbler)

Все мы любим Django за ее магию и батарейки. Но любой инструмент, даже магический, лучше использовать понимая как он устроен. В этом докладе я попробую раскрыть некоторые секреты волшебства django-моделей.

53b0434aded1fb944ec3037c382158c1?s=128

Moscow Python Meetup

January 25, 2013
Tweet

Transcript

  1. Препарируем Django: Модели Владимир Филонов

  2. Модели - простые классы? Препарируем Django: Модели На вид вполне

    обычный класс class RegularClass(object): field = models.CharField(max_length=150) class ModelClass(models.Model): field = models.CharField(max_length=150)
  3. Модели - простые классы? Препарируем Django: Модели Сравним поведение экземпляров

    >>> print ModelClass().field '' >>> print RegularClass().field <django.db.models.fields.CharField object at 0xb59a32c>
  4. Модели - простые классы? Препарируем Django: Модели А теперь сравним

    сами классы >>> print RegularClass.field <django.db.models.fields.CharField object at 0xb59a32c> >>> print ModelClass.field Traceback (most recent call last): File "<console>", line 1, in <module> AttributeError: type object 'ModelClass' has no attribute 'field'
  5. Модели - простые классы? Препарируем Django: Модели Куда-то пропало наше

    поле. Значит не так все просто?
  6. class Model(object): __metaclass__ = ModelBase ... Модели - простые классы?

    Препарируем Django: Модели Посмотрим, что же там внутри
  7. class Model(object): __metaclass__ = ModelBase ... Модели - простые классы?

    Препарируем Django: Модели Посмотрим, что же там внутри
  8. Метаклассы - как это работает? Препарируем Django: Модели Метакласс -

    это класс, который формирует другой класс в момент интерпретации кода. class Base(type): def __new__(cls, name, bases, attrs): new_cls = super(Base, cls).__new__(cls, name, bases, attrs) setattr(new_cls, 'HACKED', "!") return new_cls class Parent(object): __metaclass__ = Base class Main(Parent): data = "child"
  9. Метаклассы - как это работает? Препарируем Django: Модели Метакласс -

    это класс, который формирует другой класс в момент интерпретации кода. class Base(type): def __new__(cls, name, bases, attrs): new_cls = super(Base, cls).__new__(cls, name, bases, attrs) setattr(new_cls, 'HACKED', "!") return new_cls class Parent(object): __metaclass__ = Base class Main(Parent): data = "child" Сам метакласс
  10. Метаклассы - как это работает? Препарируем Django: Модели Метакласс -

    это класс, который формирует другой класс в момент интерпретации кода. class Base(type): def __new__(cls, name, bases, attrs): new_cls = super(Base, cls).__new__(cls, name, bases, attrs) setattr(new_cls, 'HACKED', "!") return new_cls class Parent(object): __metaclass__ = Base class Main(Parent): data = "child" Имя класса, который будет сгенерирован "Parent" "Main"
  11. Метаклассы - как это работает? Препарируем Django: Модели Метакласс -

    это класс, который формирует другой класс в момент интерпретации кода. class Base(type): def __new__(cls, name, bases, attrs): new_cls = super(Base, cls).__new__(cls, name, bases, attrs) setattr(new_cls, 'HACKED', "!") return new_cls class Parent(object): __metaclass__ = Base class Main(Parent): data = "child" Список классов-родителей [object] [Parent]
  12. Метаклассы - как это работает? Препарируем Django: Модели Метакласс -

    это класс, который формирует другой класс в момент интерпретации кода. class Base(type): def __new__(cls, name, bases, attrs): new_cls = super(Base, cls).__new__(cls, name, bases, attrs) setattr(new_cls, 'HACKED', "!") return new_cls class Parent(object): __metaclass__ = Base class Main(Parent): data = "child" Словарь аттрибутов будущего класса
  13. Метаклассы - как это работает? Препарируем Django: Модели Метакласс -

    это класс, который формирует другой класс в момент интерпретации кода. class Base(type): def __new__(cls, name, bases, attrs): new_cls = super(Base, cls).__new__(cls, name, bases, attrs) setattr(new_cls, 'HACKED', "!") return new_cls class Parent(object): __metaclass__ = Base class Main(Parent): data = "child" Словарь аттрибутов будущего класса Parent { '__metaclass__': < class '__main__.Base' > , '__module__': '__main__' }
  14. Метаклассы - как это работает? Препарируем Django: Модели Метакласс -

    это класс, который формирует другой класс в момент интерпретации кода. class Base(type): def __new__(cls, name, bases, attrs): new_cls = super(Base, cls).__new__(cls, name, bases, attrs) setattr(new_cls, 'HACKED', "!") return new_cls class Parent(object): __metaclass__ = Base class Main(Parent): data = "child" Словарь аттрибутов будущего класса Main { 'data': 'child', '__module__': '__main__' }
  15. Метаклассы - как это работает? Препарируем Django: Модели Метакласс -

    это класс, который формирует другой класс в момент интерпретации кода. class Base(type): def __new__(cls, name, bases, attrs): new_cls = super(Base, cls).__new__(cls, name, bases, attrs) setattr(new_cls, 'HACKED', "!") return new_cls class Parent(object): __metaclass__ = Base class Main(Parent): data = "child"
  16. Метаклассы - как это работает? Препарируем Django: Модели Что получилось?

    >>> print dir(Parent) ['HACKED', '__class__', '__delattr__', '__dict__', '__doc__', ...] >>> print dir(Main) ['HACKED', '__class__', '__delattr__', '__dict__', '__doc__', ..., 'data']
  17. Метаклассы - как это работает? Препарируем Django: Модели Забавно, но

    какой практический смысл? • Модификация класса на ходу • Все это происходит при интерпретации кода
  18. Метаклассы - как это работает? Препарируем Django: Модели Допустим что

    нам надо посчитать количество вызовов всех методов. Сделаем вот такой декоратор. def calls_counter(name, func): counter_attr_name = "_{0}_calls_counter".format(name) def counter(instance, *args, **kwargs): setattr(instance, counter_attr_name, getattr(instance, counter_attr_name, 0) + 1) return func(instance, *args, **kwargs) return counter
  19. Метаклассы - как это работает? Препарируем Django: Модели Как же

    нам обернуть сотни методов в десятке классов? class MethodCounterBase(type): def __new__(cls, name, bases, attrs): new_cls = super(MethodCounterBase, cls).__new__(cls, name, bases, attrs) for obj_name, obj in attrs.items(): if callable(obj) and not isinstance(obj, type): setattr(new_cls, obj_name, calls_counter(obj_name, obj)) return new_cls
  20. Метаклассы - как это работает? Препарируем Django: Модели Создадим класс

    с метаклассом MethodCounterBase class SomeClass(object): __metaclass__ = MethodCounterBase def method1(self): pass def method2(self): pass
  21. Метаклассы - как это работает? Препарируем Django: Модели Ну и

    проверим результат >>> p = SomeClass() >>> p.method1() >>> print p._method1_calls_counter 1 >>> p.method1() >>> print p._method1_calls_counter 2 >>> p.method2() >>> print p._method2_calls_counter 1
  22. Что именно происходит в ModelBase Препарируем Django: Модели Сначала формируется

    пустой класс (без атрибутов) super_new = super(ModelBase, cls).__new__ ... new_class = super_new(cls, name, bases, {'__module__': module}) Получаем класс Meta из декларации модели или унаследованный attr_meta = attrs.pop('Meta', None) abstract = getattr(attr_meta, 'abstract', False) if not attr_meta: meta = getattr(new_class, 'Meta', None) else: meta = attr_meta
  23. Что именно происходит в ModelBase Препарируем Django: Модели На основе

    вычисленного meta в модель добавляется атрибут _meta new_class.add_to_class('_meta', Options(meta, **kwargs)) Потом будет еще несколько проверок на наследование свойств, от родительских моделей.
  24. Что именно происходит в ModelBase Препарируем Django: Модели Еще одна

    проверка, связанная с наследованием if getattr(new_class, '_default_manager', None): if not is_proxy: new_class._default_manager = None new_class._base_manager = None else: new_class._default_manager = new_class._default_manager._copy_to_model(new_class) new_class._base_manager = new_class._base_manager._copy_to_model(new_class)
  25. Что именно происходит в ModelBase Препарируем Django: Модели Теперь классу

    присваиваются все атрибуты, указанные в коде модели for obj_name, obj in attrs.items(): new_class.add_to_class(obj_name, obj) def add_to_class(cls, name, value): if hasattr(value, 'contribute_to_class'): value.contribute_to_class(cls, name) else: setattr(cls, name, value)
  26. Что именно происходит в ModelBase Препарируем Django: Модели Вот тут

    происходит магия с полями моделей #Базовая имплементация contribute_to_class в models.fields.Field def contribute_to_class(self, cls, name): self.set_attributes_from_name(name) self.model = cls cls._meta.add_field(self) if self.choices: setattr(cls, 'get_%s_display' % self.name, curry(cls._get_FIELD_display, field=self))
  27. Что именно происходит в ModelBase Препарируем Django: Модели Вот тут

    происходит магия с полями моделей #Базовая имплементация contribute_to_class в models.fields.Field def contribute_to_class(self, cls, name): self.set_attributes_from_name(name) self.model = cls cls._meta.add_field(self) if self.choices: setattr(cls, 'get_%s_display' % self.name, curry(cls._get_FIELD_display, field=self)) Поля добавляются не в класс модели, а во вложенный класс Options и доступны через cls._meta.fields
  28. Что именно происходит в ModelBase Препарируем Django: Модели Последний шаг

    - если модель не абстрактная, она регистрируется в пуле моделей и возвращается уже из этого пула new_class._prepare() register_models(new_class._meta.app_label, new_class) return get_model(new_class._meta.app_label, name, seed_cache=False, only_installed=False) Абстрактная же, нигде не регистрируется if abstract: attr_meta.abstract = False new_class.Meta = attr_meta return new_class
  29. Понятно где поля, а где значения? Препарируем Django: Модели Но

    так и не понятно главное - как же возвращаются и устанавливается значения полей - строки, числа и связанные модели Значения индивидуальны для каждого экземпляра модели (объекта) Значит, присвоение происходит где-то при работе с экземпляром Заглянем в метод __init__, класса Model
  30. Как создается экземпляр модели Препарируем Django: Модели Заглянем в метод

    __init__, класса Model def __init__(self, *args, **kwargs): fields_iter = iter(self._meta.fields) ... if not kwargs: for val, field in izip(args, fields_iter): setattr(self, field.attname, val) else: for val, field in izip(args, fields_iter): setattr(self, field.attname, val) kwargs.pop(field.name, None) if isinstance(field.rel, ManyToOneRel): kwargs.pop(field.attname, None)
  31. Как создается экземпляр модели Препарируем Django: Модели Заглянем в метод

    __init__, класса Model def __init__(self, *args, **kwargs): fields_iter = iter(self._meta.fields) ... if not kwargs: for val, field in izip(args, fields_iter): setattr(self, field.attname, val) else: for val, field in izip(args, fields_iter): setattr(self, field.attname, val) kwargs.pop(field.name, None) if isinstance(field.rel, ManyToOneRel): kwargs.pop(field.attname, None) MyModel(field1=1, field2="text") Этот код ищет в self._meta.fields поля field1 и field2, и устанавливает экземпляру аттрибуты self.field1 = 1 self.field2 = "text"
  32. Как создается экземпляр модели Препарируем Django: Модели Второй проход по

    списку полей ... for field in fields_iter: ... val = field.get_default() ... if is_related_object: setattr(self, field.name, rel_obj) else: setattr(self, field.attname, val)
  33. Как создается экземпляр модели Препарируем Django: Модели obj = MyModel.objects.get(id=1)

    1. Строится SQL-запрос 2. Получаются и обрабатываются данные из БД 3. Вызывается MyModel(field_1=DATA_FROM_DB_1, field_2=DATA_FROM_DB_2, ..., field_N=field1=DATA_FROM_DB_N)
  34. Что мы пропустили Препарируем Django: Модели Что мы пропустили •

    Как решаются переопределения и наследования значений Meta • Как распределяются простые и related поля при создании класса модели • Как вообще обрабатываются related-поля • Как создаются менеджеры для моделей
  35. Где копаться дальше Препарируем Django: Модели Немного ссылок на исходники

    https://github.com/django/django/blob/stable/1.4.x/django/db/models/base.py#L28 https://github.com/django/django/blob/stable/1.4.x/django/db/models/base.py#L274 ModelBase Model https://github.com/django/django/blob/stable/1.4.x/django/db/models/options.py#L22 Options (Meta) http://docs.python.org/2/reference/datamodel.html#customizing-class-creation Customizing class creation
  36. Где копаться дальше Препарируем Django: Модели PS: В python 3.x

    изменился синтаксис указания метаклассов __metaclass__ = Meta -> class X(metaclass=Meta)
  37. It’s all about music business ВОПОСЫ? – vladimir@labbler.com FACEBOOK -

    facebook.com/pyhoster Спасибо!