読者です 読者をやめる 読者になる 読者になる

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:あまり使う機会がない

runserver がどう動いているか実装をみる

いつも開発で使うことになる runserver 。これはどういう風にしてうごいているのかを確認してみる。あと、 middleware がどう動くか実装をみる - そのあれ でかいた BaseHandler がどこで挿入されているか確認する。

参考

おさらい

実行するコマンドはこういうやつ

python manage.py runserver
python manage.py runserver 0.0.0.0:8000
python manage.py runserver 0.0.0.0:8001 --settings=proj.settings

実行コマンドをみる

読むモジュールは django 標準で登録されているので django.core.management.commands.runserver

BaseCommand.handle が起点になるのは manage.py(django-admin.py)でコマンドを実行するための実装を見る - そのあれ で確認したとおり。

流れ

  • self.handle
  • self.run
  • self.inner_run
    • self.validate
    • self.check_migrations
    • self.get_handler
      • get_internal_application
    • run

BaseHandler がつかわれているところ

get_internal_application ではこういう実装になっている(ちょっと端折っている)。

app_path = getattr(settings, 'WSGI_APPLICATION')
if app_path is None:
    return get_wsgi_application()

try:
    return import_string(app_path)

docstring にかいてあるのだけど、 settings に WSGI_APPLICATION を指定していなければ get_wsgi_application が呼ばれるので、前回書いた内容通りの WSGIHandler が使われる。settings の WSGI_APPLICATION でのデフォルトは proj.wsgi.application という風に生成されている。実際 wsgi.py では application = get_wsgi_application() としてモジュール変数にアサインしている。

ちょっと誤解していたので訂正。

  • settings に WSGI_APPLICATION を指定していなければ get_wsgi_application が呼ばれるので、前回書いたとおり WSGIHandler が使われる
  • settings の WSGI_APPLICATION のデフォルト値は proj.wsgi.application という風に生成されている

    • wsgi.py では application = get_wsgi_application() としてモジュール変数にアサインしている。
    • = WSGIHandler が使われる

つまり初期設定だとどちらでも WSGIHandler が使われるということだった。

自分の理解が正しければ、このあたりまできたら次は WSGI application を動かすってどういうことなのかってところに行きそうなんだけど。そこまでわかってないのでまた今度。

メインループがなにをしているかをみる

読むモジュールは django.cre.servers.basehttpWSGIServer.run メソッドをみる。

WSGIServer の生成

threading が有効であれば socketserver.ThreadingMixIn を Mixin する様子。無効なら WSGIServer のみつかう。

この WSGIServerdjango.core.servers.basehhtp モジュールのクラス。抜粋してはるとこう

class WSGIServer(simple_server.WSGIServer, object):
    """BaseHTTPServer that implements the Python WSGI protocol"""

    request_queue_size = 10

    def __init__(self, *args, **kwargs):
        if kwargs.pop('ipv6', False):
            self.address_family = socket.AF_INET6
        super(WSGIServer, self).__init__(*args, **kwargs)

wsgiref.simple_serverWSGIServer を呼び出している。

ここをもう少しみていくとこういう継承になっている。

  • django.core.servers.basehttp#WSGIServer
  • wsgiref.simple_server#WSGIServer
  • http.server#HTTPServer
  • socketserver#TCPServer

つまり django -> wsgiref -> HTTPServer -> socket 通信レイヤーの TCPServer まで降りて行くことになる。

WSGIserver の実行

おわりに

前に開発用サーバーを動かすというときに、たしかに Django が用意してくれたコマンドのサーバーがあるけど、なにをどこまでやっているかがわからなかった。このライブラリではどのレイヤーまで面倒をみてくれるかというのを知るとなにかと楽になる。

manage.py(django-admin.py)でコマンドを実行するための実装を見る

django のアプリやモデルに依存しているバッヂ処理やちょっとしたユーティリティーを書くとき、カスタムコマンドをつくることがままある。そのとき、作り方はチュートリアルを参考にお約束を守ってディレクトリを掘り、コピペして scaffold をつくればとくに問題なく動く。とはいえ、どういう仕組みのお約束なのかなどがわからないということがあった。

結論としては、ソースを呼んでる中で気づいたのだけど django/base.py at master · django/django · GitHub を読むのが一番確実で早いかと思う。

参考

おさらい

howtoから引用するとこういう内容。django app に management/commands をほって

polls/
    __init__.py
    models.py
    management/
        __init__.py
        commands/
            __init__.py
            _private.py
            closepoll.py
    tests.py
    views.py

BaseCommandを継承した Command をつくって、 handle に処理をかく

from django.core.management.base import BaseCommand, CommandError
from polls.models import Poll

class Command(BaseCommand):
    help = 'Closes the specified poll for voting'

    def add_arguments(self, parser):
        parser.add_argument('poll_id', nargs='+', type=int)

    def handle(self, *args, **options):
        for poll_id in options['poll_id']:
            try:
                poll = Poll.objects.get(pk=poll_id)
            except Poll.DoesNotExist:
                raise CommandError('Poll "%s" does not exist' % poll_id)

            poll.opened = False
            poll.save()

            self.stdout.write('Successfully closed poll "%s"' % poll_id)

実装を見る

ざっくりと流れを見る

django-admin.py または manage.py を見ると execute_from_commnd_line を実行しているので、そこから読むことになる。

  • execute_from_commnd_line

    • ManagementUtility クラスのインスタンス

      • コマンドのパース
      • 登録してるコマンドを探して登録
    • ManagementUtility.execute()

      • ManagementUtility.fetch_command().run_from_argv()

        • BaseCommand.execute()

          • BaseCommand.handle()

つまりコマンドをパース、コマンドを探してみつかれば実行。みつかったコマンドクラスで実行メソッドを呼び出して最終的に BaseCommand.handle() を呼び出す。ここからは自分で書いたコードが実行される。

コマンドの登録

読むモジュールは django.core.management.__init__

  • ManagementUtility クラスでコマンド一覧の取得とチェックをする

    • 引数の初期化( CommandParser ), 設定ファイルが正しいかと設定( settings.configure() , django.setup() ) autocomplete 用の設定(for bash)
    • 最終的には fetch_command(subcommand).run_from_argv(argv) の実行
    • get_commands とその中で呼び出している find_commandsdjango 標準のコマンドを探しに行っている。
    • fetch_commands の中で呼び出している load_command_class 内で import_module している。そのときに文字列で指定している。ソースが短いので貼るとこう。
def load_command_class(app_name, name):
    """
    Given a command name and an application name, returns the Command
    class instance. All errors raised by the import process
    (ImportError, AttributeError) are allowed to propagate.
    """
    module = import_module('%s.management.commands.%s' % (app_name, name))
    return module.Command()

なので、 management.commands を指定しているのは、単にこういう仕様としているからという理由になる。あと module を import したあと Command()インスタンス化しているのだけど、ここのクラス名も決め打ちということになる。

コマンドの実行

読むモジュールは django.core.management.base

run_from_argv()

BaseCommandクラスのメソッド。大まかな流れはこう

  • run_from_argv
  • execute
  • handle

なので、公式ドキュメント上の BaseCommand を継承して handle メソッドに実装を書くのは、こういう実行順序になっているから。

おわりに

公式ドキュメントを読んで素直に実装すればOKなのだけど、こうして実装をすこし知ると理解しやすい。よく management.commandsmanagement.commandtypo して動かないということをしたのも、単に文字列指定が間違っていたということがわかれば納得。