Kodokon kodokon.com

型ヒントとdataclass:モダンなPythonの型付けを本気でやる

アノテーションが実行時に実際は何をしているのかを理解し、frozen、slots、default_factoryでdataclassを最大限に活かしましょう。

10 分 · 3 問

このレッスンを Kodokon で開く

型アノテーションは制約ではなくメタデータです。インタープリターはそれらを__annotations__に保存するだけで、実行時に検証することは決してありません。検証はmypypyrightのような静的解析ツールが、実行より前に行う仕事です。その直接の帰結として、型付けが誤ったプログラムも文句を言われずに動きます - これは意図的な設計です。漸進的型付けのおかげで、既存のコードベースを何も壊さずに少しずつアノテーションできます。

PYTHON
from typing import get_type_hints

def add(a: int, b: int) -> int:
    return a + b

print(add("py", "thon"))
print(add.__annotations__)
print(get_type_hints(add))
アノテーションは何も止めない:「不正な」呼び出しも成功する

Python 3.9以降、組み込み型はそのままジェネリックです。typing.Listをインポートせずにlist[int]dict[str, float]と書けます。3.10以降、ユニオンはint | Noneと書きます。入力の型と出力の型を結びつけるにはTypeVarを使います - Python 3.12ではdef first[T](items: list[T]) -> Tというコンパクトな構文まで用意されています。見落とされがちな点として、from __future__ import annotationsはすべてのアノテーションを遅延評価(文字列として保存)にします。これにより、後ろで定義されたクラスを参照できるようになり、インポート時のコストも下がります。

PYTHON
from collections.abc import Iterable, Iterator
from typing import TypeVar

T = TypeVar("T")

def dedupe(items: Iterable[T]) -> Iterator[T]:
    seen: set[T] = set()
    for item in items:
        if item not in seen:
            seen.add(item)
            yield item

print(list(dedupe([3, 1, 3, 2, 1])))
TypeVarはdedupe(list[int])が本当にintを返すことを保証する
PYTHON
from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float

@dataclass
class Basket:
    items: list[str] = field(default_factory=list)
    total: int = 0

    def __post_init__(self) -> None:
        self.total = len(self.items)

point = Point(1.0, 2.0)
print(point, hash(point))
print(Basket(["book", "pen"]))
Pointはイミュータブルでハッシュ可能、インスタンスごとの__dict__を持たない

三つのパラメーターが@dataclassを本番品質の道具に変えます。frozen=Trueはインスタンスをイミュータブルにし、自動生成される等価比較と組み合わさってハッシュ可能にします - 辞書のキーとして使えるということです。slots=True(Python 3.10以降)はインスタンスごとの__dict__を取り除きます。メモリが減り、属性アクセスが速くなり、想定外の属性への代入は失敗するようになります。最後にkw_only=Trueはキーワード指定での明示的な呼び出しを強制します。不変条件や派生フィールドの置き場所は__post_init__です。単純なイミュータブルな集合体ならNamedTupleにも依然として意味がありますが、振る舞いや検証が入ってきたらdataclassを選びましょう。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. 関数が(a: int, b: int) -> intとアノテーションされているときにadd('py', 'thon')を呼び出すと、実行時に何が起きますか?
    • PythonがただちにTypeErrorを送出する
    • 呼び出しは普通に実行される。アノテーションは実行時に検証されることが決してない
    • PythonがRuntimeWarningを出したうえで呼び出しを実行する
    • モジュールのインポートが拒否される
  2. なぜitems: list[str] = []はdataclassのフィールドのデフォルト値として禁じられているのですか?
    • list[str]はフィールドの有効なアノテーションではないから
    • dataclassはデフォルト値のないフィールドしか許さないから
    • このミュータブルなデフォルト値がすべてのインスタンスで共有されてしまうから。dataclassはValueErrorを送出し、field(default_factory=list)を要求する
    • デフォルト値は__post_init__で設定しなければならないから
  3. frozen=Trueはdataclassに何をもたらしますか?
    • インスタンスがイミュータブルになり、自動生成される等価比較と合わせてハッシュ可能になる - 辞書のキーとして使える
    • そのクラスをもう継承できなくなる
    • フィールドが共有されるクラス属性になる
    • 代入のたびにアノテーションが検証される