(第71回)Python mini Hack-a-thonで行った内容の個人的な備忘録のその2です。
前回に引き続き、FalconというミニマルなWebフレームワークのソースコードを読んだ内容をまとめています。
Falconが持つ機能
前回はエントリポイントがFalcon.API()であることを調べ、WSGIアプリケーションとしての基本的な動きをしていることを確認しました。 今回はFalcon.API()の詳細を追っていきます。
まずはソースコードの詳細を追う前にFalconが持つ機能を整理してみました。
どのような機能が備わっているかを理解しておくと、「ソースコードが何をしているか」が理解しやすくなります。
- Routing
- URIを正規表現で定義し、対応したResourceクラスの
on_get
やon_post
などを呼び出す
- URIを正規表現で定義し、対応したResourceクラスの
- Sink
- URIを正規表現で定義し、そこへリクエストがきた際に別のサービスを呼び出せる
- Hook
@falcon.before
や@falcon.after
デコレータで、あるリクエスト処理の前後に任意の処理を追加できる
- Middleware
- Hookはリクエスト単位で定義しますが、Middlewareはすべてのリクエストの前後やResource単位で任意の処理を追加できる機能
Falcon.API()の詳細
Falcon.API()のソースコードはこちらにあります。
まずは、__init__とその周辺から。
最初に目に入ったのはこの部分でした。(122-124行目)
__slots__ = ('_request_type', '_response_type',
'_error_handlers', '_media_type', '_router', '_sinks',
'_serialize_error', 'req_options', '_middleware')
私は__slots__
を使ったことがなかったので調べてみました。
__slots__
でインスタンスの変数が宣言されていると、__dict__と__weakref__が自動的に生成されないようです。
その結果、属性を動的に追加できなくなる代わりにメモリを節約できるとのこと。
その他__init__で行っていることはインスタンス変数の初期化だけですね。
次に見るのは__call__
メソッドです。APIクラスにはこの__call__が定義されているため、インスタンスを関数のように呼び出すことができます。
__call__メソッドを上から眺めていくと、下記のコードが出てきます。(177-183行目)
for component in self._middleware:
process_request, _, process_response = component
if process_request is not None:
process_request(req, resp)
if process_response is not None:
mw_pr_stack.append(process_response)
先述したMiddlewareに関する分かりやすいコードです。
まず_middlewareの中にprocess_request
があった場合、それを呼び出しています。
API.__call__は全てのリクエストで呼び出されるため、ここで行う処理は全てのリクエストに対して行われます。
req
を渡しているので、URIに合わせた処理なども一応できそう。
そして、同じく_middlewareに格納されているprocess_response
をmw_pr_stack
というスタックに積んでいます。
このスタックに格納したprocess_responseはこのリクエストに対する最後の処理として実行されるはず。
そして、直後に現れるのはこちらのコード。(192-210行目)
responder, params, resource, req.uri_template = self._get_responder(req)
~省略~
if resource is not None:
# Call process_resource middleware methods.
for component in self._middleware:
_, process_resource, _ = component
if process_resource is not None:
process_resource(req, resp, resource, params)
responder(req, resp, **params)
req_succeeded = True
Falconでは、on_getやon_postなどの実際にリクエストに対して処理を行うResourceに定義されたメソッドをresponder
と総称しています。
_get_responder
にリクエストを渡すことで、そのリクエスト(URI)に対応する処理を行うresourceやresponderを取得しています。
resourceやresponderが正しく取得できたら、今度はそのresourceに対してMiddlewareが定義されているか確認し、定義されている場合は、process_resource
を呼び出します。
ここまででMiddlewareの処理が一旦終わるので、次にresponder
(on_getやon_postなど)を呼び出しています。
responderの処理が終わると、最初にmw_pr_stackに格納していたprocess_responseを取り出し実行します。(223-231行目)
ちなみに、_get_responder
内では、route = self._router.find(path)
というコードで、URIから必要なメソッドなどを格納したrouteオブジェクトを取得しています。
実は、ここのRouting部分にもパフォーマンスを向上させるためのコードが書かれているので、次回はこの部分に関してまとめる予定です。
while mw_pr_stack:
process_response = mw_pr_stack.pop()
try:
process_response(req, resp, resource, req_succeeded)
except Exception as ex:
if not self._handle_exception(ex, req, resp, params):
raise
req_succeeded = False
ここまでで、リクエストに対して、ユーザーが定義した処理はすべて実行されました(Hookはresponder()の呼び出し中に行われる)。
あとは、前回説明したようにWSGIの仕様(start_responseを呼び出し、iterableな値を返す)に沿って、下記コードでレスポンスデータを生成して返しています。
if req.method == 'HEAD' or resp.status in self._BODILESS_STATUS_CODES:
body = []
else:
body, length = self._get_body(resp, env.get('wsgi.file_wrapper'))
if length is not None:
resp._headers['content-length'] = str(length)
if resp.status in (status.HTTP_204, status.HTTP_304):
media_type = None
else:
media_type = self._media_type
headers = resp._wsgi_headers(media_type)
# Return the response per the WSGI spec
start_response(resp.status, headers)
return body
まとめ
ざっくりですが、流れは以上です。まとめるとこんな感じですかね。
- リクエストが来る
- _middlewareに格納したprocess_requestで前処理
- リクエストに合わせて必要なresourceやresponderをrouterから取得
- resourceによっては、_middlewareに格納したprocess_resourceを実行
- responderを実行。ここがon_getやon_post。
- _middlewareに格納したprocess_responseで後処理
- WSGIの仕様に則ってレスポンスを返す
1クラスしか見てないですが、概要は十分把握できた気がしますね。
次回に続く
次回はHookやRouting周りを見れたらいいなと思います。