django ではURLのルーティングを django.urls パッケージで処理している。このパッケージのソースはなかなか読みにくいので、読むための手がかりを記しておく。
バージョンは2.1を仮定していることに注意1。
クラス階層
django.urls
パッケージでは継承もインタフェースも使っていないダックタイピングの見本のようなコードになっており 2 、この点が読みにくくしている一番の要因である。しかも、歴史的な都合なのか、クラスの命名規則にも致命的なわかりにくさが存在している。まずは主要なクラスの構造を説明することで見通しを良くする。
リゾルバ
いちばん重要な概念はリゾルバである。リゾルバは自分が解決すべきURL ( PATH_INFO
)と、その解決方法を知っているデータである。 resolve()
メソッドを提供し、入力されたURLに合致するビュー 3 を返す。リゾルバは木構造をなしており、以下の2種類がある。
URLResolver
は他のリゾルバを含むリゾルバである。最上位のリゾルバは URLResolver
となる。また、 path
関数で include
を指定したときにも生成される。
一方、 URLPattern
は末端に位置するリゾルバで、ビューを保持している。 path
関数でビューを指定したときに生成される。
「自分が解決すべきURL」は、パターンクラスによって表されている。
パターン
パターンはリゾルバに付属する。URLが自分が扱うべきものであるかを判定し、さらにビューに必要なパラメータを抜き取ることができるクラスである。 match
メソッドを提供し、マッチ後に残ったURL、抽出したパラメータ ( args
または kwargs
)を返す。以下の3種類があるが、 URLPatternはパターンではない ことに注意する。
RegexPattern
はおそらく django 1.x 系の頃使われていたもの。今でも re_path
によって生成することはできるが、利用する必要はないだろう。
RoutePattern
が通常使われるもので、 path
関数を使ってパターンを記述すると生成される。結局入力されたパターン文字列は正規表現に変換されるが、 re_path
のように正規表現で直接記述するよりはわかりやすい記述が可能と言えよう。
LocalePrefixPattern
は特殊なもので、 i18n 対応でURLの先頭に言語 /ja/...
/en/...
などをつけるときに使われる。マッチする文字列は現在の言語設定によって異なる。例えば、現在の設定が日本語の場合、 ja/
という静的な文字列にマッチするパターンとして振る舞う 4 。このパターンを含め、URLの i18n 対応は無理やり突っ込んだ感がかなりある。例えば、このパターンは最上位のリゾルバにしか指定できないが、その判定も include
を使った構築時に 配下となる予定のリゾルバがこのパターンを使っていないかをチェックする と言っただいぶ強引な方法で判定している。後、URLの先頭に含まれる静的な文字列 ja/
は URLの先頭に含まれる文字列から決まったりする のだが、卵が先か鶏が先かよくわからない。国際化対応の実装は他にやりようがなかったんだろうか・・・。
resolve : 順方向の解決
与えられたURLを処理するビューと呼び出しに使うパラメータを得るための resolve
関数のコードを見ると、 最上位のリゾルバに依頼するだけ 。とてもシンプル。他の箇所もこれくらい読みやすければ文句がない。
念のためリゾルバ内の resolve
も読んでみると思ったより長くてウッとなるが、実際は 再帰的にresolveを呼び出している だけ。リゾルバのクラスが成す階層構造さえ知っていれば怖いことはない。ただし、ここにでてくる変数 pattern
は パターンではなくリゾルバ なので注意が必要(自分でも何を言っているかわからない)。
reverse : 逆方向の解決
reverse
は resolve
の逆で、ビューの名前と呼び出し時に使うパラメータからURLを生成する。こいつもリゾルバに依頼すれば終了なのではないかと 淡い期待で読み始める と、全くそうではない難解なコードであることがわかる。なんでリゾルバに reverse
させないの・・・マジなの・・・?
reverse
関数内では、リゾルバの階層を潜ってビューを持つ URLResolver
を得る 5 。このとき、途中で現れたパターン(を表す正規表現)を拾う 6 。得られた URLResolver
は 新しいURLResolverでラップされる 。この URLResolver
は集めておいたパターン(正規表現)に紐付けられるので、新しい URLResolver
は完全なURLを再構築できる。
reverse
関数がリゾルバの階層を潜るには URLResolver
が持つ reverse_dict
という辞書が必要になるが、この辞書は _populate というメソッドが生成する。実質、 _populate
が逆引きロジックの本丸になっていると言え、非常に重要な役割を果たすメソッドである。
URLの再構築は、新たな URLResolver
の _reverse_with_prefix メソッドが行う。なんとプライベートメソッドである・・・。このメソッドは一見長いが、 reverse_dict
に入っている再構築に必要な情報を元にURLを再構築し、 パターンに適合するURLができたかをチェック した上で返すだけである。
ということで、URLの再構築に必要な情報は _populate
の中で構築される。 _populate
の実装を見てこのエントリは終わることにしよう。 _populate
は配下のリゾルバをすべて読み込み(再帰的に _populate
が呼ばれる)、ビュー名 ( path
の name
引数)をキーとする辞書を作る。これが reverse_dict
で、 reverse
関数が URLResolver
を見つけるために用いる。
そして、この辞書の値がURLの再構築に必要な情報である。値にはパターンの正規表現(検証用)、引数のデフォルト(穴埋め用)、コンバータの一覧が含まれるが、一番重要なのは正規表現を normalize関数 で変換したフォーマット文字列と引数リストである。例えば <int:year>/<int:month>
のような RoutePattern
からは (?P<year>[0-9]+)/(?P<month>[0-9]+)
のような 正規表現が作られる が、この正規表現から '/%(year)s/%(month)s/'
のようなフォーマット文字列と ['year', 'month']
という引数リストが抽出される。 normalize
の実装は真面目に正規表現をパースしており、利便性のためとは言えここまで頑張るか・・・と思わせる実装になっている。この情報を使うことで、 _reverse_with_prefix
は URL を再構築できるというわけである。長かった。
ところで、 normalize
の戻り値は1つではない。これは、 ?
や *
など、 0 個を許すパターンにおいて、フォーマット文字列の該当する引数があるパターンとないパターンを場合分けする必要があるからだ。URL再構築時にはキーワード引数などからどの引数があるか、ないかをチェックし、利用するフォーマット文字列を決めることになる。また、 .
や [0-9]
などがパターンに使われている場合はいずれの文字を選んでもマッチするので、前者は .
、後者は 0
を含むURLが代表として構築される。