functools.wraps を使うだけでは、デコレートされた関数のシグネチャが失われます。inspect.signature を活用すると、元のシグネチャを完全に保持できます。
問題の確認
from functools import wraps import inspect def decorator(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @decorator def greet(name: str, greeting: str = "Hello") -> str: return f"{greeting}, {name}!" print(inspect.signature(greet)) # (*args, **kwargs) ← 元のシグネチャが失われている
wraps はメタデータをコピーしますが、シグネチャは wrapper のものになります。
signature を設定する
from functools import wraps import inspect def decorator(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) wrapper.__signature__ = inspect.signature(func) return wrapper @decorator def greet(name: str, greeting: str = "Hello") -> str: return f"{greeting}, {name}!" print(inspect.signature(greet)) # (name: str, greeting: str = 'Hello') -> str
引数を追加するデコレータ
新しい引数を追加する場合、シグネチャも更新する必要があります。
from functools import wraps import inspect def with_debug(func): sig = inspect.signature(func) new_param = inspect.Parameter( "debug", inspect.Parameter.KEYWORD_ONLY, default=False ) new_params = list(sig.parameters.values()) + [new_param] new_sig = sig.replace(parameters=new_params) @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}") return func(*args, **kwargs) wrapper.__signature__ = new_sig return wrapper @with_debug def add(a: int, b: int) -> int: return a + b print(inspect.signature(add)) # (a: int, b: int, *, debug=False) -> int
引数の検証にシグネチャを活用
def validated(func): sig = inspect.signature(func) @wraps(func) def wrapper(*args, **kwargs): bound = sig.bind(*args, **kwargs) bound.apply_defaults() return func(*bound.args, **bound.kwargs) wrapper.__signature__ = sig return wrapper
sig.bind() は引数をパラメータにマッピングし、不足や過剰を検出します。シグネチャを保持することで、IDE の補完やドキュメント生成も正しく動作します。