Python の Concatenate による引数の追加

Concatenate は ParamSpec と組み合わせて、関数の引数に新しいパラメータを追加する型を表現します。

基本的な使い方

from typing import Callable, Concatenate, ParamSpec, TypeVar

P = ParamSpec("P")
T = TypeVar("T")

def with_context(
    func: Callable[Concatenate[str, P], T]
) -> Callable[P, T]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        context = "default_context"
        return func(context, *args, **kwargs)
    return wrapper

Concatenate[str, P] は「最初に str 型の引数があり、その後に P の引数が続く」という意味です。

実用例:認証付きデコレータ

from typing import Callable, Concatenate, ParamSpec, TypeVar

P = ParamSpec("P")
T = TypeVar("T")

class User:
    def __init__(self, name: str):
        self.name = name

def authenticated(
    func: Callable[Concatenate[User, P], T]
) -> Callable[P, T]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        user = User("current_user")  # 実際は認証処理
        return func(user, *args, **kwargs)
    return wrapper

@authenticated
def get_profile(user: User, detailed: bool = False) -> dict:
    return {"name": user.name, "detailed": detailed}

# 呼び出し時は user を渡さない
result = get_profile(detailed=True)

メソッドに self を追加

from typing import Callable, Concatenate, ParamSpec, TypeVar

P = ParamSpec("P")
T = TypeVar("T")
S = TypeVar("S")

def bind_self(
    func: Callable[Concatenate[S, P], T]
) -> Callable[P, T]:
    ...

Concatenate の位置

Concatenate の追加パラメータは常に先頭に来ます。

# 正しい
Concatenate[int, str, P]  # int, str, その後に P の引数

# 不正(Concatenate の後に他のパラメータは置けない)
# Concatenate[P, int]  # エラー

Concatenate を使うと、デコレータが引数を追加・削除する場合でも、正確な型情報を維持できます。型チェッカーが呼び出しの正当性を検証できるため、バグの早期発見に役立ちます。