少し前から趣味で実装してるKobinというWebアプリケーションフレームワークのWeb Dispatcherの実装をする時に、BottleやDjangoを参考にしながら試行錯誤してみました。 これらの比較とKobinのWeb Dispatcherの実装についてメモ。ご意見あればお願いします。
Pythonの正規表現モジュールのおさらい
BottleやDjangoのURL Dispatcherについて見ていく前に、Pythonの正規表現モジュール について簡単におさらい。
>>> import re >>> url_scheme = '/users/(?P<user_id>\d+)/' >>> pattern = re.compile(url_scheme) >>> pattern.match('/users/1/').groupdict() {'user_id': '1'}
このように名前付きグループでパターンを定義し、マッチするか確認してからgroupdictを呼ぶことでuser_idの部分の数字が文字列で取得出来ます。 それではまずDjangoのWeb Dispatcherを見ていきます。
Django, Bottleのルーティング手法
Django
Djangoは正規表現でURLスキーマを定義するため、自由度が高く複雑な場合にも対応出来ます。
# https://docs.djangoproject.com/en/1.9/topics/http/urls/ from django.conf.urls import url from . import views urlpatterns = [ url(r'^blog/$', views.page), url(r'^blog/page(?P<num>[0-9]+)/$', views.page), ] # View (in blog/views.py) def page(request, num="1"): # Output the appropriate page of blog entries, according to num. ...
この方法は正規表現モジュールで簡単に実装が出来そうですが、型情報の推測が困難なため、Djangoではviewの関数に渡されるとき全て文字列で渡されます。 整数として扱いたい場合などには、いちいちview関数の中でキャストする必要があります。
Bottle
一方Bottleは下記のように <id: int>
のような指定方法です。型が分かるため、view関数には型変換したオブジェクトを渡すことができます。
ただ、これだけでは表現しきれない場合があるため、複雑な指定をする時のために正規表現による指定方法も用意されています。
# http://bottlepy.org/docs/dev/tutorial.html#request-routing @route('/object/<id:int>') def callback(id): assert isinstance(id, int) @route('/show/<name:re:[a-z]+>') def callback(name): assert name.isalpha()
内部の実装としては、こんな感じのdictを用意して、一度正規表現に変換しているようです。 Flaskも実装を読んだわけではないですが、同じように指定するため実装方法も大きく変わらないんじゃないかなと思います。
この方法は、↓のように感じます。
- 一度正規表現に変換する必要があるため、Djangoの方法に比べると実装が少し手間
- ただ正規表現に慣れてないユーザにとっては扱いが簡単ですね
- BottleやFlaskでType Hintsも使おうとすると、URL Dispatcherでの型を指定と重複してしまう
- ↓の例のように2箇所で型情報を定義することになってしまう
@route('/object/<id:int>') # ここで int を指定 def callback(id: int): # ここでも int を指定 pass
正規表現による指定とType Hintsの活用
Djangoのような正規表現によるURL Dispatchは比較的実装が簡単そうなのでKobinでも採用しました。 ただKobinではそれに加えてType Hintsの型定義情報からその型に変換しています。
# https://github.com/c-bata/kobin/blob/master/example/hello_world.py from kobin import Kobin app = Kobin() @app.route('^/years/(?P<year>\d{4})$') def casted_year(year: int): return 'A "year" argument is integer? : {}'.format(isinstance(year, int))
上の例を実行するとFlaskやBottleのようにint型で値が渡ってきます。 感じているメリットとしては、
- 実装がとても簡単
re.compile('/user/(?P<id>\d+').match('/users/1/').groupdict()
みたいにidにあたる数字を文字列で取得してから、Type Hintsで指定している型情報にキャストするだけ
- 正規表現をそのままつかっているため、自由度が高く複雑なURLスキーマにも対応できる
- さらにDjangoと違い、Type Hintsの型情報によってキャストされたオブジェクトをview関数で受け取れる
- Type Hintsの型定義情報を利用しているため、ドキュメントの生成時やIDEの型チェックなどType Hintsによる恩恵をそのまま受けることが出来そう
- FlaskやBottleでは型の情報をURLのところに含めていたため、Type Hintsのエコシステムによる恩恵は受けれなかった
感想
Python3.5でとても面白い機能が追加されたと思うのですが、Webアプリを書いてる時にType Hintsをどう組み込もうか考えているとこのようになりました。 他にもちょっと変わった使い方が出来ないか考えていこうと思います。
[おまけ] Type Hintsを使ってみて
ついでに最近Type Hintsを使っていて気づいたことをメモ。 Type Hints自体の基本的な使い方は↓の翻訳記事を見るのが良いと思います。
PyPIに上がってる最新版だとImportErrorが起きる。
原因はこれ → Can't import "Undefined" from typing module · Issue #1134 · JukkaL/mypy · GitHub
この記事の執筆時点だとまだ修正バージョンがリリースされてないのでGithubから直接とってくる必要があります。 tox.iniとかも↓のように変えました。
[testenv:mypy] basepython = python3.5 deps = git+https://github.com/JukkaL/mypy commands = mypy kobin
Flake8の時にF401: import but unused
で怒られる
対処としては # NOQA
をつけてskipするか、F401
をignoreするかだけど、今のところ後者の方法をとってます。
# setup.cfg [flake8] ignore = F401
ただこのチェックを無視するのはあんまりやりたくないなぁという印象
型定義情報の取得
型定義の情報は以下のように get_type_hints
関数で簡単に習得出来ます。
>>> from typing import get_type_hints >>> def hoge(a: int) -> None: ... pass ... >>> get_type_hints(hoge) {'return': None, 'a': <class 'int'>} >>> hoge.__annotations__ {'return': None, 'a': <class 'int'>}
- 作者: Mark Summerfield,斎藤康毅
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/12/01
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る