Python 製 Web フレームワークを Flask から FastAPI に変えた話
こんにちは、けんにぃです。ナビタイムジャパンで公共交通の時刻表を使ったサービス開発やリリースフローの改善を担当しています。
今回は Python 製の Web フレームワークとして FastAPI を導入した話をしようと思います。
Python 製の Web フレームワーク
Python には代表的な Web フレームワークが 2 つあります。
・Django: フルスタックフレームワーク
・Flask: マイクロフレームワーク
Django は大規模開発向け、Flask は小中規模開発向けと言われますが、今回開発したサーバは小規模なサーバだったため、以前は Flask で開発していました。
しかし、どちらのフレームワークを使う場合でも下記のような機能を使おうとするとプラグインやサードパーティの助けを借りる必要があります。
・OpenAPI
・JSON Schema
・GraphQL
・WebSocket
・タイプヒントを使ったバリデーション
・非同期処理
・CORS の設定
・リバースプロキシとの連携サポート
Django も Flask も近年登場したサーバサイドの技術や Python 3 の新機能に対するネイティブサポートがちょっと弱いです。
これらの機能を補うためにプラグインやサードパーティ製ライブラリを使ってきましたが、バージョンアップによる破壊的変更が起こったりして動かなくなると、どのライブラリが原因なのかを調査するのに結構時間がかかってしまいました。
そのような経験をすると モダンなフレームワークが欲しくなってくるのです。
Django, Flask より後発の Web フレームワーク
Python の Web フレームワークは毎年のように新たなものが登場しており、どのフレームワークも設計が Flask によく似ているという特徴があります。
新しいフレームワークの導入を検討するにあたり、下記のような点で色々と調べてみました。
・Flask 並にシンプルな設計
・前述の機能要件を満たしている
・GitHub スター数が多い
・ドキュメントが豊富
Python の Web フレームワークにおける GitHub スター数を調べてみると、こんな感じでした(2020年8月25日時点)。
1. Flask: 51,800
2. Django: 51,500
3. FastAPI: 20,100
4. Tornado: 19,400
5. Django REST Framework: 18,600
この中で 3 位に挙がっている FastAPI に注目しました。
FastAPI は 2018 年後半に登場したフレームワークで、上記の中では最も後発に当たります。それにも関わらずこのスター数なので、かなりの勢いで注目されています。
FastAPI が他のフレームワークと一番異なると感じた点は、機能面の差分というよりもドキュメントの充実さと直感的に記述できる設計です。
同等の機能を提供できていても開発者が使い方をすぐに理解できるとは限りません。しかし FastAPI はやりたいことを実現するまでの時間が非常に低コストだと感じます。この Developer Experience の手厚いサポートが人気につながっているのではないかと思います。
以上のことから Flask で実装されたサーバを FastAPI で書き直すことにしました。
FastAPI のインストール
FastAPI のインストールは下記のように行います。
$ pip install fastapi uvicorn
Uvicorn は Python のサーバを開発する時に使われる WSGI (Web サーバとWeb フレームワークの I/F を繋ぎこむ仕組み)の精神的後継と呼ばれる ASGI の実装の一つです。
Flask でサーバ開発をする時は WSGI サーバとして Gunicorn などを使うと思いますが Uvicorn はその非同期対応版と思えば大丈夫です。
それでは FastAPI の使い方を Flask と比較しながら見ていきたいと思います。Flask, Gunicorn も使えるように下記もインストールしておきます。
$ pip install flask gunicorn
ルーティングの定義
Flask で書かれたルーティングの定義を FastAPI で書き直すとこんな感じになります。
Flask
from flask import Flask
app = Flask(__name__)
@app.route("/hello")
def hello():
return {"Hello": "World!"}
FastAPI
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
def hello():
return {"Hello": "World!"}
かなりよく似た実装になっています。Flask と記述の仕方が似ているため FastAPI への移行は結構簡単でした。
サーバの起動
Flask, FastAPI のソースコードをそれぞれ flask_app.py, fastapi_app.py というファイル名で保存しておきます。サーバの起動方法は下記のとおりです。
Flask
$ gunicorn flask_app:app
[2020-08-26 15:21:09 +0900] [27823] [INFO] Starting gunicorn 20.0.4
[2020-08-26 15:21:09 +0900] [27823] [INFO] Listening at: http://127.0.0.1:8000 (27823)
[2020-08-26 15:21:09 +0900] [27823] [INFO] Using worker: sync
[2020-08-26 15:21:09 +0900] [27826] [INFO] Booting worker with pid: 27826
FastAPI
$ uvicorn fastapi_app:app
INFO: Started server process [14366]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
起動方法もよく似ています。ちなみに FastAPI は WSGI に繋ぎこむことも出来るので、Gunicorn で起動することも出来ます。
ドキュメンテーション
さて、ここから FastAPI が真骨頂を発揮することになります。FastAPI には Swagger UI と ReDoc の両スタイルのドキュメントを自動で生成してくれる機能があります。
先ほどの FastAPI サーバを起動した後、下記の URL にアクセスするとキレイなドキュメントページを見ることが出来ます。
Swagger UI: http://localhost:8000/docs
ReDoc: http://localhost:8000/redoc
Flask にはドキュメントを自動で生成する機能はありません。正直この機能を使うためだけでも FastAPI を導入する価値があると思います。
リクエストパラメータ
Flask を使用しているとリクエストパラメータの取得の仕方が分からず、悩んだ方は多いのではないでしょうか?私もその一人です。
下記のような API を作る場合を考えます。
POST /users/{name}?age={age}
{
"country": {country}
}
各パラメータはそれぞれリクエストの異なる箇所で指定されます。
・{name} : パスパラメータ
・{age} : クエリパラメータ
・{country} : ボディ
Flask はパラメータの指定箇所によって値の受け取り方が変わるため、初めて Flask を使う人は混乱するのではないかと思います。FastAPI はそれをとても直感的に解決してくれます。
それでは書き方について見ていきます(エラーチェックは省略しています)。
Flask
from flask import Flask, request
app = Flask(__name__)
@app.route("/users/<name>", methods=["POST"])
def create_user(name):
age = request.args.get("age", type=int)
body = request.json
return {
"age": age,
"name": name,
"country": body["country"],
}
FastAPI
from fastapi import FastAPI, Query, Body
app = FastAPI()
@app.post("/users/{name}")
def create_user(name: str, age: int = Query(None), body: dict = Body(None)):
return {
"age": age,
"name": name,
"country": body["country"],
}
FastAPI の場合はパスパラメータ・クエリパラメータ・ボディの全てが関数の引数として定義できます。
またパラメータの型の指定方法も FastAPI の方が Python 3 で登場した型ヒントを使って実現されている分、直感的で分かりやすいです。
Flask でクエリパラメータやボディを受け取る方法を調べるのに結構時間をかけた記憶があります。この辺のよく使う機能を実現する方法に容易にたどりつける設計になっているかどうかが Developer Experience に大きく影響しているように感じます。
Pydantic によるバリデーション
FastAPI は Pydantic という型バリデーションライブラリを同梱しており、これを使ってリクエスト・レスポンスのパラメータの型を検証することが出来ます。
試しに前述の API に対するレスポンスのバリデーションをやってみようと思います。リクエストのバリデーションも同じやり方で出来ます。
まず下記のインポートを行います。
from pydantic import BaseModel, Field
次にレスポンスとして返ってくる JSON をバリデーションするためのスキーマクラスを定義します。
class User(BaseModel):
age: int = Field(description="年齢", ge=0)
name: str = Field(description="氏名")
country: str = Field(description="出身国")
このクラスを使うことでレスポンスの JSON が "age", "name", "country" フィールドを持っており、 "age" は 0 以上の整数であるというバリデーションを行うことが出来るようになります。
このクラスをデコレータで指定します。
@app.post("/users/{name}", response_model=User)
こうすることで User スキーマに合致しないレスポンスを返そうとするとエラーが返るようになります。
ソースコード全体は次のようになります。
from fastapi import FastAPI, Query, Body
from pydantic import BaseModel, Field
app = FastAPI()
class User(BaseModel):
age: int = Field(description="年齢", ge=0)
name: str = Field(description="氏名")
country: str = Field(description="出身国")
@app.post("/users/{name}", response_model=User)
def create_user(name: str, age: int = Query(None), body: dict = Body(None)):
return {
"age": age,
"name": name,
"country": body["country"],
}
スキーマの説明は自動でドキュメントにも反映されます。
まとめ
Flask はマイクロフレームワークと言われますが、FastAPI は最近のニーズに合わせてマイクロフレームワークを再定義したかのようなバランスの良い設計になっています。
FastAPI の魅力はまだまだたくさんあります。他の機能も知りたいという方はぜひ公式ドキュメントを御覧ください。