Python製WebフレームワークのURL DispatcherとType Hintsの活用について

少し前から趣味で実装してるKobinというWebアプリケーションフレームワークのURL Dispatcherの実装をする時に、BottleやDjangoを参考にしながら考えてみました。 これらの比較とType Hintsを利用したKobinのURL Dispatcherの実装についてまとめます。

(追記) リバースルーティング

ちょっと長いですが、追記です。結論としては元々紹介していた正規表現ベースのルーティングとは違う方法をKobinで採用しました

この記事では正規表現によるルーティングについて解説していたのですが、このルーティング方法は逆引きが少し難しくなります(Django正規表現ベースの逆引きですが、自由度の高い正規表現からURLを生成するために一部妥協しているところもあるようです)。 実際の問題では正規表現が必要となるほど複雑なURL構成になることは稀かもしれません。

逆引きのしやすさを考えると次のようなルーティングがいいかと思いました。

@app.route('/users/{user_id}')
def user_detail(user_id: int):
    pass

この方法で優れているのは、 .format メソッドにより逆引きが出来てとてもシンプルです。 正引きの実装は正規表現モジュールを使ったときほど、簡単ではないですがそこまで複雑になるわけでもありません。

追記終わり

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, 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の型定義情報からURLの変数情報(urlvars)その型に変換しています。

# 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型で値が渡ってきます。 感じているメリットとしては、

  • 正規表現をそのままつかっているため、自由度が高く複雑なURLスキーマにも対応できる
  • さらにDjangoと違い、Type Hintsの型情報によってキャストされたオブジェクトをview関数で受け取れる
  • Type Hintsの型定義情報を利用しているため、ドキュメントの生成時やIDEの型チェックなどType Hintsによる恩恵をそのまま受けることが出来そう
    • FlaskやBottleでは型の情報をURLのところに含めていたため、Type Hintsのエコシステムによる恩恵は受けれなかった
  • 実装がとても簡単
    • re.compile('/user/(?P<id>\d+').match('/users/1/').groupdict() みたいにidにあたる数字を文字列で取得してから、Type Hintsで指定している型情報にキャストするだけ

github.com

ご意見あればお願いします。

感想

Python3.5でとても面白い機能が追加されたと思うのですが、Webアプリを書いてる時にType Hintsをどう組み込もうか考えているとこのようになりました。 Type Hintsの導入によって型を頼りにコードが読みやすくなるとか、ドキュメントやIDEへの情報が増えるとかメリットが有りますが、それ以外にもこれによって実装が変わってくるような面白い使い方が出来ないか考えていこうと思います。

[おまけ] Type Hintsを使ってみて

ついでに最近Type Hintsを使っていて気づいたことをメモ。 Type Hints自体の基本的な使い方は↓の翻訳記事を見るのが良いと思います。

Flake8の時にF401: import but unused で怒られる

from typing import Dict

def hoge(foo: Dict[str]) -> None:
    pass

とかするとDictが使われてませんって怒られます。対処としては # 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'>}

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)

エキスパートPythonプログラミング 改訂2版 (アスキードワンゴ)