93
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FastAPIAdvent Calendar 2020

Day 3

FastAPIでStarletteとPydanticはどのように使われているか

Last updated at Posted at 2020-12-02

FastAPIは:

主な特徴:

  • 高速: NodeJS や Go 並みのとても高いパフォーマンス (Starlette と Pydantic のおかげです)。 最も高速な Python フレームワークの一つです.

....

FastAPI は巨人の肩の上に立っています。

  • Web の部分はStarlette
  • データの部分はPydantic

と、ドキュメントで謳っています。

肩の上に立つって何?という疑問がわいたので、どのように使われているか調べました。
StarletteとPydanticについて、以下の流れで紹介します。

  • ASGI server
  • ASGI Framework/Toolkit
  • Data validation/serialization

ASGI server

まず、FastAPIの「Web」に関する特徴のひとつである「ASGI」についてみていきます。
ASGI serverは、ASGI specificationに従ってHTTPリクエストをさばくサーバーのことを指します。

以下のようなライブラリがASGI serverにあたります。

処理のイメージをつかむために、上記2つのASGI serverのドキュメントに記載されているhelloworldのサンプルを比べてみます。

Uvicornのサンプル

Uvicorn - helloworld

uvicorn_example.py
async def app(scope, receive, send):
    assert scope['type'] == 'http'

    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ],
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, world!',
    })

サーバー起動:

uvicorn uvicorn_example:app

ブラウザで http://127.0.0.1:8000/ にアクセスすると、Hello, world! と表示されます。

Hypercornのサンプル

Hypercorn - Helloworld

hypercorn_example.py
async def app(scope, receive, send):
    if scope["type"] != "http":
        raise Exception("Only the HTTP protocol is supported")

    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            (b'content-type', b'text/plain'),
            (b'content-length', b'5'),
        ],
    })
    await send({
        'type': 'http.response.body',
        'body': b'hello',
    })

サーバー起動:

hypercorn hypercorn_example:app

ブラウザで http://127.0.0.1:8000/ にアクセスすると、hello と表示されます。

ASGI serverの役割

僅かな例の差はありますが、必要な関数の引数とその使い方は全く同じものです (これは全くの偶然ではなく、ASGI specificationsに従っているためです)。この例で想像できるように、ASGI serverは:

  • HTTPリクエストを受け取り、async関数にリクエスト情報を渡して、非同期処理をはしらせてレスポンスを返す
  • ライブラリに依らず、インターフェースは共通
  • アプリケーション部分 (リクエストにどのようにレスポンスするか) は使い手に任せる

という特徴があります。

つまり、ASGI serverを利用することで、HTTPリクエストをどう処理するのか気にすることなく、以下の3つを駆使したアプリケーションの実装に注力できるようになります。

  • scope - 受信接続情報を含んだ辞書
  • receive - ASGI serverから受信メッセージを受け取るためのチャンネル
  • send - ASGI serverへ送信メッセージを送るためのチャンネル

また、FastAPIを使っていると、どこにもループ処理がでてきませんが、実際にはサーバーは繰り返しリクエストを受け取り、レスポンスを返しています。これはFastAPIで定義しているのが、上記のアプリケーション (app) 関数に該当しており、ASGI serverが上記の様なアプリケーション関数をループして呼んでくれているから成立しています。

パフォーマンスについて

UvicornやHypercornはuvloopという非同期処理実行のためのライブラリを使用しています。Cythonで実装されており、ビルトインのasyncio event loop よりも2 ~ 4倍高速らしいです。(参考)

FastAPIが高速であるのは、uvloopによる高速な非同期処理が実現されているところが大きいようです。

複数のリクエストを非同期処理 (async) でまとめてさばいているおかげで速くなっているので、1つのリクエストを返す速さだけで比べると必ずしも優位な結果はでないということに注意して下さい。

ASGI Framework/toolkit

ASGI serverだけでもWeb serverとして機能しますが、一般的なWebアプリケーションで使う場合、多くの処理を記述する必要があります。

例えば、

  • ルーティング
  • エラーハンドリング
  • ミドルウェアの追加
  • など

また、基本的な処理である「リクエストの受信/レスポンスの送信」でさえ、データ量が多い場合は一回の通信では送り切れないので、アプリケーション内で繰り返し分割して送受信する必要があり(個人的には)面倒です。

上記のような汎用的な処理が簡単に利用できるものとして、ASGI Framework/toolkit が用いられます。

Starlette

FastAPIが乗っている巨人のひとつである、StarletteはASGI Framework/toolkitの一つです。

以下のような特徴があります。処理が簡単になるだけでなく、効率的に実行するように非同期処理が工夫されており、高速です。

It is production-ready, and gives you the following:

  • Seriously impressive performance.
  • WebSocket support.
  • GraphQL support.
  • In-process background tasks.
  • Startup and shutdown events.
  • Test client built on requests.
  • CORS, GZip, Static Files, Streaming responses.
  • Session and Cookie support.
  • 100% test coverage.
  • 100% type annotated codebase.
  • Zero hard dependencies.

starlette_example.py
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route


async def homepage(request):
    return JSONResponse({'hello': 'world'})

routes = [
    Route("/", endpoint=homepage)
]

app = Starlette(debug=True, routes=routes)

サーバー起動:

uvicorn starlette_example:app

ブラウザで http://127.0.0.1:8000/ にアクセスすると、{"hello":"world"} と表示されます。

Pythonの(Flaskライクな)Webフレームワークをさわったことがあれば、どこかで見たことあるような記法だと思います。

ASGI serverとの関係

ASGI serverの例にあげたコードからあまりにもかけ離れてしまいました。

しかし、ASGI serverは(Uvicornのドキュメントにあるように)、以下の様なインスタンス形式のアプリケーションも実行可能です:

class App:
    async def __call__(self, scope, receive, send):
        assert scope['type'] == 'http'
        ...

app = App()

Starletteはこちらの記法をとっています。Starletteの例では、__call__ メソッドを直接いじらずに、インスタンス生成時に homepage 関数を (ルーティングの定義と共に) アプリケーションクラスにわたすだけで、ルーティングから関数実行までやってくれるようになっています。(アプリケーションクラスはStarlette クラスとして実装されています。)

このように、インスタンス変数やメソッドを利用して、柔軟にアプリケーションが構築できるような実装がされています。

また、Starletteは以下の様に、ASGI serverのためのToolkit (エコシステム) としても使用できます。

from starlette.responses import PlainTextResponse


async def app(scope, receive, send):
    assert scope['type'] == 'http'
    response = PlainTextResponse('Hello, world!')
    await response(scope, receive, send)

FastAPIがStarletteの肩の上に立っているというのは、上記の様なStarlette Framework/Toolkitを利用していることを指していると言えます。一番大きい所でいくと、FastAPIにおけるアプリケーションクラスであるFastAPI クラスは、Starlette クラスを継承しています。

また、バックグラウンドプロセステストクライアントなどはStarlette エコシステムをそのまま利用しています。

Data validation/serialization

次は、「データ」に関してです。

Data validationは文字通りデータの検証を指します。指定したデータが含まれるか、データの型が指定した型であるか、指定した値の範囲にあるかなど確認する処理です。

Data serializationはデータを、指定したデータスキーマに沿うように型を変換するなどの処理です。

これらがサポートされているPythonのWebフレームワークだとDjango REST FrameworkSerializersが有名だと思います。重要な機能ですが、validation/serialization が統合されているPythonのWebフレームワークは多くないです。

Pydantic

Pydantic はPythonの型ヒントを利用してData validation/serializationを行うライブラリです。

webアプリケーション以外の用途でも使われているようです。

FastAPIを使ったことがあれば、FastAPIがどのようにPydanticの肩の上に立っているのかは想像しやすいと思います。なので、簡単な紹介に留めます。

Pydantic - Example

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: Optional[datetime] = None
    friends: List[int] = []


external_data = {
    'id': '123',
    'signup_ts': '2019-06-01 12:22',
    'friends': [1, 2, '3'],
}
user = User(**external_data)
print(user.id)
#> 123
print(repr(user.signup_ts))
#> datetime.datetime(2019, 6, 1, 12, 22)
print(user.friends)
#> [1, 2, 3]
print(user.dict())
"""
{
    'id': 123,
    'signup_ts': datetime.datetime(2019, 6, 1, 12, 22),
    'friends': [1, 2, 3],
    'name': 'John Doe',
}
"""

Basemodel を継承して、データの型をアノテーションするだけです。これだけで渡したデータの型を変換してくれます。また、指定したデータが含まれない場合はエラーを返してくれます。

Pythonの型ヒントはアノテーションでしかなく、実際はどんな型でも受け入れてしまいます。ゆるく書けて便利な場合もありますが、Web APIでは無効な型のデータが渡されるケースを考慮しなければならず、コードの行数が増えてしまいます。

なので、Pydanticを使うだけで相当な行数の削減になるはずです。

FastAPIとの統合

PydanticのData validation/serializationは単体でも非常に強力ですが、FastAPIと以下の様に統合されており、さらに便利なものになっています:

  • 引数の型アノテーションに使うだけで自動的にリクエストのvalidation/serializationが行われます
  • validation errorの場合、自動的にエラーレスポンスをクライアントに返します
  • OpenAPI標準のデータスキーマが自動生成されます

FastAPIがPydanticの肩の上に立っているというのは、Pydanticによって非常に重要な機能 (他のWebフレームワークと差別化できるような) が利用しやすくなっているためだと言えそうです。ただ、必ずしもPydanticを使わなければ動かない (実際は内部で重要な処理を任せているかもしれないですが) わけではないのでStarletteほどは依存していないと思います。

さいごに

FastAPIは巨人 (StarletteとPydantic) の肩の上に立っているというのはどういうことか知るためにそれらのライブラリがどのように使われているか調べました。
何かの参考になれば嬉しいです。

93
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
93
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?