FastAPIから学ぶWebフレームワークの内部
事前準備
前提条件
作業環境は以下です.
- Python (3.7.4)
- Poetry 0.12.17
- VSCode (1.38.1)
- macOS Mojava (10.14.6)
プロジェクトの作成
poetry new --src fast-001
cd fast-001
poetry add uvicorn fastapi
依存パッケージ
Uvicorn
軽量なASGIサーバーらしいです. つまりサーバーです. Webフレームワークで作ったWebアプリを動かすソフトウェアということですね.
WSGI
ASGIはWSGIを非同期な処理にも拡張した仕様らしいです. WSGIはサーバーとWebアプリケーションの標準インターフェースを定めたもののようです.
The Web Server Gateway Interface (WSGI) is a standard interface between web server software and web applications written in Python. 1
こうやって共通のインタフェースを決めておくと開発が別々にできますねということでしょうか.
Starlette
ASGIに対応したWebフレームワーク(ツールキット)らしいです. UvicornとStarletteは両方ASGIに対応しているので一緒に使えますねということです. WebsocketやGraphQLにも対応しているようです.
対象コード
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
コード・リーディング
Q. FastAPIクラスとは?
A. Starletteを継承したクラス
つまりWebフレームワークということになります. Starletteがツールキットとも呼ばれるのはこのような拡張を意識した作りになっているからのようです.
# ...
class FastAPI(Starlette):
# ...
アプリのインスタンスでルーティングの機能などを提供します. ルートの設定はデコレータを使います. よく見るとコンストラクタの引数にroutesというのがありここに実際のルートが格納されるようです. 意外にもリスト型です.2
class FastAPI(Starlette):
def __init__(
...
routes: List[BaseRoute] = None,
...
) -> None:
BaseRouteはStarletteで定義されている抽象クラスです.
class BaseRoute:
def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
raise NotImplementedError() # pragma: no cover
def url_path_for(self, name: str, **path_params: str) -> URLPath:
raise NotImplementedError() # pragma: no cover
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
raise NotImplementedError() # pragma: no cover
またroutingモジュールのAPIRouterインスタンスをプロパティに持ちます. インスタンス化するときにroutesを渡します. このAPIRouterインスタンスが実際のルーティングの処理担うようです.
self.router: routing.APIRouter = routing.APIRouter(
routes, dependency_overrides_provider=self
)
Q. getは何をしているの?
A. 関数の再定義
実際にadd_api_routeはAPIRouterクラスのメソッドです. ルーターにルートを追加するという処理をしているわけです.
def api_route(
# ...
) -> Callable:
def decorator(func: Callable) -> Callable:
self.add_api_route(
path,
func,
# ...
)
return func
return decorator
add_api_routeの引数を前二つ見るとpath, endPointという聞き慣れた名前になっています. 両者はほぼ同じ意味です.
def add_api_route(self, path: str, endPoint: Callable, ...):
# ...
getの処理の流れをまとめておきましょう
- パス文字列をapp.getに渡す
- routerインスタンスのgetに引数を引き継ぐ
- routerインスタンスのapi_route関数に引数を引き継ぐ
- api_route関数からdecorator関数の定義を返す
def api_route(
self,
path: str,
...
) -> Callable:
def decorator(func: Callable) -> Callable:
self.add_api_route(
path,
func,
...
)
return func
return decorator
Q. デコレータとは?
A. 高階関数です.
高階関数は引数として関数をとったり戻り値として関数を返す関数のことです. 関数をとり関数を返しますが, デコレータは渡した関数プラスアルファ何かします. 例を見るのが早いでしょう.
def higher_order_function(func):
def wrapped(*arg, **kwarg):
print('🐶')
func()
print('🦜')
return wrapped
@higher_order_function
def saru():
print("🐵")
saru()
デコレータは高階関数の一種ですが, デコレータがやっているのは関数の定義を返すことです. つまり関数の再定義がデコレータが実際にしていることです. api_route関数は関数を再定義して返しています. しかしこの再定義した関数はadd_api_route関数でエンドポイントを登録する以外はもらった関数をそのまま返しています.3
getデコレータによって, @app.get("/")以下の行がapi_routeの内部で定義されたdecoratorで置き換えられていると思えばいいでしょう.
Q. APIRouterはどのように複数のルートを管理しているの?
A. リストに入れてる
つまりAPIRouterはコレクション型のラッパーです. add_api_routeでpathとendPointを登録しているのはすでに示した通りです.
def add_api_route(self, path: str, endpoint: Callable, ...) -> None:
route = self.route_class(path, endpoint=endpoint, ...)
self.routes.append(route)
まとめ
- FastAPIはStarletteを継承したクラス
- APIRouterはAPIRouteクラスのインスタンスを管理するコレクション型のラッパー