【Python】デコレータ(@)の使い方|functools.wraps・引数・スタックの順番

【Python】デコレータ(@)の使い方|functools.wraps・引数・スタックの順番 Python

デコレータ(decorator)は、関数を包んで、その前後に処理を追加する仕組みです。@記号を関数の上に書くだけで、元の関数のコードを変えずに、ログ出力・実行時間の計測・アクセス制御といった機能を足せます。FlaskやFastAPIなどのフレームワークでも@app.routeのような形で多用される、Pythonらしい便利な機能です。

つまずきやすいのは、引数のある関数に対応するための*args**kwargs、そしてfunctools.wrapsを付けないと元の関数の名前(__name__)が失われることです。さらに、複数のデコレータを重ねたときの実行順も間違えやすいポイントです。この記事では、実機のPythonで確認しながら、デコレータを整理します。

先に結論

  • デコレータは関数を受け取り、包んだ関数を返す関数です。@デコレータ名で適用します。
  • 中のwrapper*args, **kwargsで引数を受け取り、元の関数にそのまま渡します
  • 元の関数の戻り値は、wrapperreturnして返します。
  • @functools.wraps(func)を付けると、元の関数の名前やドキュメントが保たれます。
  • 複数重ねると、関数に近いほうが先に適用され、外側が後から包みます。
  • 引数を取るデコレータは、デコレータを返す関数として3段階で書きます。

デコレータの土台になる関数(def)の使い方、メッセージ整形に使うf-string、繰り返し処理のenumerate/zipもあわせて参考になります。

スポンサーリンク

デコレータとは(関数を包む)

Pythonでは関数も「値」として扱え、関数を引数に取ったり、関数を返したりする関数が書けます。デコレータはこれを利用して、元の関数を別の関数(wrapper)で包む仕組みです。包むことで、元の処理の前後に好きな処理を挟めます。

基本のデコレータ

もっとも基本的なデコレータを見てみます。funcを受け取り、その前後に処理を足したwrapperを返します。使うときは関数の上に@デコレータ名を書きます。

基本のデコレータ
def my_decorator(func):
    def wrapper():
        print("処理の前")
        func()              # 元の関数を実行
        print("処理の後")
    return wrapper

@my_decorator
def hello():
    print("こんにちは")

hello()
# 処理の前
# こんにちは
# 処理の後

@my_decoratorを付けたhelloを呼ぶと、処理の前こんにちは処理の後の順で実行されました。@my_decoratorは、実はhello = my_decorator(hello)を書いたのと同じです。helloという名前が、my_decoratorが返したwrapperに置き換わるイメージです。これにより、hello本体のコードを一切変えずに、前後の処理を追加できます。

引数のある関数に対応する

実際の関数は引数や戻り値を持ちます。どんな関数にも対応できるように、wrapper*args, **kwargsで引数を受け取り、それを元の関数にそのまま渡し戻り値をreturnで返します

引数と戻り値に対応する
def my_decorator(func):
    def wrapper(*args, **kwargs):       # どんな引数でも受け取れる
        print("前")
        result = func(*args, **kwargs)  # そのまま元の関数へ渡す
        print("後")
        return result                   # 戻り値を返す
    return wrapper

@my_decorator
def add(a, b):
    return a + b

print(add(2, 3))
# 前
# 後
# 5
*argsと**kwargsで「どんな関数でも」包める

実機で確認したところ、wrapper(*args, **kwargs)とし、func(*args, **kwargs)で渡すことで、引数のある関数(add(2, 3))も正しく動き、戻り値5も受け取れました。*argsは位置引数をタプルで、**kwargsはキーワード引数を辞書でまとめて受け取る仕組みです。これを使うと、引数の数や種類が違うどんな関数にも対応できるデコレータになります。逆に、wrapper()のように引数を受け取らないと、引数のある関数に付けたときにエラーになります。また、result = func(...)の戻り値をreturnし忘れると、デコレータを付けた関数の戻り値がNoneになってしまうので注意してください。デコレータのテンプレートとして、この*args, **kwargsreturnの形を覚えておくと便利です。

functools.wrapsを付ける

注意したいのが、デコレータを付けると元の関数の名前(__name__)やドキュメントが失われることです。helloのはずがwrapperになってしまいます。これを防ぐのがfunctools.wrapsです。

functools.wraps で元の情報を保つ
import functools

def my_decorator(func):
    @functools.wraps(func)      # これを付ける
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def hello():
    """あいさつする関数"""
    print("こんにちは")

print(hello.__name__)   # wraps なし → wrapper / wraps あり → hello
functools.wrapsが無いと関数の名前が消える

実機で確認したところ、functools.wrapsを付けないデコレータでは、hello.__name__が本来のhelloではなくwrapperになってしまいました。functools.wraps(func)wrapperの上に付けると、__name__が正しくhelloに保たれます。名前がwrapperになると、デバッグ時に「どの関数か分からない」「ドキュメント生成ツールが正しく動かない」といった問題が起こります。デコレータを書くときは、wrapperの上に@functools.wraps(func)を付けるのがお約束です。元の関数のドキュメント文字列(docstring)も一緒に保たれるため、付けておいて損はありません。

複数デコレータのスタック(順番)

デコレータは複数重ねて使えます。このとき、関数に近いほうが先に適用され、外側が後から包みます。実行されたときの順番は、外側から入って外側で終わる「玉ねぎ」のような形になります。

デコレータを重ねる
def deco_a(func):
    def wrapper(*a, **k):
        print("A開始"); r = func(*a, **k); print("A終了"); return r
    return wrapper

def deco_b(func):
    def wrapper(*a, **k):
        print("B開始"); r = func(*a, **k); print("B終了"); return r
    return wrapper

@deco_a      # 外側(後から包む)
@deco_b      # 内側(関数に近い・先に適用)
def greet():
    print("本体")

greet()
# A開始
# B開始
# 本体
# B終了
# A終了

実機でも、@deco_a@deco_bの順で重ねたgreet()を実行すると、A開始B開始本体B終了A終了の順になりました。関数にもっとも近い@deco_bが先に関数を包み、その外側を@deco_aが包みます。実行時は外側のdeco_aから入り、内側のdeco_bを通って本体に到達し、戻りは内側から外側へ抜けていきます。「上に書いたデコレータほど外側」と覚えておくと、順番を間違えません。

引数を取るデコレータ

@repeat(3)のようにデコレータ自身に引数を渡したいこともあります。その場合は、「引数を受け取ってデコレータを返す関数」という3段階の構造にします。少し複雑ですが、形は決まっています。

引数を取るデコレータ
import functools

def repeat(times):                     # ① 引数を受け取る
    def decorator(func):               # ② デコレータ本体
        @functools.wraps(func)
        def wrapper(*args, **kwargs):  # ③ 包む関数
            for _ in range(times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def say():
    print("やあ")

say()
# やあ
# やあ
# やあ

実機でも、@repeat(3)を付けたsay()が3回実行されました。引数付きデコレータはrepeat(times)decoratorを返し、decorator(func)wrapperを返す」という3重の入れ子になります。@repeat(3)と書くと、まずrepeat(3)が呼ばれてdecoratorが得られ、それがsayに適用される、という流れです。最初は複雑に見えますが、「外側に引数用の関数をもう1つ被せる」と考えると理解しやすくなります。

主なポイントまとめ

デコレータの要点をまとめます。

項目 ポイント
適用 @デコレータ名を関数の上に書く
引数の受け渡し wrapper(*args, **kwargs)func(*args, **kwargs)
戻り値 return func(...)を忘れない
名前を保つ @functools.wraps(func)を付ける
重ねる順 上に書くほど外側(後から包む)
引数付き デコレータを返す関数として3段階で書く

よくある失敗

wrapperで引数を受け取らない

引数のある関数に付けるとエラーです。wrapper(*args, **kwargs)とします。

戻り値をreturnし忘れる

デコレータを付けた関数の戻り値がNoneになります。return func(...)します。

functools.wrapsを付けない

関数名がwrapperになります。@functools.wraps(func)を付けます。

複数デコレータの順番を逆に考える

上に書いたものが外側です。実行は外側から入って外側で終わります。

引数付きデコレータを2段階で書く

引数を取るには3段階必要です。引数用の関数をもう1つ外側に被せます。

よくある質問

Qデコレータとは何ですか?
A関数を受け取り、その前後に処理を追加した新しい関数を返す仕組みです。@デコレータ名を関数の上に書くだけで、元の関数のコードを変えずにログ出力や計測などの機能を足せます。@my_decofunc = my_deco(func)と書くのと同じ意味です。
Q引数のある関数にデコレータを付けるには?
A中のwrapperwrapper(*args, **kwargs)として、func(*args, **kwargs)で元の関数に引数をそのまま渡します。これで引数の数や種類が違うどんな関数にも対応できます。戻り値もreturn func(...)で返すのを忘れないでください。
Qfunctools.wrapsは何のために付けるのですか?
Aデコレータを付けると、関数の名前(__name__)やドキュメントがwrapperのものに置き換わってしまいます。@functools.wraps(func)wrapperの上に付けると、元の関数の名前やドキュメントが保たれます。デバッグやドキュメント生成のために、付けるのが基本です。
Q複数のデコレータを付けると、どの順で実行されますか?
A関数にもっとも近い(下に書いた)デコレータが先に関数を包み、上に書いたものが外側になります。実行時は外側から入り、内側を通って本体に到達し、戻りは内側から外側へ抜けます。「上に書くほど外側」と覚えると分かりやすいです。
Q@repeat(3)のように引数を取るデコレータはどう書きますか?
A「引数を受け取ってデコレータを返す関数」という3段階の構造にします。repeat(times)decoratorを返し、decorator(func)wrapperを返す形です。通常のデコレータの外側に、引数を受け取る関数をもう1つ被せると考えると理解しやすいです。

まとめ

  • デコレータは関数を包んで前後に処理を足す仕組み。@名前で適用します。
  • wrapper(*args, **kwargs)return func(*args, **kwargs)がテンプレートです。
  • @functools.wraps(func)で元の関数の名前を保ちます。
  • 複数重ねると上に書くほど外側。実行は外側から入って外側で終わります。
  • 引数付きデコレータはデコレータを返す関数として3段階で書きます。

デコレータは最初こそ複雑に見えますが、*args, **kwargsreturnfunctools.wrapsという「型」を覚えてしまえば、あとは応用です。ログ・計測・キャッシュ・認証など、共通処理を関数から切り出して再利用する強力な手段になります。まずは基本のデコレータから書いて、少しずつ慣れていきましょう。