3.10までのまとめ ↓
知識のアップデートやっていきます。
3.11
可変長ジェネリクス
複数の値を持つことができる型変数TypeVarTuple
が追加されました。
簡単な例をあげると…
from typing import TypeVarTuple
Ts = TypeVarTuple("Ts")
def variadic_generics(*args: *Ts) -> tuple[*Ts]:
return args
のような関数を定義した場合、
ret1 = variadic_generics(1, "hoge")
ret2 = variadic_generics(2, "fuga", False)
ret1
はtuple[int, str]
に、ret2
はtuple[int, str, bool]
にそれぞれ正しく推論されますよという感じです。
が、この構文は3.12で早速使わなくなりました。 → 後述
一部のキーが省略可能なTypedDict
厳格な型のdict
がほしい場合に用いられるTypedDict
は従来全てのキーが存在することを強制されていましたが、3.11からは省略可能なキーを設定することができるようになりました。
from typing import TypedDict
class Movie(TypedDict):
title: str
year: int
m1: Movie = {"title": "Black Panther", "year": 2018} # OK
m2: Movie = {"title": "Star Wars"} # Error
from typing import TypedDict, NotRequired
class Movie(TypedDict):
title: str
year: NotRequired[int]
m1: Movie = {"title": "Black Panther", "year": 2018} # OK
m2: Movie = {"title": "Star Wars"} # OK!
とは言えデータクラスで済む場合は@dataclass
で問題ありません。
@dataclass
class Movie:
title: str
year: int | None = None
m1 = Movie("Black Panther", 2018) # OK
m2 = Movie("Star Wars") # OK
あくまでもdict
がほしい場合にのみ使用しましょう。
Self型
代替コンストラクタなど自分自身を返すメソッドの戻り値に雑にアノテーションをつけられなかったもやもやが解消されました。
from dataclasses import dataclass
from typing import Self
@dataclass
class MyInt:
val: int
@classmethod
def fromhex(cls, s: str) -> Self:
return cls(int(s, 16))
ちなみに通常メソッドの第一引数self
はSelf
、クラスメソッドのcls
はtype[Self]
型ですがそこまでするかどうかはプロジェクト次第ですね。私はやらないと思います1。
文字列リテラル型
セキュリティの観点などからリテラルではない文字列を弾きたいような場面で使えるLiteralString
型が追加されました。
from typing import LiteralString
def run_query(sql: LiteralString) -> ...:
...
run_query("SELECT * FROM students") # OK
run_query(input()) # Error
run_query( # f文字列もstr型のためError
f"SELECT * FROM students WHERE name = {input()}"
)
データクラスへの変換
データクラスのようなインターフェースを持つサードパーティ製ライブラリのクラスをデータクラスと同じように型検査できるようにするためのデコレータ@dataclass_transform()
が追加されました。
そもそものペインなど詳しくは ↓
ボトム型
従来PythonではNoReturn
を転用してボトム型を表現していましたが、正式にNever
型が使えるようになりました。
from typing import Never
def never_call_me(arg: Never) -> None:
pass
def int_or_str(arg: int | str) -> None:
match arg:
case int():
print("It's an int")
case str():
print("It's a str")
case _:
never_call_me(arg) # OK
3.12
型パラメータ構文
3.12からは型エイリアスの書き方とジェネリクスの書き方が大きく変わります。
まずtype
文が追加されたことにより型エイリアスがこのように書けるようになりました。
type Point = tuple[float, float]
更にこれをジェネリックにしたい場合はこうです。
type Point[T] = tuple[T, T]
一番のポイントは型変数を事前に定義しなくてもよいことで、これによってTypeVar
やTypeVarTuple
、ParamSpec
などをインポートする必要がなくなりました。
type IntFunc[**P] = Callable[P, int] # ParamSpec
type LabeledTuple[*Ts] = tuple[str, *Ts] # TypeVarTuple
type HashableSequence[T: Hashable] = Sequence[T] # TypeVar with bound
type IntOrStrSequence[T: (int, str)] = Sequence[T] # TypeVar with constraints
関数やクラスについても同様の書き方でジェネリックにすることが可能で、特にクラスについては明示的にGeneric
を継承する必要もなくなりました。
from typing import Iterable
def max[T](args: Iterable[T]) -> T:
...
class MyList[T]:
def __getitem__(self, index: int, /) -> T:
...
def append(self, element: T) -> None:
...
なおtype
はソフトキーワード(特定の文脈でのみ予約語)だそうです。
可変長キーワード引数の型付け
def print_(**kwargs: str):
print(kwargs)
print_(hoge="hoge", fuga="fuga") # OK
print_(hoge="hoge", piyo=1) # `piyo`の値がstr型ではないためError
このように可変長キーワード引数は全ての値が同一の型でなければ正しくアノテーションすることができないので何とかしたいと。
そこまでするなら素直に構造体を受け取ればいいのに… と思いPEPを見にいったところ
As described in the Intended Usage section, using **kwargs is not always the best tool for the job. Despite that, it is still a widely used pattern. As a consequence, there has been a lot of discussion around supporting more precise **kwargs typing and it became a feature that would be valuable for a large part of the Python community. This is best illustrated by the mypy GitHub issue 4441 which contains a lot of real world cases that could benefit from this propsal.
要約:
**kwargs
を使うことがベストじゃないのは分かってるけどむっちゃ使われちゃってるししょうがない
などと書かれており悲しい気持ちになりました。紹介は割愛します。
明示的オーバーライド
@override
デコレータが追加され、メソッドのオーバーライドを明示できるようになりました。
from typing import override
class Base:
def log_status(self) -> None:
...
class Sub(Base):
@override
def log_statas(self) -> None: # typoによりオーバーライドできてないためError
...
うっかりを防ぐ意味でも使った方がいいと思います。
3.13
デフォルト型引数
ジェネリクスにデフォルトの型を指定できるようになりました。
from dataclasses import dataclass
from typing import TypeVar
ET = TypeVar("ET", bound=Exception, default=SyntaxError)
@dataclass
class MyErrorEvent[ET]:
error: ET
type: str
ただし現在サポートされているのはTypeVar
/ TypeVarTuple
/ ParamSpec
の引数に直接渡してあげる方法のみで、
from dataclasses import dataclass
@dataclass
class MyErrorEvent[T: Exception = SyntaxError]:
error: T
type: str
このようにはまだ書けないようです。
一部の値が読み取り専用なTypedDict
from typing import TypedDict, ReadOnly
class Movie(TypedDict):
title: ReadOnly[str]
year: int
def mutate_movie(m: Movie) -> None:
m["year"] = 1999 # OK
m["title"] = "The Matrix" # Error
TypedDict
の機能が拡張されまくっているのはどういう理由からなんでしょうか?一部だけReadOnlyにしたいという需要もよく分かりませんし普通に@dataclass
でイミュータブルなオブジェクトを作ればいいだけのように思います。
from dataclasses import dataclass
# `frozen`を指定することで各値が変更不可になる
@dataclass(frozen=True)
class Movie:
title: str
year: int
def mutate_movie(m: Movie) -> None:
m.year = 1999 # Error
m.title = "The Matrix" # Error
賢くなったユーザー定義型ガード
従来のTypeGuard
は元々の型情報が考慮されないため、isinstance()
のような分岐を書こうとしてもうまく型を絞り込めないということがよくありました。
def typeguard(x: str | int) -> None:
if isinstance(x, str):
reveal_type(x) # str
else:
reveal_type(x) # int
from typing import TypeGuard
def is_str(x: object) -> TypeGuard[str]:
return isinstance(x, str)
def typeguard(x: str | int) -> None:
if is_str(x):
reveal_type(x) # str
else:
reveal_type(x) # str | int ← ?
新しく追加されたTypeIs
ではこの点が解消され、isinstance()
などと同じように型を絞り込むことが可能になっています。
from typing import TypeIs
def is_str(x: object) -> TypeIs[str]:
return isinstance(x, str)
def typeguard(x: str | int) -> None:
if is_str(x):
reveal_type(x) # str
else:
reveal_type(x) # int ← !
もう少し詳しい挙動 ↓
from typing import TypeGuard, TypeIs, final, reveal_type
class Base: ...
class Child(Base): ...
@final
class Unrelated: ...
def is_base_typeguard(x: object) -> TypeGuard[Base]:
return isinstance(x, Base)
def is_base_typeis(x: object) -> TypeIs[Base]:
return isinstance(x, Base)
# TypeGuard[Base]がTrueの場合xはBase型として扱われる
def use_typeguard(x: Child | Unrelated) -> None:
if is_base_typeguard(x):
reveal_type(x) # Base
else:
reveal_type(x) # Child | Unrelated
# TypeIs[Base]がTrueの場合xは(Child | Unrelated)型とBase型との交差をとる
def use_typeis(x: Child | Unrelated) -> None:
if is_base_typeis(x):
reveal_type(x) # Child
else:
reveal_type(x) # Unrelated
なおdef guard(x: TypeA) -> TypeIs[TypeB]
におけるTypeB
はTypeA
のサブタイプである必要があるため、例えば
def is_str_list(val: list[object]) -> TypeIs[list[str]]: # Error
return all(isinstance(x, str) for x in val)
このように書くとエラーが出ます。この場合は従来通りTypeGuard
を使用しましょう。
おわりに
というわけで3.13までのまとめでした。個人的にはジェネリクスの書き方が変わったのがかなりいいなと思っているので積極的に広めていきたいです。逆にTypedDict
周りはちょっとやりすぎな気がしますね…。
それではまた3.16でお会いしましょう。
-
自明すぎるので ↩