Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

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

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

Moscow Python Meetup
PRO

January 25, 2013
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. Модели - простые классы?
    Препарируем Django:
    Модели
    Сравним поведение экземпляров
    >>> print ModelClass().field
    ''
    >>> print RegularClass().field

    View Slide

  4. Модели - простые классы?
    Препарируем Django:
    Модели
    А теперь сравним сами классы
    >>> print RegularClass.field

    >>> print ModelClass.field
    Traceback (most recent call last):
    File "", line 1, in
    AttributeError: type object 'ModelClass' has no attribute 'field'

    View Slide

  5. Модели - простые классы?
    Препарируем Django:
    Модели
    Куда-то пропало наше поле. Значит не так все просто?

    View Slide

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

    View Slide

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

    View Slide

  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"

    View Slide

  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"
    Сам метакласс

    View Slide

  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"

    View Slide

  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]

    View Slide

  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"
    Словарь аттрибутов будущего класса

    View Slide

  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__'
    }

    View Slide

  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__'
    }

    View Slide

  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"

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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)

    View Slide

  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)

    View Slide

  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))

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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)

    View Slide

  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"

    View Slide

  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)

    View Slide

  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)

    View Slide

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

    View Slide

  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

    View Slide

  36. Где копаться дальше
    Препарируем Django:
    Модели
    PS: В python 3.x изменился синтаксис указания метаклассов
    __metaclass__ = Meta -> class X(metaclass=Meta)

    View Slide

  37. It’s all about
    music business
    ВОПОСЫ? – [email protected]
    FACEBOOK - facebook.com/pyhoster
    Спасибо!

    View Slide