アノテーションが実行時に実際は何をしているのかを理解し、frozen、slots、default_factoryでdataclassを最大限に活かしましょう。
このレッスンを Kodokon で開く型アノテーションは制約ではなくメタデータです。インタープリターはそれらを__annotations__に保存するだけで、実行時に検証することは決してありません。検証はmypyやpyrightのような静的解析ツールが、実行より前に行う仕事です。その直接の帰結として、型付けが誤ったプログラムも文句を言われずに動きます - これは意図的な設計です。漸進的型付けのおかげで、既存のコードベースを何も壊さずに少しずつアノテーションできます。
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はすべてのアノテーションを遅延評価(文字列として保存)にします。これにより、後ろで定義されたクラスを参照できるようになり、インポート時のコストも下がります。
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])))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"]))三つのパラメーターが@dataclassを本番品質の道具に変えます。frozen=Trueはインスタンスをイミュータブルにし、自動生成される等価比較と組み合わさってハッシュ可能にします - 辞書のキーとして使えるということです。slots=True(Python 3.10以降)はインスタンスごとの__dict__を取り除きます。メモリが減り、属性アクセスが速くなり、想定外の属性への代入は失敗するようになります。最後にkw_only=Trueはキーワード指定での明示的な呼び出しを強制します。不変条件や派生フィールドの置き場所は__post_init__です。単純なイミュータブルな集合体ならNamedTupleにも依然として意味がありますが、振る舞いや検証が入ってきたらdataclassを選びましょう。
(a: int, b: int) -> intとアノテーションされているときにadd('py', 'thon')を呼び出すと、実行時に何が起きますか?items: list[str] = []はdataclassのフィールドのデフォルト値として禁じられているのですか?list[str]はフィールドの有効なアノテーションではないからfrozen=Trueはdataclassに何をもたらしますか?