Djangoを読む

はじめに

前回飛ばした部分、モデルをインポートする際に何が行われているのか、を見ていきます。チュートリアルでは以下のようにmodels.pyが定義されていました。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 
 
 
 
 
 
 
 
 
 
 
from django.db import models
 
class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
 
 
class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

django/db/models

ともかく、django.db.modelsを見てみましょう。__init__.py、の必要な部分のみ抜き出しです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
 
 
 
 
 
 
 
from django.db.models.fields import *  # NOQA
 
from django.db.models.base import DEFERRED, Model  # NOQA isort:skip
from django.db.models.fields.related import (  # NOQA isort:skip
    ForeignKey, ForeignObject, OneToOneField, ManyToManyField,
    ManyToOneRel, ManyToManyRel, OneToOneRel,
)

このことから、

  • Modelはbaseモジュール
  • ForeignKeyはfields.relatedモジュール
  • CharFieldなどはfieldsモジュールに書かれているだろう

ということがわかります。

django/db/models/base.py

まずはModelクラスの定義から見てみます。というものの今回注目するのは先頭部分のみ

Everything is expanded.Everything is shortened.
  1
 
class Model(six.with_metaclass(ModelBase)):

Python2でも3でも実行できるようにsixが使われていますが、メタクラスを使用し処理が行われているようです。

ModelBase

というわけでModelBaseの__new__メソッドを見ます。200行以上あるので少しずつ読み進めていきます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 
 
 
-
|
!
 
 
 
-
!
 
    def __new__(cls, name, bases, attrs):
        super_new = super(ModelBase, cls).__new__
 
        # Also ensure initialization is only performed for subclasses of Model
        # (excluding Model class itself).
        parents = [b for b in bases if isinstance(b, ModelBase)]
        if not parents:
            return super_new(cls, name, bases, attrs)
 
        # Create the class.
        module = attrs.pop('__module__')
        new_class = super_new(cls, name, bases, {'__module__': module})

先頭部分。クラスを作成しています。ただし、渡されたattrsをそのまま使うのではなくモジュールパスだけ渡しています。 次にメタデータの処理が行われていますが指定してないのでさくっと無視。ここで出ているMetaはメタクラスや次に出てくる_metaとはまた別物というところだけ注意(ややこしい)

で、続き。

Everything is expanded.Everything is shortened.
  1
 
        new_class.add_to_class('_meta', Options(meta, app_label))

前回も出てきた_metaの正体はOptionsインスタンスのようです。このクラスはoptions.pyで定義されています。

add_to_classメソッド。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
-
!
 
 
 
    def add_to_class(cls, name, value):
        # We should call the contribute_to_class method only if it's bound
        if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'):
            value.contribute_to_class(cls, name)
        else:
            setattr(cls, name, value)

というわけで、クラスへの追加を行う際にcontribute_to_classというメソッドが呼び出されるようです。

Optionsクラスのcontribute_to_classでは名前の設定とかMataクラスから情報拾ってきての設定とかが行われています。特に重要なところだけ抜き出すと、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 
-
!
 
 
 
-
!
-
!
 
 
    def contribute_to_class(self, cls, name):
        # 省略
 
        self.object_name = cls.__name__
        self.model_name = self.object_name.lower()
 
        # 省略
 
        # If the db_table wasn't provided, use the app_label + model_name.
        if not self.db_table:
            self.db_table = "%s_%s" % (self.app_label, self.model_name)
            self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())

ということでアプリ名とモデル名(の小文字)からDBのテーブル名が作成されています。

ModelBaseの__new__に戻って、次の例外とかを登録している箇所は省略。 その次、

Everything is expanded.Everything is shortened.
  1
  2
  3
-
!
 
        # Add all attributes to the class.
        for obj_name, obj in attrs.items():
            new_class.add_to_class(obj_name, obj)

クラス定義中に書かれている変数やメソッドはattrs(第4引数)として__new__に渡されます。ここではそれらを先ほどと同様add_to_classで追加しています。追加されるものとは、

Everything is expanded.Everything is shortened.
  1
  2
  3
 
 
 
class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

みたいなフィールドオブジェクトですね。こっちに行くと長くなりそうなので先に__new__の続きを見てしまいましょう。 と言っても、後書いてあるのはプロキシとか継承とかに関する処理なのでこれらもさくっと無視して最後。

Everything is expanded.Everything is shortened.
  1
  2
  3
 
 
 
        new_class._prepare()
        new_class._meta.apps.register_model(new_class._meta.app_label, new_class)
        return new_class

appsに登録が行われていますね。これにより前回謎だった「いつの間にかモデルが登録されている」の個所がわかりました。

django/db/models/fields

それではフィールドクラスです。予想通り、CharFieldもDateTimeFieldもFieldというクラスのサブクラスで大体の処理はFieldクラスで定義されています。そのcontribute_to_classメソッド(一部省略)

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
-
|
|
|
|
|
!
 
 
 
 
 
 
 
-
|
|
!
 
    def contribute_to_class(self, cls, name, private_only=False, virtual_only=NOT_PROVIDED):
        """
        Register the field with the model class it belongs to.
 
        If private_only is True, a separate instance of this field will be
        created for every subclass of cls, even if cls is not an abstract
        model.
        """
        self.set_attributes_from_name(name)
        self.model = cls
        if private_only:
            cls._meta.add_field(self, private=True)
        else:
            cls._meta.add_field(self)
        if self.column:
            # Don't override classmethods with the descriptor. This means that
            # if you have a classmethod and a field with the same name, then
            # such fields can't be deferred (we don't have a check for this).
            if not getattr(cls, self.attname, None):
                setattr(cls, self.attname, DeferredAttribute(self.attname, cls))

やってることは2つ

  • _meta(Optionsオブジェクト)にフィールド登録
  • モデルクラスにDeferredAttribute登録(columnとかattnameとかありますが普通はnameと同じです)

add_fieldメソッド(一部省略)

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 
-
|
|
|
!
 
 
 
 
 
    def add_field(self, field, private=False, virtual=NOT_PROVIDED):
        # 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 private:
            self.private_fields.append(field)
        elif field.is_relation and field.many_to_many:
            self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field)
        else:
            self.local_fields.insert(bisect(self.local_fields, field), field)

というわけでlocal_fieldsに追加されます。bisectは配列中のどの位置に要素を挿入すればいいかを返す標準モジュールの関数です。フィールドオブジェクトは定義されている順にcreation_counterが割り振られており、それに基づいて挿入位置を決めています(Fieldクラスに__lt__メソッドが定義されています)。なんで単純にappendしないのかについては後で説明します。

django/db/models/fields/related.py

関連についても確認しましょう。まずは関連を設定しているところの復習。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

ForeignKeyはrelatedモジュールに記述されています。まず継承関係を確認しましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
-
|
|
|
|
|
!
 
 
-
|
!
 
 
-
|
!
class ForeignKey(ForeignObject):
    """
    Provide a many-to-one relation by adding a column to the local model
    to hold the remote value.
 
    By default ForeignKey will target the pk of the remote model but this
    behavior can be changed by using the ``to_field`` argument.
    """
 
class ForeignObject(RelatedField):
    """
    Abstraction of the ForeignKey relation, supports multi-column relations.
    """
 
class RelatedField(Field):
    """
    Base class that all relational fields inherit from.
    """

めんどくさい(笑)。ともかく関連について確認するにはこれらのクラスを調べる必要があります。

ForeignKeyの__init__を見てみましょう。とりあえずon_deleteは無視な方向で。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 
 
 
 
 
 
-
!
-
|
|
!
 
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
    def __init__(self, to, on_delete=None, related_name=None, related_query_name=None,
                 limit_choices_to=None, parent_link=False, to_field=None,
                 db_constraint=True, **kwargs):
        try:
            to._meta.model_name
        except AttributeError:
            # 省略
        else:
            # For backwards compatibility purposes, we need to *try* and set
            # the to_field during FK construction. It won't be guaranteed to
            # be correct until contribute_to_class is called. Refs #12190.
            to_field = to_field or (to._meta.pk and to._meta.pk.name)
 
        # on_deleteに関する処理
 
        kwargs['rel'] = self.rel_class(
            self, to, to_field,
            related_name=related_name,
            related_query_name=related_query_name,
            limit_choices_to=limit_choices_to,
            parent_link=parent_link,
            on_delete=on_delete,
        )
 
        kwargs['db_index'] = kwargs.get('db_index', True)
 
        super(ForeignKey, self).__init__(
            to, on_delete, from_fields=['self'], to_fields=[to_field], **kwargs)
 
        self.db_constraint = db_constraint

to_fieldは指定してないのでtoのモデル(Question)のpkが取得され設定されます。まだ見てませんがpkはidになります。

次に、rel_classのインスタンスを作ってキーワード引数のrelに設定、スーパークラスの__init__を呼び出しています。rel_classは__init__メソッドのすぐ上に書かれていますがManyToOneRelクラスです。

ManyToOneRelクラスはreverse_relatedモジュールに書かれています。必要なところだけ抜き出し

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
class ManyToOneRel(ForeignObjectRel):
    def __init__(self, field, to, field_name, related_name=None, related_query_name=None,
                 limit_choices_to=None, parent_link=False, on_delete=None):
        super(ManyToOneRel, self).__init__(
            field, to,
            related_name=related_name,
            related_query_name=related_query_name,
            limit_choices_to=limit_choices_to,
            parent_link=parent_link,
            on_delete=on_delete,
        )
 
        self.field_name = field_name
 
class ForeignObjectRel(object):
    def __init__(self, field, to, related_name=None, related_query_name=None,
                 limit_choices_to=None, parent_link=False, on_delete=None):
        self.field = field
        self.model = to
        self.related_name = related_name
        self.related_query_name = related_query_name
        self.limit_choices_to = {} if limit_choices_to is None else limit_choices_to
        self.parent_link = parent_link
        self.on_delete = on_delete
 
        self.symmetrical = False
        self.multiple = True

というわけでmodelとしてtoで渡されたクラスが指定されています。その後、ForeignObject→Fieldと追いかけていくと(RelatedFieldには__init__はありません)、remote_fieldとしてManyToOneクラスのインスタンスが設定され、field.remote_field.modelのような形で参照先のモデル情報が取得できることになります。

_prepareでの処理

さて、モデルがインポートされたときにappsに登録、フィールドもインスタンス化したときに自己登録している様子を見てきました。モデルの自己登録処理(ModelBaseの__new__)では登録前に_prepareが呼び出されています。最後にそれについて見てみましょう。

ModelBase._prepare

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
-
|
!
 
 
 
-
!
 
 
 
-
!
 
 
 
 
 
 
    def _prepare(cls):
        """
        Creates some methods once self._meta has been populated.
        """
        opts = cls._meta
        opts._prepare(cls)
 
        # 省略
 
        if not opts.managers or cls._requires_legacy_default_manager():
            if any(f.name == 'objects' for f in opts.fields):
                raise ValueError(
                    "Model %s must specify a custom Manager, because it has a "
                    "field named 'objects'." % cls.__name__
                )
            manager = Manager()
            manager.auto_created = True
            cls.add_to_class('objects', manager)
 
        signals.class_prepared.send(sender=cls)

objectsを登録しています。これにより、Question.objects.get(pk=1)のように検索が行えることになります。 managerの作りもまたややこしいですけど、そこを見るのはまた後で。

Options._prepare

_meta、Optionsクラスの方の_prepareを見てみましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 
-
!
 
 
-
!
 
 
    def _prepare(self, model):
        # 省略
 
        if self.pk is None:
            if self.parents:
                # 省略
            else:
                auto = AutoField(verbose_name='ID', primary_key=True, auto_created=True)
                model.add_to_class('id', auto)

というわけでidフィールドが設定されています。

ところで、モデルのdocstringを見てみると以下のようにidが先頭に来ています。

>>> print(Question.__doc__)
Question(id, question_text, pub_date)

add_fieldの際に、appendではなくinsertを使い、さらに挿入位置をbisectで決めていました。これは、idが先頭に来るようにするためです。該当部分だけピックアップすると、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
class Field(RegisterLookupMixin):
    creation_counter = 0
    auto_creation_counter = -1
 
    def __init__(self, verbose_name=None, name=None, primary_key=False,
                 max_length=None, unique=False, blank=False, null=False,
                 db_index=False, rel=None, default=NOT_PROVIDED, editable=True,
                 serialize=True, unique_for_date=None, unique_for_month=None,
                 unique_for_year=None, choices=None, help_text='', db_column=None,
                 db_tablespace=None, auto_created=False, validators=[],
                 error_messages=None):
 
        if auto_created:
            self.creation_counter = Field.auto_creation_counter
            Field.auto_creation_counter -= 1
        else:
            self.creation_counter = Field.creation_counter
            Field.creation_counter += 1

というわけで、auto_createdの場合はマイナスの値が割り振られ、先頭に並ぶというからくりになっています。

おわりに

今回はモデルインポート時の処理、モデルの自己登録とメタ情報の構築、必要な属性の補完について見てきました。メタクラスというと身構えてしまいますが、Pythonとしてのメタクラスの利用は先頭だけで残りは淡々とDjangoの世界(メタじゃないプログラミング)でメタ情報を設定していっているという印象でした。


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-06-13 (火) 10:25:07 (2736d)