高校化学2916128 views
高校物理158718 views
Computer366119 views
小学算数1197058 views
高校日本史190051 views
教育149096 views
小学理科718021 views
中学理科1627838 views
高校倫理1435514 views
いろは2995081 views

Python でシグネチャを保持するデコレータ(inspect.signature の活用)

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 の補完やドキュメント生成も正しく動作します。