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"})
このように書き下すことで、モジュールが読み込まれたタイミングでクラス文が評価されるということがわかりやすくなったと思う。
実行サンプル
ちなみに 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:あまり使う機会がない
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.basehttp
。WSGIServer.run
メソッドをみる。
WSGIServer
の生成
threading
が有効であれば socketserver.ThreadingMixIn
を Mixin する様子。無効なら WSGIServer
のみつかう。
この WSGIServer
は django.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_server
の WSGIServer
を呼び出している。
ここをもう少しみていくとこういう継承になっている。
django.core.servers.basehttp#WSGIServer
wsgiref.simple_server#WSGIServer
http.server#HTTPServer
socketserver#TCPServer
つまり django -> wsgiref -> HTTPServer -> socket 通信レイヤーの TCPServer まで降りて行くことになる。
WSGIserver
の実行
- インスタンス作成
- setup
- serve_forever() でループに入る
おわりに
前に開発用サーバーを動かすというときに、たしかに Django が用意してくれたコマンドのサーバーがあるけど、なにをどこまでやっているかがわからなかった。このライブラリではどのレイヤーまで面倒をみてくれるかというのを知るとなにかと楽になる。