Django のモデルとフィールドのクラス変数について

普段 Django でモデル宣言をするときは、下のコードのように django.db.models.Model を継承したクラスをつくってクラス変数にフィールドを宣言する。メタ情報として class Meta を書くことも多いと思う。

from django.db import models

class MyUser(models.Model):
    name = models.CharField("名前", max_length=128)
    age = models.IntegerField("年齢", default=0)

    class Meta:
        db_table = "my_user"
        verbose_name = verbose_name_plural = "マイユーザー"

サンプル通りに記述すればアプリを作れる。ただ「特定のクラスを継承してクラス変数にフィールドを宣言するだけで、マイグレーションファイルがつることができ、DBのテーブルを更新できる」ということは相当な部分をフレームワークが処理してくれているからだ。いまさらながらとはいえ、これはけっこうすごいことだなと思ったので今回書いている次第。

なにか間違ってるところや微妙なところがあったら、どこかで指摘してください。

結論

  • django.db.models.Model の初期化は多くのことをしているのに、使う側は1クラスを継承するだけでいいので楽
  • クラスとインスタンスのどちらを使用しても問題ないときは、フレームワークがいい感じに処理していることがある

目次

  • 一般的なモデルの宣言
  • class 文と type 関数
  • クラス変数にクラスとインスタンスの両方を渡せる場合(django.forms.fields.Field)
  • django.db.models.Model でのクラス変数はどこへいった?

調べてない

  • 他のORMでどうしているか
  • django.db.models.Model の初期化処理の詳細

    • コードを読もうしたけど、だいぶ大変

バージョン

class 文と type 関数

class をつかったクラス宣言

クラス文はモジュールが読み込まれたときに読み込まれる。いったん django から離れて確認する。

In [1]: class A(object):
   ...:     a = 1
   ...:

In [2]: print(A.a)
1

In [3]: obj = A()

In [4]: obj.a
Out[4]: 1

クラス変数なので class 文で宣言した後メンバーである a を参照できる。 Aインスタンスもクラス変数の a を参照する。そのため、 1 が返ってくる。

type をつかったクラス宣言

type 関数はメタクラスをつくるとき、あるいは動的にクラスをつくるときに使うようだけど*1、今回は理解しやすくするために単純に class 文の書き換えとしてつかう。

つまり class 文では下のようにかくことが

class MyUser(object):
    def __init__(self, name):
        self.name = name
assert isinstance(MyUser, type) == True

type を使うことで、モジュールに関数が並んでいるだけのような記述ができる。

MyUser = type("MyUser", (object, ), {"__init__": None})
assert isinstance(MyUser, type) == True

django での例(マイグレーションをしてみよう)

先ほどの Django での例も type 関数を使った表現で書ける。( __module__ がなくてエラーになったので、それは付け足している)

MyModel = type("MyModel", (models.Model,), {"name": models.CharField("名前", max_length=128),
                                            "__module__": "myuser.models"})

このように書き下すことで、モジュールが読み込まれたタイミングでクラス文が評価されるということがわかりやすくなったと思う。

実行サンプル

github.com

ちなみに makemigrations を実行するとこうなる

(mymodel)[altnight@mba ~/PycharmProjects/mymodel ]
: py manage.py makemigrations
Migrations for 'myuser':
  0001_initial.py:
    - Create model MyModel

migrate の結果、dbファイルができる。

(mymodel)[altnight@mba ~/PycharmProjects/mymodel ]
: python manage.py migrate
Operations to perform:
  Apply all migrations: myuser, auth, sessions, contenttypes, admin
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying myuser.0001_initial... OK
  Applying sessions.0001_initial... OK

クラス変数にクラスとインスタンスの両方を渡せる場合(django.forms.fields.Field)

django.db.models.Model の話からいったん離れる。django を使っていてクラス変数にクラスとインスタンスの両方が宣言できる場合がある。具体的にはフォームで使われる django.forms.fields.Field のこと。

フォームを定義するときは下のように書くことがある。

from django import forms

class MyForm(forms.Form):
    name1 = forms.CharField()
    name2 = forms.CharField(widget=forms.TextInput)
    name3 = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"}))

自前でフィールドを定義する場合は下のようにクラスあるいはインスタンスを渡している。

class MyField1(forms.CharField):
    widget = forms.TextInput
    
class MyField2(forms.CharField):
    widget = forms.TextInput(attrs={"class": "form-control"})

どうしてこのように動くのかがしばらく疑問だったのだけど、ソースを読むと isinstance(widget, type) で class かどうかを判定し、クラスの場合はインスタンスにしていることがわかる。つまり、うまいこと処理してくれたのは django でどちらをわたしても動くように処理がかいてあったから、ということになる。

具体的に isinstance で判定している箇所以外のところを削除したコードは以下。

https://github.com/django/django/blob/master/django/forms/fields.py#L48

class Field(six.with_metaclass(RenameFieldMethods, object)):
    widget = TextInput  # Default widget to use when rendering this type of Field.
    hidden_widget = HiddenInput  # Default widget to use when rendering this as "hidden".
    
    def __init__(self, required=True, widget=None, label=None, initial=None,
                 help_text='', error_messages=None, show_hidden_initial=False,
                 validators=[], localize=False, disabled=False, label_suffix=None):
        self.required, self.label, self.initial = required, label, initial
        
        # ここから
        widget = widget or self.widget
        if isinstance(widget, type):
            widget = widget()

        # Trigger the localization machinery if needed.
        self.localize = localize
        if self.localize:
            widget.is_localized = True

        # Let the widget know whether it should display as required.
        widget.is_required = self.required

        # Hook into self.widget_attrs() for any Field-specific HTML attributes.
        extra_attrs = self.widget_attrs(widget)
        if extra_attrs:
            widget.attrs.update(extra_attrs)

        self.widget = widget 
        # ここまで

django.db.models.Model で宣言したフィールドはどこへいった?

django.db.models.Model の話に戻る。すこしややこしい話。

最初の例でも示したとおり、単なる object を継承したクラスの場合はクラス変数がクラスからでもインスタンスからでも参照できることがわかりやすい。A(object) に a = 1 と書いてあれば、インスタンスでも print(a.a) # 1 という結果は予想しやすい。 しかし django.db.models.Model の場合、クラス変数で宣言したフィールドはインスタンスだとフィールドが見つかるがクラスの公開フィールドには見つからない。あくまで MyModel._meta._fields というプライベートな領域を通して参照できるようになっている。

下に具体的な例をかく。クラスの場合は以下のメンバーが見つかる。

In [9]: MyModel
Out[9]: myuser.models.MyModel

In [10]: MyModel.
MyModel.DoesNotExist             MyModel.objects
MyModel.MultipleObjectsReturned  MyModel.pk
MyModel.check                    MyModel.prepare_database_save
MyModel.clean                    MyModel.refresh_from_db
MyModel.clean_fields             MyModel.save
MyModel.date_error_message       MyModel.save_base
MyModel.delete                   MyModel.serializable_value
MyModel.from_db                  MyModel.unique_error_message
MyModel.full_clean               MyModel.validate_unique
MyModel.get_deferred_fields

インスタンスの場合は以下のメンバーが見つかる。name が独自に定義した箇所、 id は自動的に発行されるフィールド。

In [10]: mymodel
Out[10]: <MyModel: MyModel object>

In [11]: mymodel.
mymodel.DoesNotExist             mymodel.name  # 増えてる
mymodel.MultipleObjectsReturned  mymodel.objects
mymodel.check                    mymodel.pk
mymodel.clean                    mymodel.prepare_database_save
mymodel.clean_fields             mymodel.refresh_from_db
mymodel.date_error_message       mymodel.save
mymodel.delete                   mymodel.save_base
mymodel.from_db                  mymodel.serializable_value
mymodel.full_clean               mymodel.unique_error_message
mymodel.get_deferred_fields      mymodel.validate_unique
mymodel.id  # 増えてる

先ほど書いたとおり、フィールドは MyModel._meta.fields 以下にみつかる

In [13]: MyModel._meta.fields
Out[13]:
(<django.db.models.fields.AutoField: id>,
 <django.db.models.fields.CharField: name>)

どうして django.db.models.Model を継承すると、クラス変数で宣言したフィールドは消えて、インスタンスには存在するのか。それは django.db.models.Model がそうしているから。

ソースを確認したいけど、けっこう長いので処理を追った結果だけかいておく。 https://github.com/django/django/blob/master/django/db/models/base.py#L67

  1. django.db.models.base#157 __init__ new_class -> add_to_class(obj_name, obj)
  2. django.db.models.base#305 add_to_class -> value_contribute_to_class
  3. django.db.models.fields.init #666 cls._meta.add_field(self, virtual=True)
  4. django.db.models.options #312 self.local_fields.insert(bisect(self.local_fields, field), field)

ちなみに、PK 指定しているフィールドは new_class._prepare() の文で new_class._meta.fields に追加されている。

*1:あまり使う機会がない