LoginSignup
478
505

More than 1 year has passed since last update.

Pythonの型を完全に理解するためのtypingモジュール全解説(3.10対応)

Last updated at Posted at 2021-07-12

はじめに

Pythonでキチンと型アノテーションを書くのであれば一度は読んでおきたいのがtypingライブラリの公式ドキュメントです。

前回の記事でも読んでくださいと(偉そうに)書いたわけですが、実のところこれは型アノテーションを解釈する側1に向けたドキュメントだったりもするのでアノテを書く側にとっては情報がごちゃごちゃしてるんですよね。加えて未翻訳の箇所もそれなりにあってま~~~~読みにくい。実際に読んでいただいた方々からの評判もすこぶる悪かったです。また

機能は分かったけど結局どんな時に使えばいいのか分からない

という致命的なコメントも聞きました。
そこで今回はこれらの不満を解消すべく、公式ドキュメントを一般ユーザー向けに再編し具体的なユースケースを盛り込んだ非公式ドキュメントをご用意しました。

なおPython 3.10は2021年10月04日リリース予定ですが、先駆けてドキュメントが公開されていますので当記事はこちらの3.10.0b2バージョンを元に執筆しています。

想定読者

  • 型アノテーションは書き始めてるけどまだなんとなくふわっとしている
  • 最後にtypingのドキュメントを読んでから時間があいている(~3.7くらい)
  • 他の言語から来たけどPythonの型ってどうなってるの
  • 漸進的型付けやっていくぞ

そもそも型アノテーションとは何ぞやという方はまずこちらこちらの記事を一読されることをおすすめします。

解説

typingモジュールからインポートできるオブジェクトを

  • 型アノテーションの拡張
  • ユーティリティ

の2つに分類して紹介します。
いかんせん数がはちゃめちゃに多いので読み飛ばせるよう特に重要な項目には★をつけています。

型アノテーションの拡張

def hoge(x: int) -> str:
    dx: float = x * 0.1
    return str(dx)

上記で言うint/str/floatに該当する部分に書くことで型チェッカーに対して様々な情報を伝えることができる特殊な型です。
Python 3.9で多くが非推奨になり(理由は後述)公式ドキュメントが無駄に読み辛くなっているためここでは3.9以降OK/NGに分けて記載します。

3.9以降も使えるもの

Any

なんでもいいよ型です。
型に依らない汎用的なライブラリを作る際や型情報のないデータを取ってきて自分でこねこねしないといけない場合に使います。

汎用的な関数の例
from typing import Any, TypeVar
from collections.abc import Iterable

T = TypeVar("T")

# 配列(のようなオブジェクト)の中からt型の要素だけを残して返す関数
# Iterable[Any]はIterableと等価ですが、Anyを書いた方が使う人にやさしいので書いた方がいいです
def type_filter(itr: Iterable[Any], t: type[T]) -> list[T]:
    return [elm for elm in itr if isinstance(elm, t)]

x = ["hoge", 1, "fuga", 2, "piyo", 3]
type_filter(x, str)
>>> ["hoge", "fuga", "piyo"]

NoReturn

実行時エラーで値が返らない関数に使います。というか使いません
Pythonは返すものを明示しない場合暗黙的にNoneオブジェクトが返るので-> NoReturn-> Noneは別物であることに注意してください。
returnを書かない時は-> Noneです。

def hoge() -> None:
    print("hoge!!!")

x = hoge()
>>> hoge!!!
x
>>> None

Union

3.9までの合併型
from typing import Union

hoge: Union[int, str]

とした場合hogeint/str両方の値を取り得ます。「もしくは」です。
3.10からは|演算子の適用が拡張されて

3.10からの合併型
hoge: int | str

と書けるようになりますが、これは

  • 見た目がスマート
  • パッと見で意味が分かる(論理和なので)
  • Unionをインポートする必要がない
  • TypeScriptやScalaと同じ書き方

と良いことずくめなので是非Unionから乗り換えて欲しいなと思います。

Optional

hoge: Union[int, None]

を省略した形が

hoge: Optional[int]

です。そんなに省略できてなくて面白いですね2
そしてこの略記というものがチーム開発をする上では非常にやっかいで、上記Unionの略記もあわせると

Null許容型の表記いろいろ
hoge: Union[int, None]
hoge: int | None
hoge: Optional[int]

と3パターンもの表記ゆれが発生することになります。特にNull許容型は発生頻度も高いのでプロジェクトのコーディング規約でどれを使うのか定めておきましょう。

なお公式ドキュメントではダメだよと言われている

xの型が不一致
def hoge(x: int = None) -> None:
    ...

この書き方ですが、どうやら3.10以降inspect.signature()はこれをOptional[int]と見なしてくれるらしく、型チェッカーも追随してくれないかなぁとひそかに期待しています。

Literal

ブラケットの中で指定したリテラル(文字列/整数)3のみを受け入れます。

from typing import Literal

x: Literal["hoge", "fuga", "piyo"]
x = "hoge"  # OK
x = "foo"  # NG

TSのuinon型ほど万能ではありませんが大抵のEnumを置換できるため非常に強力です。
Enumと違って反復処理ができない(xから"hoge"/"fuga"/"piyo"を取り出すことができない)のが弱点。

TypeGuard (3.10 ~)

要はTSのアレなのですが説明が難しいので1から解説します。
image.png
11行目でエラーが出ていますね。
lの中身はis_str_list関数によってstr型であることが確定しているので我々人間からするといいんじゃないの?と思うのですが、型チェッカーくんは自作の型判定関数を理解しません。普段isinstanceで型の絞り込みが行えているのは型チェッカーがisinstanceissubclassに対して特別なプログラムを組んでいるからなのです4

ということでTypeGuardの出番なのですが…
image.png
ちょっと何言ってるか分からないですね。
押さえるべきTypeGuardの仕様は3つです。

  1. TypeGuardを使う関数は引数を1つだけ取る
  2. この関数は実際にはbool値を返す
  3. この関数がTrueを返した場合、型チェッカーは渡された引数がTypeGuardのブラケット内に書かかれた型であると判定する

よって型チェッカーがlの中身(elm)をstr型であると認識しエラーが消えたということになります。

コピペ用(画像と同じコードです)
from typing import Any, TypeGuard

def is_str_list(l: list[Any]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in l)

def hoge(l: list[int | str]):
    if is_str_list(l):
        for elm in l:
            print(elm.lower())

Final

Finalがついた変数には再代入ができなくなります(強制的に定数になります)。

HOSHII_YEN: Final = 5000_0000_0000_0000
HOSHII_YEN += 1  # NG

クラス定数をサブクラスでオーバーライドすることも禁止されます。

class Hoshii:
    YEN: Final[int] = 5000_0000_0000_0000

class MottoHoshii(Hoshii):
    YEN = 5000_0000_0000_0000_0000  # NG

Annotated (3.9~)

事実上アノテーションの機能が型アノテーションに支配されたので、型以外のアノテーションもつけれる方法を再定義しようぜ、ということで生まれた構文です。

hoge: Annotated[type, x, y, ...]

とすることでhogeという名前に対して型と後続のメタデータ(なんでも可、長さも自由)を付与することができるようになります。
型チェッカーが付与されたメタデータを元にいろいろ警告を出してくれるようになったりすると便利なんでしょうね。
今のところコメント以上の意味はありません🙃

★ プロトコル(一覧)

これらの型は通常の型(公称型)と異なり特定のメソッドが実装されているかどうかを元に型チェッカーが型の整合を判断します。(→Protocol)

from typing import Iterable, SupportsRound
from decimal import Decimal

def round_sum(nums: Iterable[SupportsRound]) -> int:
    return sum(round(num) for num in nums)

round_sum([1, 1.3, Decimal("1.4")])  # これらの型に親子関係は存在しないが全員`__round__`メソッドを持つのでOK
>>> 3
プロトコル 対応するメソッド 備考
SupportsBytes __bytes__
SupportsInt __int__
SupportsFloat __float__
SupportsComplex __complex__
SupportsAbs __abs__ 戻り値の型は共変
SupportsRound __round__ 戻り値の型は共変
SupportsIndex __index__

__add__などのプロトコルを自分で作りたい場合は同様にSupportsAddのような名前にすると統一感があっていいと思います。
またコレクションのABCはABCでありながらプロトコルと同様に扱うことが可能です。

from collections.abc import Sequence

def count_hoge(seq: Sequence[str]) -> int:  # Sequenceは破壊的メソッドを持たないため渡したseqに副作用が起こらないことを保証できる
    return seq.count("hoge")

count_hoge(["hoge", "fuga", "piyo"])  # OK
count_hoge([1, 2, 3])  # NG
count_hoge("hoge")  # OK(str型はSequence[str]でもあるので)

3.9で非推奨になったもの

intlist」ように「型'」で表せる型'のことをジェネリック型(総称型/汎用型)と言いますが、以前はこのジェネリックを型チェッカーへ通知するためにtypingからジェネリック専用の型をインポートする必要がありました。

3.8以前
from typing import List

any_list: list = [1, 2, "3"]  # OK
int_list: List[int] = [1, 2, 3]  # OK
str_list: list[str] = ["1", "2", "3"]  # NG

ただこれはもう今の時代にそぐわなかろう、ということで3.9からはいよいよ元の型にも[](正確に言うと__class_getitem__)がサポートされます。

3.9以降
int_list: list[int] = [1, 2, 3]  # OK!!

役目を終えたtypingのジェネリック専用型たちはPython3.9のリリース(2020/10/05)から5年後のバージョンで削除されることが決まっている5ので、今のうちに置き換えておきましょう。

Tuple / List / Dict / Set / FrozenSet

それぞれbuiltinsのtuple / list / dict / set / frozensetに相当。
アノテーションの書き方が少し複雑なのでいくつか具体例を書きます。

このコードはちゃんと動きます(3.10以降)
int型の配列: list[int] = [1, 2, 3]
int型またはstr型の配列: list[int | str] = [1, 2, "3"]
int型3つのタプル: tuple[int, int, int] = (1, 2, 3)
int型int型str型のタプル: tuple[int, int, str] = [1, 2, "3"]
int型がいくつか入ったタプル: tuple[int, ...] = (1, 2, 3)
空のタプル: tuple[()] = ()
str型がkeyでint型をvalueとする辞書: dict[str, int] = {"alice": 1, "bob": 2, "carol": 3}
# 「1つ目の要素がint型、2つ目以降の要素がstr型の長さが不明なタプル」のようなアノテーションは書けない
# tuple[int, str, ...] ← これはダメ
# なのでその場合はこう↓
int型とstr型配列のタプル: tuple[int, list[str]] = (1, ["2", "3"])

またちょっと特殊な使い方としてGeneric[T]継承ができます。
例えば自作オブジェクトの配列に属性をつけたい時ってあるじゃないですか?ああいう時に

from typing import NamedTuple

class Line(NamedTuple):
    min: int
    max: int

class Lines(list[Line]):
    @property
    def bbox(self) -> Line:
        return Line(min(l.min for l in self), max(l.max for l in self))

このような宣言を書くことによってLinesのインスタンスにはLine以外の要素を追加できなくなります。
もちろん特別な属性がいらない場合は

TypeAliasは3.10から
Lines: TypeAlias = list[Line]

でいいので登場機会は少ないですが覚えておいて損はないです。(→GenericTypeAlias)

Callable

collections.abcCallableに相当。
関数を受け取ったり返したりする関数(=高階関数)を作るとき、最もメジャーな例で言うとデコレータを作るときに使います。

3.9までのデコレータ
from typing import TypeVar
from collections.abc import Callable
from functools import wraps

R = TypeVar("R")

def with_good_morning(func: Callable[..., R]) -> Callable[..., R]:
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("おはよー!")
        return func(*args, **kwargs)

    return wrapper

@with_good_morning
def greet(*greets: str) -> None:
    for g in greets:
        print(g)
    print("おやすみー!")

@with_good_morning
def greet_(afternoon: str, evening: str) -> None:
    print(afternoon)
    print(evening)
    print("おやすみー!")

greet("こんちわー!", "こんばんわー!")
>>> おはよー
>>> こんちわー
>>> こんばんわー
>>> おやすみー
greet_("こんちわー!", "こんばんわー!")
>>> おはよー
>>> こんちわー
>>> こんばんわー
>>> おやすみー

ただ我々のような型愛好家にとってデコレータは長らく鬼門でありました。
なぜなら
image.png
デコレータをつけた関数は外部から引数の情報が見えなくなってしまうのです。
だからといってデコレータのアノテーションを

def with_good_morning(func: Callable[[P], R]) -> Callable[[P], R]:
    ...

にしてはgreetgreet_のように引数の数が異なる関数に適用できなくなってしまいます。
そこで、3.10では以下の新しい仕組みが導入されました。

ParamSpec (3.10 ~)

引数のための特別な型変数です。

3.10からのデコレータ
from typing import TypeVar, ParamSpec
from collections.abc import Callable
from functools import wraps

P = ParamSpec("P")
R = TypeVar("R")

def with_good_morning(func: Callable[P, R]) -> Callable[P, R]:
    @wraps
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("おはよー!")
        return func(*args, **kwargs)

    return wrapper

@with_good_morning
def greet(*greets: str) -> None:
    for g in greets:
        print(g)
    print("おやすみー!")

@with_good_morning
def greet_(afternoon: str, evening: str) -> None:
    print(afternoon)
    print(evening)
    print("おやすみー!")

これで引数が見れるようになります。
image.png
image.png
悲願成就!
ちなみにfunctools.wrapsについてはこちらの記事に詳しいです。

Concatenate (3.10 ~)

ParamSpecConcatenate(=結合)します。

def with_good_morning(func: Callable[Concatenate[Logger, P], R]) -> Callable[P, R]:
    ...

とかなんとかすることによってデコレートした関数の引数を減らしたり増やしたりできるんですが、私はそもそもそういう目的でデコレータを使うべきではないと思っている6ので使いません。

Type

builtinsのtypeに相当。
Anyの説明でちょろっと使いましたがあんな感じで型そのものを受け入れる場合に使います。

int型の値を受け入れるとき
def hoge(t: int):
    ...
int型を受け入れるとき
def hoge(t: type[int]):
    ...

int型の~と書きましたが、実際にはどちらもintとして扱えるものが対象なので例えば前者にはTrueを、後者にはboolを渡すことも可能です(関数内ではint(型)として扱われます)。
受け取った型を戻り値に使いたい場合にはこうなります。

intを渡すとint値が、boolを渡すとbool値が返る。strは渡せない
IntT = TypeVar("IntT", bound=int)

def make_int(t: type[IntT]) -> IntT:
    ...

(→TypeVar)

Iterator / Generator

collections.abcIterator / Generatorに相当。
ジェネレータのアノテーションはGenerator[yieldで返す値の型, sendで受ける値の型, returnで返す値の型]ですが、sendreturnも使わない場合にはGenerator[yieldで返す値の型, None, None]となり、これは事実上Iterator[yieldで返す値の型]と同じです。
どちらで書くかは好みのレベルですがプロジェクト内でバラバラになると気持ち悪いので規約なりで定めておくといいでしょう7

みんな大好きフィボナッチ数列ジェネレータ
from collections.abc import Generator, Iterator

def fibonacci_max(max: int) -> Generator[int, None, str]:
    a, b = 0, 1
    while a <= max:
        yield a
        a, b = b, b + a
    return "done"

def fibonacci_n(n: int) -> Iterator[int]:  # -> Generator[int, None, None] でも○
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, b + a

その他(一覧)

非推奨の型 対応する型 備考
ChainMap collections.ChainMap
DefaultDict
OrderedDict
Counter
collections.defaultdict
collections.OrderedDict
collections.Counter
dictのサブクラス3兄弟。
CounterはKey-VauleのVがintで固定されているためCounter[KT]
Deque collections.deque
Pattern
Match
re.Pattern
re.Match
これらが持っている値はAnyStrなのでMatch[T]でどちらなのか明示できます。
Container collections.abc.Container
Sized collections.abc.Sized
Collection collections.abc.Collection
ByteString collections.abc.ByteString bytesのスーパークラス。
Sequence
MutableSequence
collections.abc.Sequence
collections.abc.MutableSequence
listのスーパークラス。
AbstractSet
MutableSet
collections.abc.Set
collections.abc.MutableSet
setのスーパークラス。
Mapping
MutableMapping
collections.abc.Mapping
collections.abc.MutableMapping
dictのスーパークラス。
MappingView
ItemsView
KeysView
ValuesView
collections.abc.MappingView
collections.abc.ItemsView
collections.abc.KeysView
collections.abc.ValuesView
Iterable collections.abc.Iterable
Hashable collections.abc.Hashable
Reversible collections.abc.Reversible
Awaitable collections.abc.Awaitable
Coroutine collections.abc.Coroutine コルーチンのアノテーションはジェネレータと同じ。
AsyncIterable collections.abc.AsyncIterable
AsyncIterator
AsyncGenerator
collections.abc.AsyncIterator
collections.abc.AsyncGenerator
非同期ジェネレータのアノテーションは基本ジェネレータと同じですが
ジェネレータと違って値を返せないのでreturn部分がない。
AsyncIterator
AsyncContextManager
contextlib.AbstractContextManager
contextlib.AbstractAsyncContextManager

ユーティリティ

型にまつわる様々な便利機能を提供してくれるオブジェクトたちです。

TypeVar

型を入れる変数、型変数です。
これを引数の型に指定することで呼び出し時に型が決まる汎用的(=ジェネリック)な関数/クラスを作ることができます。

基本的な宣言
from typing import TypeVar

T = TypeVar("T")  # なんでも取れる
S = TypeVar("S", str, int)  # strかintを取れる
IntT = TypeVar("IntT", bound=int)  # intかintのサブクラスを取れる
合併型(Union)との違い
from typing import TypeVar, TypeAlias

T = TypeVar("T", str, int)
U: TypeAlias = str | int

def hoge(x: T, y: T):  # ジェネリック関数
    ...

def fuga(x: U, y: U):  # 普通の関数
    ...

hoge(0, 0)  # OK
fuga(0, 0)  # OK
hoge("", "")  # OK
fuga("", "")  # OK
hoge(0, "")  # NG!!
fuga(0, "")  # OK

共変や反変な型を使いたい場合はそれぞれcovariant/contravariantTrueを渡します8

共変/反変の型変数の命名規則についてはPEP484に小さく書かれていますが要はハンガリアンしろとのことです
from typing import TypeVar

T_co = TypeVar("T_co", covariant=True)
T_contra = TypeVar("T_contra", contravariant=True)

AnyStr

AnyStr == TypeVar("AnyStr", str, bytes)

です。なんかよく使うから用意したらしい。

TypeAlias (3.10 ~)

複雑になってしまったアノテーションにはエイリアスを設定できるのですが、その書き方がグローバル変数への代入と区別がつき辛いという意見がありエイリアスをキチンと明示するために導入されました。

from typing import TypeAlias

Mutables = [list, set, dict]  # グローバル変数への代入
Mutable: TypeAlias = list | set | dict  # 型エイリアス

def hoge(m: Mutable):
    ...

ちなみにこの型エイリアスもジェネリックにできます
一番わかりやすい例としてOptional(のようなもの)を自前で実装してみましょう。

from typing import TypeVar, TypeAlias

T = TypeVar("T")
Optional: TypeAlias = T | None

def hoge(x: Optional[int] = None):
    ...

def fuga(x: Optional[T] = None) -> T:  # エイリアスを使ってジェネリック関数も作れる
    ...

とは言え所詮エイリアスはエイリアスであって型ではない(つまりisinstanceによる判定ができない)ので過信は禁物です。あくまでも簡易的な方法として。

NewType

サブクラスのように振舞うサブクラスではないなにかを生み出します。
得られる計算コストの減少量に対してコードの複雑化というデメリットが大きすぎるので素直にサブクラスを作ってください。

Generic

ジェネリックな型を1から自作したい時に継承するABCです。

究極まで単純化したジェネリック型
from typing import Generic, TypeVar
from dataclasses import dataclass

T = TypeVar("T")

@dataclass
class Hoge(Generic[T]):
    x: T

    def hoge(self, t: T) -> T:
        ...

hoge: Hoge[int] = Hoge(0)  # OK
hoge_: Hoge[str] = Hoge(0)  # NG
hoge.hoge(0)  # OK
hoge.hoge("")  # NG

Generic[T]を継承するという一見ぎょっとするような書き方ですが、なんとこの継承方法は全てのジェネリック型で共通です。
またその際型変数に型変数を代入することも出来るので

from typing import TypeVar

T = TypeVar("T")

# collections.Counterと同じ
class Counter(dict[T, int]):
    ...

x: Counter[str]
x = Counter({"hoge": "h", "fuga": "f", "piyo": "p"})  # NG
x = Counter({"hoge": 1, "fuga": 2, "piyo": 3})  # OK

このように型の一部だけを固定、ということが可能です。

Protocol

構造的部分型を定義するための基底クラスです。

from typing import Protocol

class Greeter(Protocol):
    def greet(self, name: str) -> str:
        ...

class Caller:
    def greet(self, name: str) -> str:
        return f"{name}!おはよー!"

def greet(g: Greeter) -> None:
    print(g.greet("alice"))

greet(Caller())  # OK

GreeterCallerに直接の継承関係はありませんが、GreeterProtocolを継承していることによって型チェッカーはCallerGreeterであると見なせるようになります。
(それがなぜ嬉しいのかについては前回の記事を読んでいただけると嬉しいです。)

もう少し具体的に説明するとプロトコルは

  • メソッド名
  • 引数名
  • 引数の型
  • 戻り値の型

の4つが一致するメソッドを持つクラスを派生型と見なします9。よって

class Repeater:
    def greet(self, n: int) -> str:
        return "おはよー!" * n

greet(Repeater())  # NG

引数名と引数の型が異なるこれはダメです。
ただし逆に言えばメソッド名が一致していても他の3つが一致しなければ別物として扱われる(関係ないクラスを巻き込まない)ので、PEP544にあるように複雑なコールバック関数を複雑な__call__メソッドを持つプロトコルとして定義してしまうことも可能です。

from typing import Protocol, Optional

class Combiner(Protocol):
    def __call__(self, *vals: bytes, maxlen: Optional[int] = None) -> list[bytes]:
        ...

def hoge(cb: Combiner):  # これをCallableで表現しようと思ったら大変
    ...

(→Callable)

@runtime_checkable

実行時にチェックできる。
プロトコルを@runtime_checkableでデコレートするとisinstance/issubclassが反応するようになります。

from typing import Protocol, runtime_checkable

@runtime_checkable
class Greeter(Protocol):
    def greet(self, name: str) -> str:
        ...

class Caller:
    def greet(self, name: str) -> str:
        return f"{name}!おはよー!"

issubclass(Caller, Greeter)
>>> True

注意点がありまして、型チェッカーはメソッドのシグネチャ全ての一致を確認するのに対してプロトコルに対するruntime_checkはメソッド名の一致しか確認しません。よって引数名と引数の型が異なるRepeater

class Repeater:
    def greet(self, n: int) -> str:
        return "おはよー!" * n

issubclass(Repeater, Greeter)
>>> True

Greeterの派生型になります。大嘘です。なんか上手く言葉にできないんですが、構造的部分型ってそういう風に使うものじゃないような気がするのでこれでいいんだと思います…。
そしてそういう感じなので私は@runtime_checkable使わないことをおすすめします。

ClassVar

from typing import ClassVar

class Hoge:
    x: ClassVar[int] = 1  # これはクラス変数!
    y: int  # こっちはインスタンス変数

こんな感じでクラスを定義するとインスタンスでxの値を変更しようとした際にエラーを出してくれます。

hoge = Hoge()
hoge.y = 1  # OK
hoge.x = 2  # NG
Hoge.x = 3  # OK

、いうのが本来の(公式ドキュメントに書かれている)目的なのですが、私は事実上@dataclassにクラス変数を通知するためのものだと思っています。

datacalssが作る諸々のメソッドにClassVarは含まれない
from typing import ClassVar
from dataclasses import dataclass

@dataclass
class Hoge:
    x: ClassVar[int] = 1
    y: int

hoge = Hoge(3)
hoge.x
>>> 1
hoge.y
>>> 3

大変かしこいですね10

NamedTuple

collections.namedtupleという名前で要素にアクセスできるtupleに型を追加して最強になった姿です。

from typing import NamedTuple

class RGB(NamedTuple):
    r: int
    g: int
    b: int

RGB("hoge", "fuga", "piyo")  # NG
saxe_blue = RGB(65, 139, 137)  # OK
saxe_blue[0]
>>> 65
saxe_blue.b
>>> 137

面倒なクラス宣言をすっ飛ばせて大変便利ですがあくまでもtupleなのでtupleが欲しい時だけ使うようにしましょう。
tupleである必要がないのであれば@dataclassで十分です。

TypedDict

項目の存在と型を型チェッカーによって強制されるdictを作ります。
でもそれって悪意のある言い方をすれば属性へのアクセスが面倒になった構造体だと思うんですけど…

from typing import TypedDict
from dataclasses import dataclass

class Point2D(TypedDict):  # 実行時はdictになる
    x: int
    y: int
    label: str

@dataclass
class Point3D:
    x: int
    y: int
    z: int
    label: str

td: Point2D
td = {"x": 1, "y": 2}  # NG
td = {"x": 1, "y": 2, "label": 3}  # NG
td = {"x": 1, "y": 2, "label": "ok"}  # OK
td = Point2D(1, 2, "ng")  # NG
td = Point2D(x=1, y=2, label="TypedDict")  # OK
td["label"]
>>>TypedDict
dc: Point3D
dc = Point3D(1, 2, 3, "ok")  # OK
dc = Point3D(x=1, y=2, z=3, label="dataclass")  # OK
dc.label
>>>dataclass

一応TypedDictはコンストラクタ引数が強制的にキーワード専用になること、dictのメソッドを扱えること、dictが持つ属性以外の属性が存在しないことを保証できること@dataclassにはないメリットですが、このメリットが美味しく見える場面はかなり限定的です。
使い分けを意識しなければいけない億劫さを考えると一律使わないで問題ないと思います。

cast

どうしても正規の方法では型チェッカーに型を認識させられない場合の緊急避難として変数の型情報を上書きするための関数でした。
3.10からはTypeGuardを使うのでcastの使用は禁止しましょう。

TypeGuardがない時代にはこうする必要がありました
from typing import Any, cast

def is_str_list(l: list[Any]) -> bool:
    return all(isinstance(x, str) for x in l)

def hoge(l: list[int | str]):
    if is_str_list(l):
        for elm in l:
            elm = cast(str, elm)  # ここで型チェッカーから見たelmの型がint | strからstrに変化する
            print(elm.lower())

@overload

なんちゃってオーバーロードです。
合併型型変数では対応しきれないような複雑な引数と戻り値の組み合わせを持つ関数にはこっちを使います。

from typing import overload

@overload
def hoge() -> None:
    ...

@overload
def hoge(x: int) -> tuple[int, str]:
    ...

@overload
def hoge(x: bytes) -> str:
    ...

def hoge(x=None):  # 実際の分岐はダラダラやるしかない(_hoge_int()/_hoge_bytes()みたいな関数を作ってもいいですが…)
    if x is None:
        return x
    if isinstance(x, int):
        return x, str(x)
    return str(x)

エディタでhoge関数を見てみると…
image.png
キチンと3パターンの情報が載っています。

@final

Finalのデコレータ版です。
メソッドのオーバーライドとクラスの継承を禁止します。

from typing import final

class Hoge:
    @final
    def hoge(self):  # @finalのついたメソッドをオーバーライドすることは出来ない
        ...

class HogeHoge(Hoge):
    def hoge(self):  # NG
        ...

@final
class Fuga:  # @finalのついたクラスを継承することは出来ない
    ...

class FugaFuga(Fuga):  # NG
    ...

@no_type_check / @no_type_check_decorator

型ではないアノテーションを書きたい時に使うものだったので3.9以降は使いません。
代わりに → Annotated

@type_check_only

スタブファイル(.pyi)にプライベートクラスを書くために使います(.pyの中では使えません)。
公式ドキュメントにもある通りこのデコレータを使うような状況はそもそも設計として推奨されないので存在自体を忘れてしまって大丈夫です。

おわりに

以上Introspection helpers(型アノテーションを解釈するためのヘルパー関数群)を除いた全オブジェクトの解説でした。
Pythonの型システムは3.9を迎えてほぼ完成したなと去年は思っていたのですが、こうしてまとめてみると3.10でも大きく進化していることが分かります。特にParamSpecTypeGuardなどは待望だった方も多いのではないでしょうか?

使わないものをバッサリ切ったりでかなり私見(というか思想)の強い解説にはなっていますが、この記事が世のPythonコードを堅牢に、そして綺麗にすることに貢献できれば幸いです。


  1. 型チェッカーとかあるいはそういったライブラリを作る人々 

  2. 普通にint?じゃダメだったんでしょうか? 

  3. その他にTrue/False/Noneを使うことが出来ますが解説が蛇足っぽかったので割愛 

  4. なのでよく見るif type(hoge) is type:も型の絞り込みには使えません 

  5. PEP 585より引用; The deprecated functionality will be removed from the typing module in the first Python version released 5 years after the release of Python 3.9.0. 

  6. コードが複雑になるので 

  7. 一応参考程度に私はIterator[yield]派です 

  8. ここの解説を入れるとそれだけで1記事分になってしまうのですみませんが省略させてください 

  9. 公式ドキュメントではこれをシグネチャと呼んでいるのでPythonの言うシグネチャには戻り値の型も含まれるということが分かりました 

  10. それはそうとアノテーションが実行に影響を与えるのは思想的にどうなんだろうという疑問はあります 

478
505
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
478
505