結論: FastAPIとDependency Injectorを併用する時は、Dependency Injectorのバージョンを最新版あるいはv4.46以降にアップグレードしましょう
今年の2月にv4.46がリリースされて解決した話ですが、比較的新しくかつマニアックな内容でChatGPTやGeminiに聞いてもズバッと解決してくれなかったのと、ネット上では昔の実装方法が今も主流のようなので一応ここに書いておこうかなと。
そもそもPythonのプロジェクトでDIっている?っていう考えもあるし、プロダクションで非機能要件が厳しいサービスだとPythonでAPI作らないと思うので、比較的小さいサービスとかプライベートワークでの話になるかもしれません。自分も仕事では機械学習領域はPythonですがそれ以外では別言語を使います。
問題
PythonでWebAPIサーバを作るときにデファクトスタンダードになっている FastAPI ですが、依存性注入(Dependency Injection, DI)を扱うモジュールとしてこちらもよく使われる Dependency Injector と併用した際の型指定についてこれまで問題がありました。
Pythonのリンター/フォーマッターとして近年よく使われるRuffでチェックするときにそれがエラーとなってしまい、該当ルールを無視する設定を常に入れる必要がありました。
以下例を挙げます。クレジットカード情報を取得するためのServiceをDIで管理する実装例です。
* DBコンテナの定義 (dependencies.py)
from dependency_injector import containers, providers # 他 importは省略 class Container(containers.DeclarativeContainer): wiring_config = containers.WiringConfiguration(modules=[".controller"]) config = providers.Configuration() session = providers.Resource( get_session, ## PostgreSQL用のDBセッションを取得する関数 dsn=config.dsn, echo=config.debug, ) card_repository = providers.Factory( PostgresCardRepository, session=session, ) card_service = providers.Factory( CardService, repository=card_repository, )
* コントローラーで依存性注入 (controller.py)
from dependency_injector.wiring import Provide, inject from fastapi import APIRouter, Depends, FastAPI # 他 importは省略 @router.get("/card", description="Get all cards") @inject async def get_card_all( service: CardService = Depends(Provide[Container.card_service]) ) -> list[Card]: return await service.get_card_all()
上記のデフォルト値を使った書き方で動作はするのですがRuffのチェックに引っかかってしまいます。Depends
をデフォルト値で使うな、代わりにtyping.Annoteted
を使えというルールです。
実はFastAPI公式もデフォルト値よりtyping.Annotated
を使おうと言っており、理由についても以下公式ページで説明されています。
ところが、代わりにtyping.Annotated
で書き換えようとするとDependency Infector v4.46未満だとエラーで動作しない状況でした。
AttributeError: 'Provide' object has no attribute 'get_card_all'
なので、仕方なくLintチェックでFAST002ルールを無視しつつ、デフォルト値を設定して実装する必要があったわけです。
補足: typing.Annotatedについて
Pythonのtypingモジュールに密かに入っているAnnotetedについて整理します。これはPythonの型ヒント(Type Hint)に追加のメタデータを付与するための機能でPython 3.9で導入されています。3.9はだいぶ昔だと思うのですが、現場レベルでは認知度が低い気もします。
from typing import Annotated 変数名: Annotated[型, メタデータ1, メタデータ2, ...]
Annotated
の主な目的は、型ヒントに付随する情報を拡張し、その情報を利用するツールやライブラリとの連携を強化することです。注意点としては、Annotated
単体ではPythonの実行時に何らかの動作をするわけではなく、あくまで型ヒントにメタデータを付けるだけです。このメタデータをどのように解釈して利用するかは外部ツールやライブラリ(FastAPI, Pydanticなど)に依存します。
FastAPIの場合は、以下例のように型だけでなく値の範囲を設定してバリデーションすることができます。
Annotated[str | None, Query(min_length=3, max_length=50)]
解決
長らく解決していなかった問題ですが今年2月に公式対応されました。
v4.46以降ならtyping.Annotated
を使って正しくDIが機能するようになり、Ruffのルール(FAST002,B008)も有効化にできるようになりました。
@router.get("/card", description="Get all cards") @inject async def get_card_all( service: Annotated[CardService, Depends(Provide[Container.card_service])] ) -> list[Card]: return await service.get_card_all()
とはいえ、機械学習系ライブラリ・フレームワークはデフォルト値だらけですし、加えて**kwargs
だらけでツライものが多い上にそれを変えるモチベーションも無いと思うので、typing.Annotated
を使う意義に理解ある人は対応すればいいだけかなとも思いました。