コンテンツへスキップ

Pythonの型入門

Pythonには、オプションの「型ヒント」(「型アノテーション」とも呼ばれる)のサポートがあります。

これらの「型ヒント」またはアノテーションは、変数のを宣言できる特殊な構文です。

変数に型を宣言することで、エディタやツールはより良いサポートを提供できます。

これは、Pythonの型ヒントに関する簡単なチュートリアル/復習です。FastAPIでそれらを使用するために必要な最低限のことだけを扱っています...実際にはごくわずかです。

FastAPIはすべてこれらの型ヒントに基づいており、それらは多くの利点と利益をもたらします。

しかし、たとえFastAPIを一度も使わなくても、それらについて少し学ぶことで利益を得られるでしょう。

注釈

Pythonの専門家で、型ヒントについてすべて知っている場合は、次の章に進んでください。

動機

簡単な例から始めましょう

def get_full_name(first_name, last_name):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

このプログラムを実行すると出力されます

John Doe

この関数は次のことを行います

  • first_namelast_nameを受け取ります。
  • title()を使用して、それぞれの最初の文字を大文字に変換します。
  • 連結し、間にスペースを入れます。
def get_full_name(first_name, last_name):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

編集

非常にシンプルなプログラムです。

しかし、今、これをゼロから書いていると想像してみてください。

ある時点で関数の定義を始め、パラメータを準備していました...

しかし、その後、「最初の文字を大文字に変換するあのメソッド」を呼び出す必要があります。

upperだったか? uppercaseだったか? first_uppercaseだったか? capitalizeだったか?

そこで、古いプログラマーの友、エディタのオートコンプリートを試します。

関数の最初のパラメータであるfirst_nameを入力し、ドット(.)を入力して、Ctrl+Spaceを押して補完をトリガーします。

しかし、残念ながら、何も役立つものは得られません

型を追加する

前のバージョンから1行だけ変更します。

関数のパラメータであるこの部分を、正確に次のように変更します。

    first_name, last_name

から

    first_name: str, last_name: str

それだけです。

これらが「型ヒント」です。

def get_full_name(first_name: str, last_name: str):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

これは、次のようにデフォルト値を宣言することとは異なります。

    first_name="john", last_name="doe"

それは別のことです。

コロン(:)を使用しており、イコール(=)ではありません。

そして、型ヒントを追加しても、通常、型ヒントなしの場合と何が変わるわけではありません。

しかし今、その関数を作成している途中で、型ヒントを使用していると想像してください。

同じ時点で、Ctrl+Spaceでオートコンプリートをトリガーしようとすると、次のように表示されます。

これで、オプションをスクロールして、「ピンとくる」ものを見つけることができます。

さらなる動機

この関数を確認してください。すでに型ヒントがあります。

def get_name_with_age(name: str, age: int):
    name_with_age = name + " is this old: " + age
    return name_with_age

エディタが変数の型を知っているので、補完だけでなくエラーチェックも受けられます。

これで、agestr(age)で文字列に変換して修正する必要があることがわかります。

def get_name_with_age(name: str, age: int):
    name_with_age = name + " is this old: " + str(age)
    return name_with_age

型の宣言

型ヒントを宣言する主な場所を見ました。それは関数のパラメータとしてです。

これは、FastAPIでそれらを使用する主な場所でもあります。

単純な型

strだけでなく、すべての標準的なPython型を宣言できます。

たとえば、次のように使用できます。

  • int
  • float
  • bool
  • bytes
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
    return item_a, item_b, item_c, item_d, item_d, item_e

型パラメータを持つジェネリック型

dictlistsettupleのように、他の値を含むことができるデータ構造があります。そして、内部の値も独自の型を持つことができます。

内部の型を持つこれらの型は「ジェネリック」型と呼ばれます。そして、それらを内部の型を含めて宣言することが可能です。

これらの型と内部の型を宣言するには、標準のPythonモジュールtypingを使用できます。これは、これらの型ヒントをサポートするために特別に存在します。

新しいバージョンのPython

typingを使用する構文は、Python 3.6から最新バージョン(Python 3.9、Python 3.10などを含む)まですべてのバージョンと互換性があります。

Pythonの進化に伴い、新しいバージョンではこれらの型アノテーションのサポートが向上しており、多くの場合、型アノテーションを宣言するためにtypingモジュールをインポートして使用する必要すらありません。

プロジェクトでより新しいバージョンのPythonを選択できる場合は、その追加のシンプルさを活用できます。

すべてのドキュメントには、Pythonの各バージョンと互換性のある例が含まれています(違いがある場合)。

たとえば、「Python 3.6+」はPython 3.6以降(3.7、3.8、3.9、3.10などを含む)と互換性があることを意味します。「Python 3.9+」はPython 3.9以降(3.10などを含む)と互換性があることを意味します。

最新バージョンのPythonを使用できる場合は、最新バージョンの例を使用してください。それらは最も優れたシンプルな構文を持っています。たとえば、「Python 3.10+」です。

リスト

たとえば、strlistである変数を定義してみましょう。

コロン (:) 構文と同じように変数を宣言します。

型としてlistを置きます。

リストはいくつかの内部型を含む型なので、角括弧の中にそれらを置きます。

def process_items(items: list[str]):
    for item in items:
        print(item)

typingからList(大文字のL)をインポートします。

from typing import List


def process_items(items: List[str]):
    for item in items:
        print(item)

コロン (:) 構文と同じように変数を宣言します。

型として、typingからインポートしたListを置きます。

リストはいくつかの内部型を含む型なので、角括弧の中にそれらを置きます。

from typing import List


def process_items(items: List[str]):
    for item in items:
        print(item)

情報

角括弧内のこれらの内部型は「型パラメータ」と呼ばれます。

この場合、strList(またはPython 3.9以降ではlist)に渡される型パラメータです。

これは、「変数itemslistであり、このリスト内の各項目はstrである」という意味です。

ヒント

Python 3.9以降を使用している場合、typingからListをインポートする必要はなく、代わりに通常のlist型を使用できます。

そうすることで、エディターはリストの項目を処理している間でもサポートを提供できます。

型がない場合、これはほとんど不可能です。

変数itemはリストitemsの要素の1つであることに注意してください。

それでも、エディターはそれがstrであることを認識し、それに対するサポートを提供します。

タプルとセット

tuplesetを宣言する場合も同様です。

def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
    return items_t, items_s
from typing import Set, Tuple


def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]):
    return items_t, items_s

これは以下を意味します

  • 変数items_tは3つの項目を持つtupleで、int、別のint、およびstrです。
  • 変数items_ssetで、その各項目はbytes型です。

辞書

dictを定義するには、カンマで区切られた2つの型パラメータを渡します。

最初の型パラメータはdictのキー用です。

2番目の型パラメータはdictの値用です。

def process_items(prices: dict[str, float]):
    for item_name, item_price in prices.items():
        print(item_name)
        print(item_price)
from typing import Dict


def process_items(prices: Dict[str, float]):
    for item_name, item_price in prices.items():
        print(item_name)
        print(item_price)

これは以下を意味します

  • 変数pricesdictです。
    • このdictのキーはstr型です(各項目の名前としましょう)。
    • このdictの値はfloat型です(各項目の価格としましょう)。

Union

変数がいくつかの型のいずれかであることを宣言できます。たとえば、intまたはstr

Python 3.6以降(Python 3.10を含む)では、typingからUnion型を使用して、角括弧内に受け入れ可能な型を入れることができます。

Python 3.10には、可能な型を縦棒 (|)で区切って記述できる新しい構文もあります。

def process_item(item: int | str):
    print(item)
from typing import Union


def process_item(item: Union[int, str]):
    print(item)

どちらの場合も、これはitemintまたはstrのいずれかであることを意味します。

Noneの可能性

値がstrのような型を持つ可能性があるが、Noneである可能性もあることを宣言できます。

Python 3.6以降(Python 3.10を含む)では、typingモジュールからOptionalをインポートして使用することで宣言できます。

from typing import Optional


def say_hi(name: Optional[str] = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")

単にstrとする代わりにOptional[str]を使用すると、値が常にstrであると仮定しているが、実際にはNoneである可能性もある場合に、エディターがエラーを検出するのに役立ちます。

Optional[Something]は実際にはUnion[Something, None]のショートカットであり、同等です。

これは、Python 3.10ではSomething | Noneを使用できることも意味します。

def say_hi(name: str | None = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")
from typing import Optional


def say_hi(name: Optional[str] = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")
from typing import Union


def say_hi(name: Union[str, None] = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")

UnionまたはOptionalの使用

Python 3.10未満のバージョンを使用している場合、私の非常に主観的な視点からのヒントです。

  • 🚨 Optional[SomeType]の使用は避けてください。
  • 代わりに✨ Union[SomeType, None]を使用してください ✨。

どちらも同等で、内部的には同じですが、私はOptionalではなくUnionを推奨します。なぜなら、「optional」という言葉は値がオプションであることを示唆するように思えるかもしれませんが、実際には「Noneになる可能性がある」という意味であり、それがオプションではなく、依然として必須である場合でもそうです。

Union[SomeType, None]の方が、その意味についてより明確だと思います。

それは単に単語と名前に関するものです。しかし、これらの単語は、あなたやあなたのチームメイトがコードについて考える方法に影響を与える可能性があります。

例として、この関数を見てみましょう。

from typing import Optional


def say_hi(name: Optional[str]):
    print(f"Hey {name}!")
🤓 その他のバージョンとバリアント
def say_hi(name: str | None):
    print(f"Hey {name}!")

パラメータnameOptional[str]として定義されていますが、オプションではありません。パラメータなしで関数を呼び出すことはできません。

say_hi()  # Oh, no, this throws an error! 😱

nameパラメータはデフォルト値がないため、依然として必須ですオプションではありません)。それでも、nameNoneを値として受け入れます。

say_hi(name=None)  # This works, None is valid 🎉

良いニュースは、Python 3.10以降では、型結合を定義するために|を使用するだけで済むため、この心配をする必要がなくなることです。

def say_hi(name: str | None):
    print(f"Hey {name}!")
🤓 その他のバージョンとバリアント
from typing import Optional


def say_hi(name: Optional[str]):
    print(f"Hey {name}!")

そうすれば、OptionalUnionのような名前について心配する必要はありません。😎

ジェネリック型

角括弧で型パラメータを取るこれらの型は、ジェネリック型またはジェネリクスと呼ばれます。たとえば、

同じ組み込み型をジェネリックとして使用できます(角括弧と型を内部に記述)。

  • リスト
  • タプル
  • セット
  • 辞書

そして、Python 3.8と同様に、typingモジュールから

  • Union
  • Optional(Python 3.8と同じ)
  • ...など。

Python 3.10では、ジェネリックのUnionOptionalを使用する代わりに、縦棒 (|)を使用して型のユニオンを宣言できます。これははるかに優れていてシンプルです。

同じ組み込み型をジェネリックとして使用できます(角括弧と型を内部に記述)。

  • リスト
  • タプル
  • セット
  • 辞書

そして、Python 3.8と同様に、typingモジュールから

  • Union
  • Optional
  • ...など。
  • リスト
  • タプル
  • セット
  • 辞書
  • Union
  • Optional
  • ...など。

型としてのクラス

クラスを変数の型として宣言することもできます。

名前を持つPersonクラスがあるとしましょう。

class Person:
    def __init__(self, name: str):
        self.name = name


def get_person_name(one_person: Person):
    return one_person.name

次に、変数をPerson型として宣言できます。

class Person:
    def __init__(self, name: str):
        self.name = name


def get_person_name(one_person: Person):
    return one_person.name

そして、再び、すべてのエディタサポートを受けられます。

これは「one_personPersonクラスのインスタンスである」ことを意味することに注意してください。

one_personPersonというクラスである」という意味ではありません。

Pydanticモデル

Pydanticはデータ検証を実行するためのPythonライブラリです。

データの「形状」を、属性を持つクラスとして宣言します。

そして、各属性は型を持っています。

次に、そのクラスのインスタンスをいくつかの値で作成すると、値が検証され、適切な型に変換され(該当する場合)、すべてのデータを含むオブジェクトが返されます。

そして、その結果のオブジェクトですべてのエディタサポートを受けられます。

Pydantic公式ドキュメントからの例

from datetime import datetime

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: datetime | None = None
    friends: list[int] = []


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

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: Union[datetime, None] = None
    friends: list[int] = []


external_data = {
    "id": "123",
    "signup_ts": "2017-06-01 12:22",
    "friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import List, Union

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: Union[datetime, None] = None
    friends: List[int] = []


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

FastAPIはPydanticに基づいて構築されています。

これらすべての詳細については、チュートリアル - ユーザーガイドで実践的に説明されています。

ヒント

Pydanticは、デフォルト値なしでOptionalまたはUnion[Something, None]を使用すると特殊な動作をします。これについては、Pydanticの必須オプションフィールドに関するドキュメントで詳しく読むことができます。

メタデータ注釈付き型ヒント

Pythonには、Annotatedを使用してこれらの型ヒントに追加のメタデータを配置できる機能もあります。

Python 3.9では、Annotatedは標準ライブラリの一部なので、typingからインポートできます。

from typing import Annotated


def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
    return f"Hello {name}"

Python 3.9未満のバージョンでは、typing_extensionsからAnnotatedをインポートします。

FastAPIと共にすでにインストールされています。

from typing_extensions import Annotated


def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
    return f"Hello {name}"

Python自体は、このAnnotatedで何も行いません。エディタやその他のツールにとって、型は依然としてstrです。

しかし、このAnnotatedのスペースを使用して、アプリケーションの動作方法に関する追加のメタデータをFastAPIに提供できます。

覚えておくべき重要なことは、Annotatedに渡す最初の型パラメータ実際の型であるということです。残りは、他のツール用のメタデータにすぎません。

今のところ、Annotatedが存在し、それが標準的なPythonであることを知っていれば十分です。😎

後で、それがどれほど強力であるかを確認できます。

ヒント

これが標準Pythonであるという事実は、コードの分析やリファクタリングに使用するツールなど、エディタで可能な限り最高の開発者体験を得られることを意味します。✨

また、あなたのコードが他の多くのPythonツールやライブラリと高い互換性を持つことを意味します。🚀

FastAPIにおける型ヒント

FastAPIは、これらの型ヒントを利用していくつかのことを行います。

FastAPIで型ヒントを使用してパラメータを宣言すると、以下が得られます。

  • エディターのサポート.
  • 型チェック.

...そしてFastAPIは同じ宣言を使用して次を行います。

  • 要件の定義: リクエストパスパラメータ、クエリパラメータ、ヘッダー、ボディ、依存関係などから。
  • データの変換: リクエストから必要な型へ。
  • データの検証: 各リクエストから送られてくるデータを検証します。
    • データが無効な場合にクライアントに返される自動エラーを生成します。
  • OpenAPIを使用してAPIをドキュメント化します。
    • これは、自動的な対話型ドキュメントユーザーインターフェイスで使用されます。

これらすべては抽象的に聞こえるかもしれません。心配しないでください。チュートリアル - ユーザーガイドでこれらすべてを実際に確認できます。

重要なのは、標準的なPythonの型を1箇所で使用するだけで(追加のクラスやデコレーターなどを追加する代わりに)、FastAPIが多くの作業を行ってくれるということです。

情報

すでにすべてのチュートリアルを終えて型についてさらに学習するために戻ってきた場合は、mypyの「チートシート」が良い参考資料になります。