TT Lab
はじめる
学ぶ 学習パス コース

FastAPI — 型がそのまま契約だ

全行をためずにJSON Linesを出力する:設計原理

TT Labで続きを見る

一言でいうと

遅延生成・行の境界・公開フィールド・出力の上限を、HTTPのストリームにつなげます。

なぜ必要なのか

管理者がすべての注文をダウンロードすると、サーバーのメモリが急増しました。export関数が、JSONを作る前に、すべての行をリストに集めたからです。行単位の出力に変えたものの、本文の中の改行が実際の行の境界を壊し、内部の原価もそのまま出ていきました。ストリーミングは、戻り値の型を1つ変える作業ではなく、遅延評価と表現の契約を一緒に決める作業です。

どう動くのか

各行を検証し、公開フィールドだけを射影したあと、JSONにエンコードして、行の末尾に改行を1つ付けます。ジェネレーターは、呼び出しただけでは入力を消費せず、1つの項目を取り出すと、1行だけ進みます。出力の上限は、boolを除く整数で検証します。最後に、FastAPIのStreamingResponseにつなげて、Content-Typeと、ダウンロードした行を、もう一度パースします。

입력 iterator → 한 행 검증 → 공개 투영 → JSON + 개행 → StreamingResponse

契約を読んで失敗を予測するワークシート

以下は、実装をまるごと暗記するための答案ではなく、ステップごとのコードレビューです。各変更の断片は、意図的に契約を壊しています。変更したあとでも、正常な例は通ることがある点に注意してください。実行する前に、どの入力・例外・状態を観測すれば違いが表に出るかを予想し、実装したあとで、その予想と結果を比べます。

1. 行の契約を検証する

validate_row(row)は、dictで、idがboolを除く正のint、nameが空でないstrのときに、rowを返します。それ以外は、ValueErrorです。追加の内部フィールドは許可します。

判断の根拠: boolと数値を区別し、空の名前をエラーとして扱います。

レビューする誤った変更の断片:

not isinstance(row.get("id"), int)

この断片が入った関数の公開契約と比べてみてください。成功例1つでは区別できないなら、拒否されるべき入力や、失敗したあとの状態を観測の対象に選びます。

2. 公開する行だけを作る

project(row)は、validate_rowのあとで、idとnameだけを持つ新しいdictを返します。元の内部フィールドは、そのまま保存します。

判断の根拠: エクスポートの経路にも、通常のAPIと同じ公開フィールドのポリシーを適用する必要があります。

レビューする誤った変更の断片:

"name":row["name"], "internal_cost":row.get("internal_cost")}

この断片が入った関数の公開契約と比べてみてください。成功例1つでは区別できないなら、拒否されるべき入力や、失敗したあとの状態を観測の対象に選びます。

3. 行の境界を保ちながらエンコードする

encode_line(row)は、projectの結果を、ensure_ascii=False、separators=(',',':')、sort_keys=TrueでJSONエンコードして、最後に' 'を1つ付けたstrです。nameの中の改行は、JSONのエスケープでなければなりません。

判断の根拠: 文字列の連結でJSONを作ると、引用符と改行で形式が壊れます。

レビューする誤った変更の断片:

ensure_ascii=True

この断片が入った関数の公開契約と比べてみてください。成功例1つでは区別できないなら、拒否されるべき入力や、失敗したあとの状態を観測の対象に選びます。

4. 出力の件数の上限を検証する

validate_max(value)は、boolを除くintの1–1000だけをそのまま返し、それ以外はValueErrorです。

判断の根拠: 無限の入力を、誤って最後まで読まないように、呼び出し側に上限を要求します。

レビューする誤った変更の断片:

<= 1001

この断片が入った関数の公開契約と比べてみてください。成功例1つでは区別できないなら、拒否されるべき入力や、失敗したあとの状態を観測の対象に選びます。

5. 必要な行だけを消費する

take_rows(rows, maximum)は、isliceなどで最大maximum個だけを遅延して返すiteratorです。呼び出したときにmaximumを検証し、1回nextすると、入力を1回だけ消費します。

判断の根拠: list(rows)に変換した瞬間、無限の入力や大容量の入力を処理できなくなります。

レビューする誤った変更の断片:

islice(list(rows), validate_max(maximum))

この断片が入った関数の公開契約と比べてみてください。成功例1つでは区別できないなら、拒否されるべき入力や、失敗したあとの状態を観測の対象に選びます。

6. 行を遅延してシリアライズする

json_lines(rows, maximum=100)は、take_rowsから受け取った行ごとに、encode_lineをyieldします。すべてをまとめた文字列やリストは返しません。

判断の根拠: オブジェクトの選択と、表現の変換を、それぞれ遅延のステップとして維持します。

レビューする誤った変更の断片:

for row in list(take_rows(rows, maximum)):

この断片が入った関数の公開契約と比べてみてください。成功例1つでは区別できないなら、拒否されるべき入力や、失敗したあとの状態を観測の対象に選びます。

7. ダウンロードした行を再び検証する

decode_lines(text)は、splitlinesの空でない各行を、json.loadsしたあとでvalidate_rowして、リストとして返します。空の文字列は[]、空である途中の行は、ValueErrorです。

判断の根拠: 空のファイルと、形式が壊れた空のレコードを区別します。

レビューする誤った変更の断片:

continue

この断片が入った関数の公開契約と比べてみてください。成功例1つでは区別できないなら、拒否されるべき入力や、失敗したあとの状態を観測の対象に選びます。

8. HTTPのダウンロードを完成させる

create_app(rows)は、GET /exportで、json_lines(rows, 100)を、application/x-ndjsonのStreamingResponseとして返します。rowsは、再び走査できるリストです。内部フィールドがなく、各行の内容と順序を保存する必要があります。

判断の根拠: Content-Typeだけをストリーミングとして書いて、内部では全体を集めていないかを、ジェネレーターのテストと一緒に確認します。

レビューする誤った変更の断片:

media_type="application/json"

この断片が入った関数の公開契約と比べてみてください。成功例1つでは区別できないなら、拒否されるべき入力や、失敗したあとの状態を観測の対象に選びます。

現場での姿

TestClientは、レスポンスをバッファリングするので、ネットワークの最初のバイトの遅延や、メモリの上限全体は、証明しません。遅延評価をしているかどうかは、別のカウント用のジェネレーターで検査します。ストリームが始まったあとで不正な行に出会うと、通常のエラーのJSONでステータスを変えるのが難しくなります。実際のサービスは、事前の検証・行ごとのエラーの形式・中断のポリシーのうち、何を選ぶかを決める必要があります。

次のラボですること

8つのステップが、1つの実行可能な成果物につながります。行の契約を検証する → 公開する行だけを作る → 行の境界を保ちながらエンコードする → 出力の件数の上限を検証する → 必要な行だけを消費する → 行を遅延してシリアライズする → ダウンロードした行を再び検証する → HTTPのダウンロードを完成させる、という流れです。

各ステップは、関数やファイルが存在するという事実ではなく、実際の戻り値・例外・状態の変化を検査します。正解を見たあとには、わざと境界の比較や後始末のコードを変えて、どのテストが失敗するかを確認してください。前のテストが次のステップでも維持される理由を説明し、このラボが保証しない本番の条件を1つ書いてみてください。