ParamSpec は Python 3.10 で導入され、デコレータで元の関数の引数型を完全に保持できます。従来の Callable[..., T] では失われていた情報を維持できます。
問題の背景
従来の型付けでは、デコレートされた関数の引数情報が失われます。
from typing import Callable, TypeVar T = TypeVar("T") def decorator(func: Callable[..., T]) -> Callable[..., T]: def wrapper(*args, **kwargs) -> T: return func(*args, **kwargs) return wrapper
wrapper の型は Callable[..., T] となり、具体的な引数の型がわかりません。
ParamSpec の基本
from typing import ParamSpec, TypeVar, Callable P = ParamSpec("P") T = TypeVar("T") def decorator(func: Callable[P, T]) -> Callable[P, T]: def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: print("Before call") result = func(*args, **kwargs) print("After call") return result return wrapper @decorator def greet(name: str, count: int = 1) -> str: return f"Hello, {name}!" * count
型チェッカーは greet の型を (name: str, count: int = 1) -> str と正しく認識します。
P.args と P.kwargs
ParamSpec は 2 つの特殊属性を持ちます。
P.args
位置引数の型を表す。*args の型注釈に使う。
P.kwargs
キーワード引数の型を表す。**kwargs の型注釈に使う。
実用例:リトライデコレータ
from typing import ParamSpec, TypeVar, Callable P = ParamSpec("P") T = TypeVar("T") def retry(times: int) -> Callable[[Callable[P, T]], Callable[P, T]]: def decorator(func: Callable[P, T]) -> Callable[P, T]: def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: for attempt in range(times): try: return func(*args, **kwargs) except Exception: if attempt == times - 1: raise raise RuntimeError("Unreachable") return wrapper return decorator @retry(3) def fetch(url: str, timeout: int = 30) -> bytes: ...
fetch の型情報は完全に保持され、IDE の補完も正しく動作します。ParamSpec により、デコレータの型安全性が大幅に向上します。