Pythonのtypingについて改めて調べてみた
今回はPythonのtypingについて今までちゃんと調べずに使っていたこともあったので調べてみることにしました。
typingとは?
Pythonでデフォルトで利用可能なtyping
は、Pythonにおけるタイプヒントをサポートする目的で実装されています。以下が公式ページになります。注釈にも書いてありますが、一般的はPythonランタイムは関数や変数のアノテーションについて強制することはありません。あくまで型アノテーションは明示的に型を示すことでセルフドキュメントとして機能します。
※ mypyなどの静的型チェッカーを利用するさいはタイプヒントが必要です。基本的にはmypyを利用しない場合もアノテーションはつけるようにしましょう。本記事では、以下のリンクの内容を要約したものとなります。
代表的なものを解説します!
型エイリアス
型エイリアスはtype
を利用して定義され、TypeAliasType
インスタンスが生成されます。例えば以下のように使います。なお、この記法は3.12より利用できます。
type Vector = list[float]
def sum_vector(vector: Vector) -> float:
return sum(vector)
なお、typing.TypeAlias
を利用することで明示的に指定することもできます。
from typing import TypeAlias
Vector: TypeAlias = list[float]
NewType
元の型と異なる型として定義したい場合はtyping.NewType
を使います。
from typing import NewType
UserId = NewType("UserId", int)
def get_user_name(user_id: UserId) -> str:
return ""
get_user_name(UserId(1)) # OK: UserId型を引数として指定しているため静的型チェックに成功する
get_user_name(1) # NG: int型を引数として指定しているため失敗する
呼び出し可能オブジェクト
例えば関数などのような呼び出し可能オブジェクトをアノテーションする場合はtyping.Callable
またはcollections.abc.Callable
を利用します。例えば以下のようになります。
from typing import Callable
def func(callback: Callable[[int], str]):
...
callback(...)
Callable[[A], B]
において、Aには引数の型、Bには戻り値の型を指定します。
ジェネリクス
コンテナに含まれるオブジェクトに関する型情報はcollections.abc
のなかのMapping
やSequence
などがあります。例えば、int型は確定しているがコンテナのデータは確定していない場合、つまりlistもあればtupleもある場合、以下のように記載できます。
from collections.abc import Sequence
def calculate_sum(items: Sequence[int]) -> int:
return sum(items)
ジェネレータ
ジェネレータを定義するにはtyping.Generator
を用います。利用方法位は以下になります。
from typing import Generator
def stream() -> Generator[int, float, str]:
...
ここでGenerator
の中の設定内容ですが最初から順番にyieldで返されるデータ型
、sendで送られるデータ型
そしてreturn
で戻されるデータ型を指定します。
ユーザ定義のジェネリック型
ユーザ定義のジェネリック型を定義するには以下のようにすることで実現できます。
class Hoge[T]:
def __init__(self, value: T):
self.value = value
hoge = Hoge[int](10)
これ以外にも、typing.Generitc
とTypeVar
を利用して以下のようにかくこともできます。
from typing import TypeVar, Generic
T = TypeVar("T")
class Hoge(Generic[T]):
...
Any型
どのような値が入ることも受け入れるためにtyping.Any
が用意されています。利用方法は以下です。
from typing import Any
def func(value: Any) -> Any:
...
ただし、Anyだとどのようなデータを指定するべきか判定ができないので、基本的には利用しない方針がいいかと思います。
Never
typing.Never
というものがあり、これを指定された関数はreturnがない、つまりエラーやsys.exit
などが実行されることを明記できます。
from typing import Never
def stop() -> Never:
raise RuntimeError()
TypeAlias
先ほども出てきましたが、データ型を定義するために明示的に指定する方法としてTypeAlias
があります。
from typing import TypeAlias
Vector: TypeAlias = list[float]
特殊形式
まず初めはユニオン型になります。ユニオン型は複数の候補がありうるデータ型になっており、従来はtyping.Union
が利用されていましたが、最近のバージョンではパイプ(|
)を利用することで記載できます。
from typing import Union
def func1(x: Union[int, str]):
...
def func2(x: int | str):
...
次はオプショナルになります。オプショナルは指定した型またはNoneとなるものです。従来はtyping.Optional
が主流でしたが、今はUnion + None という感じでかけます。つまり以下のようになります。
from typing import Optional
def func1(x: Optional[int]):
...
def func2(x: int | None):
...
3つ目はLiteral
です。これは複数の値の候補を明示する時に利用できるものです。例えば以下のように利用します。
from typing import Literal
type Mode = Literal["r", "w", "rb", "wb"]
def write_file(filepath: str, mode: Mode):
...
def write_file("hoge.txt", "w")
Annotated
typing.Annotated
はデータ型にコンテキスト特有のメタデータを不要する時に利用されます。メタデータを与えることで、静的分析の際に利用されます。
例えばベースはint型ですがその値の取りうる範囲をメタデータとして与えたい場合、以下のように記載できます。
from typing import Annotated
from dataclasses import dataclass
@dataclass
class IntValueRange:
lo: int
hi: int
RangedInt = Annotated[int, ValueRange(-10, 10)]
このようにすることで、RangeIntは-10から10の値を取りうるint型であると定義できます。なお、基本的にインタプリタには無視される情報であり、FastAPIやPydanticなど一部のライブラリなどでは解釈されます。
TypedDict
TypedDictは全てのインスタンスでキーと値が完全に固定されている場合に利用すると便利です。例えば、以下のようにキーxはint、キーyはfloatの値を必ず取ると明示する場合、以下のようにします。
from typing import TypedDict
class Point2D(TypedDict):
x: int
y: float
value1: Point2D = {"x": 1, "y": 1.5} # OK
value2: Point2D = {"x": 1.5, "y": 1} # NG
なお、この記述も他と同様インタプリタでは解釈されないです。
overload
Pythonではオーバーロードは基本的にされませんが、typing.overload
を利用することでmypyなどに向けて関数のオーバーライドが実装されているということを明示できます。例えば以下のようにすることで対応できます。
from typing import overload
@overload
def func(x: int) -> str:
...
@overload
def func(x: str) -> int:
...
def func(x):
if isinstance(x, int):
return str(x)
elif isinstance(x, str):
return len(x)
else:
raise TypeError("Unsupported type")
なお、上記のように具体的な実装は単一の関数であり、それ以外はあくまでオーバーロードの型だけ定義しています。
まとめ
今回はPythonのtypingについて、主要なよく目にするものについて紹介しました。公式ドキュメントではより詳細に記載されていますので、より深くキャッチアップされたい方はぜひドキュメントも参照してもらえたらと思います。型アノテーションはインタプリタには無視されるものの、開発する際のドキュメントとして機能する大変大事な要素なので、ぜひ開発する際は積極的にアノテーションをつけてもらえればと思います。
Discussion