北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

django.urlsのコードリーディング

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 : 逆方向の解決

reverseresolve の逆で、ビューの名前と呼び出し時に使うパラメータから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 が呼ばれる)、ビュー名 ( pathname 引数)をキーとする辞書を作る。これが 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が代表として構築される。


  1. 今のプロジェクトで使っているため。世間では2.2が出ているようだ。

  2. しかも、ダックタイピングを使う必要はまったくない。

  3. djangoのビューは一般的なWAFで言うコントローラであり、 callable な関数またはクラスである。

  4. 静的な文字列なので、 reverse 関数適用時に利用者が言語名を指定する必要はない。

  5. 末端である URLPattern ではないことに注意。

  6. 正確には値の変換に使うコンバータも。