2017年 学んでよかったツール/ライブラリ/サービス

ツール/ライブラリ/サービスをまぜこぜして書いていく。

1: Vue

今年前半にSPAを組む機会があり、Vueを使った。SPA ということで vue-cli の webpack-simple をベースに Vue + Vuex + Vue Router あたりを使用した。Vue は学習コスト低めで公式日本語ドキュメントも充実。丁寧に公式ドキュメントを読んで実行すればけっこうやりたいことができる。

案件としてはE2Eテスト環境等を整備できず、その他振り返ってみて保守性が悪かったかもしれない。せめてTypeScriptを使ったらよかったのかもしれないし、規模がある程度大きくなる場合 Angular のほうが適切だったかもしれないという反省がある。

個人的にこのVueまわりを扱ったのはなかなか大きな経験になった。また、一つの値を変更すると連鎖的に他の値が自動的に計算されて表示が変わるのが、ダイナミックで面白い。初めてファイルサーバーを動かしたとき、あるいは初めてcronを動かしたときのような。こういうのを触るのはけっこう楽しい。

あとSPAについての雑感。ドキュメントのマークアップではなくブラウザ上にGUIアプリを再実装するもので、つまりデスクトップアプリやモバイルアプリと同一線上にあると感じた。だからSPAを実装するためにはGUIアプリの知識が必要で、比較的規模も大きくなるのでビルド環境等を整える必要も出てきて、ついでにCSSコンパイル等もいれたくなり、アニメーションについて考え等々。結果的に学習コストがわりとかかる。やっぱり一定線を超える部分は専門の人に頼みたい。

せっかくなのでいくつかアプリを作った。1つ目は筋肉量計算で、身長体重等を入力するとそれっぽく計算してくれる。学習用サンプルとして小さいコードでわかりやすいし、こういうちょっとしたものを作るだけなら1枚の index.html だけで組めるのが気分的にいい。

2つ目は django のフォームセット周りを jQuery で扱いたくないためにつくったサンプル。非SPA想定で、DatePicker は別ライブラリを使用している。

3つ目は Trello のようなドラッグ&ドロップができるカードアプリ。Trello は今もヘビーに使っているサービスで、一度組んでみたかった。そこそこ大変だったもののいちおう完成できた。だいぶぐちゃっとしたコードだけどそれなりに動く。

2: Terraform

今年中盤くらいに触った。役割と機会的にインフラを担当することになり、Terraform を使った。CloudFormation などと厳密に比較はしていないものの、比較的学習コスト低めで使えるのがよかった。環境ごとに tfstate を分けたい、環境ごとに使用する変数を分けたい、微妙に Terraform では対応していない箇所がある等使いこなそうとするとそれなりに面倒なところはあるものの、 wiki や口伝で構成管理されているよりは比較的安心感があった。

なんだかんだでコマンドを実行すると環境が1発で整い、逆にコマンド1発で環境をすべて破壊できる。こういうのを触ると、プログラミングって楽しいものなんだなと思う。

せっかくなので Terraform を使ったリポジトリをつくった。 socks proxy 用インスタンスを建てられる。ついでにVPC + EC2 + RDS + S3 あたりも揃えられる。

GitHub - altnight/terraform-socks-proxy-instance

次点: Circle CI 2.0

今年中盤くらいに Circle CI 2.0 が正式リリースされたのにあわせて、実行速度が上がるとのことで試してみていた。多少実行速度が上がり、 Docker ベースなのでテスト実行コンテナやDBに docker hub 上のイメージを使えるのは便利だった。

ただ個人的には Docker を扱う手間の方が大きい気がしていて、労力に見合っている気がしない。Docker 難しい。

GitHub - altnight/circleci2_test

2018年は

これからかんがえようふわっと

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 が用意してくれたコマンドのサーバーがあるけど、なにをどこまでやっているかがわからなかった。このライブラリではどのレイヤーまで面倒をみてくれるかというのを知るとなにかと楽になる。