Python の ParamSpec で引数を完全に保持する型付け
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 により、デコレータの型安全性が大幅に向上します。