The Magic of Django Model

The Magic of Django Model

Model is frequently used in django applications, we basically use model like this:

from django.db import models

class Blog(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField(max_length=1000)

then, we can use it like normal python object,

# blog is an instance of Blog
blog = Blog(title='title', content='content')
title = blog.title
content = blog.content

seems nothing special right? Actually, django uses some magic to make things easier for us. Let's dig it out!

The Metaclass

Most magic of django model hide in the model metaclass - ModelBase

class ModelBase(type):
    """
    Metaclass for all models.
    """
    def __new__(cls, name, bases, attrs):
        ...
        new_class = super_new(cls, name, bases, {'__module__': module})
        ...
        new_class.add_to_class('_meta', Options(meta, **kwargs))
        ...
        # Add all attributes to the class.
        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)

If you are familiar with python, you may know that metaclass is the class that create class and the __new__ method defines how to create a new class.

Most model class in django is created using ModelBase metaclass. Actually, the __new__ method here is a bit long, but I select some important pieces.

add_to_class method

First of all, we need to understand what does the method add_to_class do. The add_to_class method take three arguments:

  • cls - the class object
  • name - the name of the attribute
  • value - the attribute value need to be set

What it basically does is to assign a new attribute with name name and value value to class cls. But the assignment mechanism is a bit different when the value object itself has attribute contribute_to_class. In such case, the assignment will be delegated to the value object's contribute_to_class method, which then add the value back to the class!

Important _meta Attribute

The _meta attribute plays an important role in model class, it's created using:

new_class.add_to_class('_meta', Options(meta, **kwargs))

We can see that django use the add_to_class method to add an attribute called _meta to the new created model class. Moreover, the _meta attribute is just an instance of Option class.

So we need to step down to dig out what Options class is, interesting!

The Options class resides in django.db.models.options module. It looks like:

class Options(object):
    def __init__(self, meta, app_label=None):
        self.local_fields = []
        self.local_many_to_many = []
        self.virtual_fields = []
        self.model_name = None
        self.verbose_name = None
        self.verbose_name_plural = None
        ...

    def contribute_to_class(self, cls, name):
        from django.db import connection
        from django.db.backends.utils import truncate_name

        cls._meta = self
        self.model = cls
        # First, construct the default values for these options.
        self.object_name = cls.__name__
        self.model_name = self.object_name.lower()
        self.verbose_name = camel_case_to_spaces(self.object_name)
        ...

    def add_field(self, field):
        # Insert the given field in the order in which it was created, using
        # the "creation_counter" attribute of the field.
        # Move many-to-many related fields from self.fields into
        # self.many_to_many.
        if field.rel and isinstance(field.rel, ManyToManyRel):
            self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field)
            if hasattr(self, '_m2m_cache'):
                del self._m2m_cache
        else:
        ...

We see a familiar name contribute_to_class, which the add_to_class method will delegate to. The most important part of this method is to assign itself to class:

cls._meta = self

In summary, we can treat the _meta attribute as a dictionary, it contains a lot of information of the model class such as model, model_name, verbose_name, fields declared in model, etc. You will find that the _meta attribute is used a lot in other django modules such as model forms.

Fields ?

You may think the title attribute get from model like blog.title is just the title field declared in model title = models.CharField(max_length=255). But it is totally wrong. All fields declared in model class will be collected in the _meta attribute, and attributes with field name of model object (like the title attribute of blog object) are just python objects assigned during instance initialization!

fields in _meta

Fields declared in model class will be collected and assigned to the _meta attribute when creating the model class.

class ModelBase(type):
    """
    Metaclass for all models.
    """
    def __new__(cls, name, bases, attrs):
        ...
        # Add all attributes to the class.
        for obj_name, obj in attrs.items():
            new_class.add_to_class(obj_name, obj)

Still in the ModelBase metaclass, all fields are contained in the attrs dictionary. Similar to the assignment of _meta attribute, it assigns all fields to the new created class using add_to_class method.

You may remember the contribute_to_class case of the assigned obj, here, the fields. Does field class has this method? The answer is YES!

class Field(RegisterLookupMixin):
    """Base class for all field types"""

    def contribute_to_class(self, cls, name, virtual_only=False):
        self.set_attributes_from_name(name)
        self.model = cls
        if virtual_only:
            cls._meta.add_virtual_field(self)
        else:
            cls._meta.add_field(self)
        if self.choices:
            setattr(cls, 'get_%s_display' % self.name,
                    curry(cls._get_FIELD_display, field=self))

Bingo! The contribute_to_class method eventually call the class _meta attribute's add_virtual_field or add_field...

In my opinion, I doubt whether such delegation is a good design. Different classes should not couple too much, but here, the Field class assumes the _meta attribute of model class, which is not reasonable.

Never mind, lets move to the add_field method in _meta attribute.

class Options(object):
    def add_field(self, field):
        # Insert the given field in the order in which it was created, using
        # the "creation_counter" attribute of the field.
        # Move many-to-many related fields from self.fields into
        # self.many_to_many.
        if field.rel and isinstance(field.rel, ManyToManyRel):
            self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field)
            if hasattr(self, '_m2m_cache'):
                del self._m2m_cache
        else:
            self.local_fields.insert(bisect(self.local_fields, field), field)
            self.setup_pk(field)
        ...

Nothing special, it just add field to its local_fields or local_many_to_many attributes. At the end of the class creation, all the fields declared in model class are collected and stored in _meta attributes for later use.

What's next

We have a brief introduction about the creation process of model class above. The fields declared in model are stored in _meta attribute. When we need to get model object from database or create model object ourself and then save it to database, django will use these fields to setup a bridge between database and python, which is funny to dive in.

Current rating: 4.7

Comments

ADDRESS

  • Email: jimmykobe1171@126.com
  • Website: www.catharinegeek.com