【Python】自作デコレーターで関数名が消える?wrapsで使いやすく整える

PythonのTopに戻る


デコレーターを付けた関数の名前がwrapperになり、ヘルプやログが分かりにくくなる場合は、内側の関数へfunctools.wrapsを付ける。元の関数名や説明文などを引き継げる。ただし、wrapsが計算結果や例外の扱いまで自動で直すわけではない。戻り値を返し、元の例外を不用意に隠さないラッパーを書くことも大切である。

関数を置き換えると情報も変わる

デコレーターは元の関数を受け取り、別の呼び出し可能オブジェクトを返す。よくある構成では、内側に定義したwrapperが新しい関数として使われる。そのままだと、__name__や__doc__はwrapperのものになり、利用者が元の関数を調べにくくなる。

例えば入力値の検査を複数の関数へ共通に追加したい場合、同じ検査を各関数へコピーする代わりにデコレーターを使える。共通処理をまとめる利点を保ちながら、元の関数の説明も残すのがwrapsの役割になる。

入力検査を加え、名前と説明を残す

ここではvalueという引数を持つ関数だけを対象にする。inspect.signatureで引数を対応付けるので、位置で渡してもキーワードで渡しても同じ値を検査できる。どんな関数にも無条件で使える万能な検査ではないことを、名前と実装で明確にしている。

example.py

from functools import wraps
from inspect import signature


def nonnegative_value(func):
    sig = signature(func)
    if "value" not in sig.parameters:
        raise TypeError("A value parameter is required")

    @wraps(func)
    def wrapper(*args, **kwargs):
        bound = sig.bind(*args, **kwargs)
        bound.apply_defaults()
        if bound.arguments["value"] < 0:
            raise ValueError("value must be nonnegative")
        return func(*args, **kwargs)

    return wrapper


@nonnegative_value
def convert(value, *, factor=2):
    """Multiply a nonnegative value by factor."""
    return value * factor


print(convert.__name__)
print(signature(convert))
print(convert(value=3, factor=4))
assert convert.__doc__ == "Multiply a nonnegative value by factor."
assert convert(3) == 6 and convert(value=3, factor=4) == 12
assert convert.__wrapped__(3, factor=4) == 12
try:
    convert(-1)
except ValueError:
    print("ValueError")
else:
    raise AssertionError("Negative input must fail")
try:
    convert()
except TypeError:
    pass
else:
    raise AssertionError("Missing input must fail")

実行結果

convert
(value, *, factor=2)
12
ValueError

@wraps(func)をwrapperへ付けたことで、関数名はconvertのままになる。inspect.signatureは通常、__wrapped__をたどって元のシグネチャを示す。この表示と、実際にwrapperがどんな引数を受け取りどう転送するかは、両方を確認する必要がある。

戻り値と例外を保つ

wrapperの最後はreturn func(*args, **kwargs)としている。returnを忘れると、元の関数は正しく計算しても利用側にはNoneが返る。ログや検査を加えたときに起こりやすいので、正常な呼び出しの戻り値を必ず確かめよう。

例では自分で追加した非負の検査だけがValueErrorを起こし、引数不足などはsignature.bindがTypeErrorとして知らせる。元の関数内で起きる例外を一括してNoneやFalseへ置き換える処理は入れていない。失敗の意味を変えるなら、それも関数の契約として説明する必要がある。

wrapsが保つものと保たないもの

wrapsは関数のメタデータを引き継ぎ、元の関数へたどれる__wrapped__も設定する。元の処理を丸ごと複製したり、wrapperの実装を検証したりするものではない。入力の検査が正しいか、引数の転送が正しいかは別に確認する。

__wrapped__から元の関数へ直接アクセスできることもあるため、デコレーターだけを強固な認可や安全境界として扱わない方がよい。この例の検査は、通常の関数利用で入力ミスを早めに見つけるためのものである。

検査対象の意味を限定する

この例は、比較可能な数値をvalueとして受け取る前提である。文字列、複素数、NumPy配列などを渡した場合まで、value < 0が期待どおりに動くとは限らない。汎用化するなら、受け入れる型や欠損値、配列全体の扱いを別途設計する必要がある。

signature.bindとapply_defaultsを使うと、渡された引数と既定値を名前で確認しやすい。ただし、関数によってはシグネチャを取得できない場合もある。通常のPython関数から始め、対応範囲を広げる際には対象の呼び出し形式も調べよう。

重ねる順序と呼び出し方を確認する

複数のデコレーターを重ねると、適用する順序によって処理の順番も変わる。入力検査、キャッシュ、ログを組み合わせた場合、キャッシュ命中時にどこまで実行されるかなどが違ってくる。個別に動くことだけでなく、組合せでも意図を確認したい。

まずは名前・説明文・シグネチャを表示し、位置引数とキーワード引数の両方で呼び、戻り値と失敗時の例外を確かめる。見た目の情報を保つwrapsと、処理の意味を保つwrapperの実装をセットで考えると、使いやすいデコレーターになる。

動作確認と参考資料

掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。

関連するTips


PythonのTopに戻る