[[Djangoを読む]] #contents *はじめに [#j8694acf] 前回飛ばした部分、モデルをインポートする際に何が行われているのか、を見ていきます。チュートリアルでは以下のようにmodels.pyが定義されていました。 #code(Python){{ 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 [#xfb3fcb5] ともかく、django.db.modelsを見てみましょう。__init__.py、の必要な部分のみ抜き出しです。 #code(Python){{ 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 [#cdb008c3] まずはModelクラスの定義から見てみます。というものの今回注目するのは先頭部分のみ #code(Python){{ class Model(six.with_metaclass(ModelBase)): }} Python2でも3でも実行できるようにsixが使われていますが、メタクラスを使用し処理が行われているようです。 **ModelBase [#p0bdff75] というわけでModelBaseの__new__メソッドを見ます。200行以上あるので少しずつ読み進めていきます。 #code(Python){{ 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をそのまま使うのではなくモジュールパスだけ渡しています。 次に[[メタデータ>https://docs.djangoproject.com/ja/1.10/ref/models/options/]]の処理が行われていますが指定してないのでさくっと無視。ここで出ているMetaはメタクラスや次に出てくる_metaとはまた別物というところだけ注意(ややこしい) で、続き。 #code(Python){{ new_class.add_to_class('_meta', Options(meta, app_label)) }} 前回も出てきた_metaの正体はOptionsインスタンスのようです。このクラスはoptions.pyで定義されています。 add_to_classメソッド。 #code(Python){{ 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クラスから情報拾ってきての設定とかが行われています。特に重要なところだけ抜き出すと、 #code(Python){{ 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__に戻って、次の例外とかを登録している箇所は省略。 その次、 #code(Python){{ # 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で追加しています。追加されるものとは、 #code(Python){{ class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') }} みたいなフィールドオブジェクトですね。こっちに行くと長くなりそうなので先に__new__の続きを見てしまいましょう。 と言っても、後書いてあるのはプロキシとか継承とかに関する処理なのでこれらもさくっと無視して最後。 #code(Python){{ new_class._prepare() new_class._meta.apps.register_model(new_class._meta.app_label, new_class) return new_class }} appsに登録が行われていますね。これにより前回謎だった「いつの間にかモデルが登録されている」の個所がわかりました。 *django/db/models/fields [#jf71dbcc] それではフィールドクラスです。予想通り、CharFieldもDateTimeFieldもFieldというクラスのサブクラスで大体の処理はFieldクラスで定義されています。そのcontribute_to_classメソッド(一部省略) #code(Python){{ 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メソッド(一部省略) #code(Python){{ 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 [#lffffc5c] 関連についても確認しましょう。まずは関連を設定しているところの復習。 #code(Python){{ 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モジュールに記述されています。まず継承関係を確認しましょう。 #code(Python){{ 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は無視な方向で。 #code(Python){{ 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モジュールに書かれています。必要なところだけ抜き出し #code(Python){{ 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での処理 [#zed52b7d] さて、モデルがインポートされたときにappsに登録、フィールドもインスタンス化したときに自己登録している様子を見てきました。モデルの自己登録処理(ModelBaseの__new__)では登録前に_prepareが呼び出されています。最後にそれについて見てみましょう。 **ModelBase._prepare [#y0c38dba] #code(Python){{ 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 [#d8f39be7] _meta、Optionsクラスの方の_prepareを見てみましょう。 #code(Python){{ 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が先頭に来るようにするためです。該当部分だけピックアップすると、 #code(Python){{ 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の場合はマイナスの値が割り振られ、先頭に並ぶというからくりになっています。 *おわりに [#s0d866b3] 今回はモデルインポート時の処理、モデルの自己登録とメタ情報の構築、必要な属性の補完について見てきました。メタクラスというと身構えてしまいますが、Pythonとしてのメタクラスの利用は先頭だけで残りは淡々とDjangoの世界(メタじゃないプログラミング)でメタ情報を設定していっているという印象でした。