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"})
このように書き下すことで、モジュールが読み込まれたタイミングでクラス文が評価されるということがわかりやすくなったと思う。
実行サンプル
ちなみに 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
- django.db.models.base#157
__init__
new_class ->add_to_class(obj_name, obj)
- django.db.models.base#305
add_to_class
->value_contribute_to_class
- django.db.models.fields.init #666
cls._meta.add_field(self, virtual=True)
- django.db.models.options #312
self.local_fields.insert(bisect(self.local_fields, field), field)
ちなみに、PK 指定しているフィールドは new_class._prepare()
の文で new_class._meta.fields
に追加されている。
*1:あまり使う機会がない