🍞

Flaskユーザー向けFastAPIの使い方まとめ

2020/09/27に公開

はじめに

本記事は、「FastAPI for Flask Users」を翻訳したものになります。
この記事では、FlaskとFastAPIを比較しながらわかりやすく解説した記事なので、許可をとって翻訳することにしました。

以下、本文になります。

記事本文

Flaskは機械学習プロジェクトでのAPI開発の事実上の選択肢となっていますが、
FastAPIと呼ばれる新しいフレームワークがあり、コミュニティーから多くの支持を得ています。

最近、Flaskで作成された本番プロジェクトを移行することになり、FastAPIを試してみることにしました。
FastAPIFlaskライクな文法で記述されているので、移し替えが非常に簡単で、わずか数時間で起動して実行できました。

自動データ検証ドキュメント生成、およびpydanticスキーマpythonタイピングなどのベストプラクティスの追加により、これは将来のプロジェクトにとって強力な選択肢となります。

本稿では、FlaskとFastAPIの両方でさまざまな一般的なユースケースの実装を対比して紹介します。

準備

バージョン情報:
本稿の執筆時点では、Flaskのバージョンは1.1.2で、FastAPIのバージョンは0.58.1です。

インストール

FlaskFastAPIの両方がPyPI(通常のpipによるインストール)で利用できます。
condaを使用する場合、conda-forgeのチャンネルを利用する必要がありますが、FastAPIは、Flaskのデフォルトチャネルで利用することができます。

通常のインストール

Flask

$ pip install flask

FastAPI

$ pip install fastapi uvicorn

condaを使ったインストール

Flask

$ conda install flask

FastAPI

$ conda install fastapi uvicorn -c conda-forge

比較

Hello World

Flask

# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return {'hello': 'world'}

if __name__ == '__main__':
    app.run()

上記を、以下のコマンドを使用して実行できます。デフォルトではポート5000で実行できます。

$ python app.py

FastAPI

# app.py
import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get('/')
def home():
    return {'hello': 'world'}

if __name__ == '__main__':
    uvicorn.run(app)

FastAPIは、デフォルトポート8000で次のように実行できます。

$ python app.py

本番サーバ

Flask

# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return {'hello': 'world'}

if __name__ == '__main__':
    app.run()

本番サーバーの場合、Flaskでは一般にgunicornを利用します。

$ gunicorn app:app

次のコマンドを実行して、ホットリロードモードで起動することもできます。

$ gunicorn app:app --reload

FastAPI

# app.py
import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get('/')
def home():
    return {'hello': 'world'}

if __name__ == '__main__':
    uvicorn.run(app)

FastAPIは、本番対応サーバへのサービス提供を行うuvicornを使い、次のように実行します。

$ uvicorn app:app

次のコマンドを実行して、ホットリロードモードで起動することもできます。

$ uvicorn app:app --reload

HTTPメソッド

Flask

@app.route('/', methods=['POST'])
def example():
    ...

FastAPI

@app.post('/')
def example():
    ...

FastAPIでは、HTTPメソッドごとに個別のデコレータメソッドがあります。

@app.get('/')
@app.put('/')
@app.patch('/')
@app.delete('/')

URL変数

URLからユーザーIDを取得します。 /users/1
その後、ユーザーIDをユーザーに返します。

Flask

@app.route('/users/<int:user_id>')
def get_user_details(user_id):
    return {'user_id': user_id}

FastAPI

FastAPIでは、Pythonの型ヒント(Type Hints) を使用してすべてのデータ型を指定します。例えば、ここではuser_idが整数であることを指定しています。 URLパスの変数もf-stringsと同様に指定できます。

@app.get('/users/{user_id}')
def get_user_details(user_id: int):
    return {'user_id': user_id}

クエリストリング

ユーザーがURLにクエリストリング?q=abcを指定したい場合は次のようにします。

Flask

# app.py
from flask import request

@app.route('/search')
def search():
    query = request.args.get('q')
    return {'query': query}

FastAPI

# app.py
@app.get('/search')
def search(q: str):
    return {'query': q}

JSON POSTリクエスト

textをkey、アッパーケースをvalueとするJSONをPOSTリクエストで送信して、ローワーケースのvalueを取得する簡単な例を見てみましょう。

# 指定するリクエスト
{"text": "HELLO"}

# 期待するレスポンス
{"text": "hello"}

Flask

# app.py
from flask import request

@app.route('/lowercase', methods=['POST'])
def lower_case():
    text = request.json.get('text')
    return {'text': text.lower()}

FastAPI

Flaskから機能を複製する場合、FastAPIでは次のように実行できます。

# app.py
from typing import Dict

@app.post('/lowercase')
def lower_case(json_data: Dict):
    text = json_data.get('text')
    return {'text': text.lower()}

しかし、ここでFastAPIではpydanticスキーマーという新しい概念を導入して、リクエストされたJSONデータをマップするスキーマーを作成します。pydanticを使い上記の例をリファクタリングするとこのようになります。

# app.py
from pydantic import BaseModel

class Sentence(BaseModel):
    text: str

@app.post('/lowercase')
def lower_case(sentence: Sentence):
    return {'text': sentence.text.lower()}

このように、辞書を取得する代わりに、JSONデータはスキーマSentenceのオブジェクトに変換されます。

また、sentence.textのようなデータ属性を使ってデータにアクセスすることができます。これにより、データ型の自動検証も可能になります。

ユーザーが文字列以外のデータを送ろうとすると、自動生成されたバリデーションエラーが発生します。

無効なリクエストの例

例では、stringではない値、nullを指定しました。

{"text": null}

自動応答

{
    "detail": [
        {
            "loc": [
                "body",
                "text"
            ],
            "msg": "none is not an allowed value",
            "type": "type_error.none.not_allowed"
        }
    ]
}

ファイルのアップロード

アップロードされたファイル名を返すAPIを作成しましょう。
ファイルのアップロード時に使用されるキーはfileです。

Flask

Flaskを使用すると、リクエストオブジェクトを介してアップロードされたファイルにアクセスできます。

# app.py
from flask import Flask, request
app = Flask(__name__)

@app.route('/upload', methods=['POST'])
def upload_file():
    file = request.files.get('file')
    return {'name': file.filename}

FastAPI

FastAPIは、関数にインポートしたFile型を引数に指定します。

# app.py
from fastapi import FastAPI, UploadFile, File

app = FastAPI()

@app.post('/upload')
def upload_file(file: UploadFile = File(...)):
    return {'name': file.filename}

フォームの送信

以下に示すように定義されたテキストフォームのフィールドにアクセスして値を返し​​ます。

<input name='city' type='text'>

Flask

Flaskでは、リクエストオブジェクトを介してフォームのフィールドにアクセスできます。

# app.py
from flask import Flask, request
app = Flask(__name__)

@app.route('/submit', methods=['POST'])
def echo():
    city = request.form.get('city')
    return {'city': city}

FastAPI

インポートしたForm型を引数に指定して、フォームフィールドのキーとデータ型を定義します。

# app.py
from fastapi import FastAPI, Form
app = FastAPI()

@app.post('/submit')
def echo(city: str = Form(...)):
    return {'city': city}

以下に示すように、フォームフィールドをオプションにすることもできます。

# app.py
from typing import Optional

@app.post('/submit')
def echo(city: Optional[str] = Form(None)):
    return {'city': city}

同様に、以下に示すように、フォームフィールドに初期値を設定できます。

# app.py
@app.post('/submit')
def echo(city: Optional[str] = Form('Paris')):
    return {'city': city}

クッキー

nameリクエストから呼び出されたCookieにアクセスしたいと思います。

Flask

Flaskを使用すると、リクエストオブジェクトを介してCookieにアクセスできます。

# app.py
from flask import Flask, request
app = Flask(__name__)

@app.route('/profile')
def profile():
    name = request.cookies.get('name')
    return {'name': name}

FastAPI

インポートしたCookie型を引数に指定して、Cookieのキーnameを定義します。

# app.py
from fastapi import FastAPI, Cookie
app = FastAPI()

@app.get('/profile')
def profile(name = Cookie(None)):
    return {'name': name}

Modular Views

単一のapp.pyからのビューを別々のファイルに移動し、アプリケーションの機能を分解したいと思います。
肥大化したプロジェクトを整理することができるためFlaskではこの機能が推奨されています。

Flask

- app.py
- views
  - user.py

Flaskでは、Blueprintと呼ばれる概念を使用してこれを管理します。
まず、クライアント側のBlueprintを次のように作成します。

# views/user.py
from flask import Blueprint
user_blueprint = Blueprint('user', __name__)

@user_blueprint.route('/users')
def list_users():
    return {'users': ['a', 'b', 'c']}

次に、このビューをメインapp.pyファイルに登録します。

# app.py
from flask import Flask
from views.user import user_blueprint

app = Flask(__name__)
app.register_blueprint(user_blueprint)

FastAPI

- app.py
- routers
  - user.py

FastAPIでは、Blueprintに相当するものはルーターと呼ばれます。
例では、次のようにユーザールーターを作成します。

# routers/user.py
from fastapi import APIRouter
router = APIRouter()

@router.get('/users')
def list_users():
    return {'users': ['a', 'b', 'c']}

次に、このルーターをメインのアプリオブジェクトに次のように接続します。

# app.py
from fastapi import FastAPI
from routers import user

app = FastAPI()
app.include_router(user.router)

データの型検証

Flask

Flaskは、デフォルトで入力データ検証機能を提供していません。カスタム検証ロジックを作成するか、marshmalllowpydantic などのライブラリを使用するのが一般的な方法です。

FastAPI

FastAPIは、pydanticをフレームワークにラップし、pydanticスキーマPython型ヒントの組み合わせを使用するだけでデータ検証を可能にします。

# app.py
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    name: str
    age: int

@app.post('/users')
def save_user(user: User):
    return {'name': user.name,
            'age': user.age}

このコードは自動検証を実行してnameが文字列でageが整数であることを確認します。他のデータ型が送信されると、関連するメッセージとともに検証エラーが自動生成されます。

以下では、一般的なユースケースのpydanticスキーマの例を示します。

例1:キーと値のペア

{
  "name": "Isaac",
  "age": 60
}
from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

例2:リスト

{
  "series": ["GOT", "Dark", "Mr. Robot"]
}
from pydantic import BaseModel
from typing import List

class Metadata(BaseModel):
    series: List[str]

例3:ネストされたオブジェクト

{
  "users": [
    {
      "name": "xyz",
      "age": 25
    },
    {
      "name": "abc",
      "age": 30
    }
  ],
  "group": "Group A"
}
from pydantic import BaseModel
from typing import List

class User(BaseModel):
    name: str
    age: int

class UserGroup(BaseModel):
    users: List[User]
    group: str

Pythonタイプのヒントについて詳しくは、こちらをご覧ください

自動ドキュメント

Flask

Flaskには、ドキュメント生成用の組み込み機能はありません。そのギャップを埋めるためにflask-swaggerflask-restfulなどの拡張機能がありますが、ワークフローは比較的複雑です。

FastAPI

FastAPIでは、直感的でブラウザから操作可能なSwaggerドキュメントのエンドポイント/docsReDocドキュメントのエンドポイント/redocが用意されており、これらは自動的に生成されます。

例えば、以下のようなユーザーが検索したものを返す簡単なAPIがあるとします。

# app.py
from fastapi import FastAPI

app = FastAPI()

@app.get('/search')
def search(q: str):
    return {'query': q}

Swaggerドキュメント

サーバーを起動してエンドポイントhttp://127.0.0.1:8000/docsにアクセスすると、自動生成されたSwaggerドキュメントが表示されます。

ブラウザから直接APIを試すことができます。

ReDocドキュメント

同様に、サーバー起動後エンドポイントhttp://127.0.0.01:8000/redocにアクセスすると、自動生成されたリファレンスドキュメントが表示されます。
ここでは、パラメータ、リクエスト形式、レスポンス形式、ステータスコードに関する情報があります。

オリジン間リソース共有(CORS)

異なるオリジンからのアクセスを許可する方法を示します。

Flask

Flaskは、デフォルトでCORSのサポートを提供していません。以下に示すように、CORSを構成するには、flask-corsなどの拡張機能を使用する必要があります。

# app.py
from flask import Flask
from flask_cors import CORS

app_ = Flask(__name__)
CORS(app_)

FastAPI

FastAPIは、CORSに対応するための組み込みミドルウェアが提供されています。
以下に、任意のoriginがAPIにアクセスすることを許可するCORSの例を示します。

# app.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

*を任意のドメインに置き換えることでアクセスを制限することができます。

おわりに

FastAPIは、ベストプラクティスが組み込まれた堅牢なAPIを構築するための、Flaskの優れた代替手段です。詳細については、ドキュメントを参照してください。

参考文献

Discussion