前々回に続き、 FalconというミニマルなWebフレームワークのソースコードを読んだ内容をまとめています。
今回はRouting周り(デフォルトルーターのroutingパッケージ)のコードをメインにまとめます。
ちなみに、FalconのデフォルトルーターはCompiledRouterというクラス名になっております。
なぜCompiled
なのか、という点もコードを読めばわかるはず。。。
Falconのルーティング
Falconでルーティングを追加する際のコードは、下記のようにAPIクラスのadd_route
メソッドを呼び出します。
class Resource(object):
def on_get(self, req, resp, name):
resp.body = "Hello {0}!".format(name)
resp.status = falcon.HTTP_200
app = Falcon.API()
res = Resource()
# nameは変数として渡される
app.add_route("/res/{name}", res)
APIクラスのadd_routeはこちら。
def add_route(self, uri_template, resource, *args, **kwargs):
~ 省略(ちょっとしたバリデーション) ~
method_map = routing.create_http_method_map(resource)
self._router.add_route(uri_template, method_map, resource, *args, **kwargs)
各HTTPメソッドと対応するResponder(on_getなどのメソッド)の辞書を作成し、最後の行でURIのパスと共にCompiledRouterクラスのadd_routeに渡しています。
ということで、次はCompiledRouterクラスのadd_routeを見ていきます。
CompiledRouterクラス
CompiledRouter.add_route
add_routeメソッドの内容はこちら。
最後の行でクラス名にもなっている_compile()
というメソッドを読んでいますね。
def add_route(self, uri_template, method_map, resource):
if re.search('\s', uri_template):
raise ValueError('URI templates may not include whitespace.')
fields = re.findall('{([^}]*)}', uri_template)
for field in fields:
is_identifier = re.match('[A-Za-z_][A-Za-z0-9_]+$', field)
if not is_identifier or field in keyword.kwlist:
raise ValueError('Field names must be valid identifiers.')
path = uri_template.strip('/').split('/')
def insert(nodes, path_index=0):
~ 省略 ~
insert(self._roots)
self._find = self._compile()
やっていることはこんな感じ。
- スペースが含まれていないか、重複している要素(/{name}/{name}など)がないか等のチェック
- insert関数を使って、
self._roots
にパスを追加 - 最後に
_compile
メソッドを呼び出す
ちなみにここのself._rootsは、下記のようなlistにCompiledRouterNodeを格納したツリー構造となっています。
CompiledRouter._compile
さて、ここから_compileメソッドを見ていきましょう。
下記がコメント以外は省略していない_compileメソッドの全コードです。
def _compile(self):
self._return_values = []
self._expressions = []
self._code_lines = [
'def find(path, return_values, expressions, params):',
TAB_STR + 'path_len = len(path)',
]
self._compile_tree(self._roots)
self._code_lines.append(
TAB_STR + 'return None'
)
self._src = '\n'.join(self._code_lines)
scope = {}
exec(compile(self._src, '<string>', 'exec'), scope)
return scope['find']
組み込み関数のcompile
を読んでいる箇所がありました。
これがCompiledRouterという名前の所以です。
組み込み関数のcompile, exec
_compile()メソッドは、exec(compile(self._src, '<string>', 'exec'), scope)
という1行にすべてが詰まっています。
が、私もあまりexec
やcompile
は使ったことがないので、調べました。(ドキュメント => exec, compile)
とてもざっくりな理解ですが、下記のような感じでしょうか。
- compile
- ファイルや文字列からコードオブジェクトを作成する
- exec
- 文字列かコードオブジェクトを与えられたスコープ内で実行する
ちょっと触って挙動を確認して見ました。
# 文字列でfindを定義してcompileするとcode objectになる
>>> src = 'def find():\n print("called")'
>>> compiled_src = compile(src, '<string>', 'exec')
>>> compiled_src
<code object <module> at 0x106488810, file "<string>", line 1>
# execでコードオブジェクトをscope内で実行するとそのスコープでfindが定義される
>>> scope = {}
>>> exec(compiled_src, globals(), scope)
>>> scope
{'find': <function find at 0x106494510>}
# もちろんfind関数の呼び出し可能
>>> scope['find']()
called
CompiledRouter._compile に戻る
さて、compileとexecの動きがだいたい理解できたところで
exec(compile(self._src, '<string>', 'exec'), scope)
に戻ります。
execが終わったところにブレークポントを追加して、self._src
の中身を確認。
さすがに画像だけだと見づらいので_srcに格納されている文字列を整形(笑)
def find(path, return_values, expressions, params):
path_len = len(path)
if path_len > 0:
if path[0] == "res":
if path_len > 1:
params["name"] = path[1]
if path_len == 2:
return return_values[0]
return None
return None
return None
return None
app.add_route("/res/{name}", res)で渡したres
, name
があることがわかります。
ちなみに、この_srcに格納された文字列は少し上のself._compile_tree(self._roots)
で作られています。
これをexecで実行することで、find関数がscopeという名前空間に定義されます。
execの第2引数scopeが__builtins__
を含んでいなかったので、組み込みモジュールbuiltinsへの参照も追加されています。
これはドキュメントに記載されている通りですね。
いろいろ処理しているように見えますが、まとめるとadd_routerはfind関数を定義して、Falcon.APIのrouter._findとして保持しているだけです。
リクエストがきたら
さてさて、add_routerの動きは理解できました。 ということで、次は先ほど定義したfindが実際にどう使われるかを見ていきます。
前回紹介しましたが、FalconアプリケーションではリクエストがくるとFalcon.API.__call__が呼ばれ、その中で_get_responder
が呼ばれます。
その_get_responderのコードがこちら。
def _get_responder(self, req):
path = req.path
method = req.method
uri_template = None
route = self._router.find(path)
if route is not None:
try:
resource, method_map, params, uri_template = route
except ValueError:
resource, method_map, params = route
else:
resource = None
if resource is not None:
try:
responder = method_map[method]
except KeyError:
responder = falcon.responders.bad_request
else:
~ 省略 ~
return (responder, params, resource, uri_template)
このなかで、本質的に大事なのは下記の3行。
- route = self._router.find(path)
- resource, method_map, params, uri_template = route
- responder = method_map[method]
まず、最初のfindメソッドのコードはこちらです。pathをリストに変換し、先ほどの_find関数を呼び出しています。
def find(self, uri):
path = uri.lstrip('/').split('/')
params = {}
node = self._find(path, self._return_values, self._expressions, params)
if node is not None:
return node.resource, node.method_map, params, node.uri_template
else:
return None
_findとして格納されたfind関数も再掲。
def find(path, return_values, expressions, params):
path_len = len(path)
if path_len > 0:
if path[0] == "res":
if path_len > 1:
params["name"] = path[1]
if path_len == 2:
return return_values[0]
return None
return None
return None
return None
node変数に格納される値は、_srcのコード文字列を動的に作成した際に、ひっそりと作られていたCompiledRouterNodeです。
このnodeから、node.method_mapを含むタプルがrouteに返されます。 そして、routeからこの値を取りdし、HTTPメソッドと合わせることでresponderを特定しています。
つまり、リクエストがきた際の挙動をまとめると下記のようになっています。
- WSGIサーバーからfalcon.API.__call__が呼び出される。
- falcon.API.__call__が_get_responderを呼び出す。
- _get_responder内でfind関数によりリクエストのパスに対応するCompiledRouterNodeが特定。
- _get_responderには、CompiledRouterNodeからHTTPメソッドに対応するresponderを格納したmethod_map(を含むタプル)が返される。
- _get_responderの
responder = method_map[method]
というコードでresponderを特定、返却する。 - このresponderがfalcon.API.__call__で
responder(req, resp, **params)
という形で実行される。
まとめ
動的にソースコードを生成しているところが面白いですね。 add_routeが呼ばれるたびにコードを生成、実行しているのでアプリケーションが大きくなればなるほど起動にかかる時間が増えそうな気がします。 (コードの生成箇所は再帰な処理もしていますし)
長くて見づらくなってしまいました(笑) まぁ、個人用のメモなので多めに見ていただければと思います。
ちなみにexecは文字列かコードオブジェクトを第1引数にとります。 したがって、
exec(compile(self._src, '<string>', 'exec'), scope)
はcompileしなくても動くはずです。 わざわざcompileしている理由は下記あたりでしょうか。
- syntax errorの検出し、不完全なコードの実行を制御
- セキュリティが少し向上する?
パフォーマンスの向上もあるのかなとも考えましたが、インタプリタ上で試したところcompileの有無ではパフォーマンスに差異は見つからず。 Pythonistaな方々、本当の理由を教えてくださいm(_ _)m