順番に読み進めながら学べます

デコレータ — @ で関数に処理を後付けする

Python のデコレータを基礎から解説します。@ で関数に処理を後付けする基本形から *args の素通し、引数付きデコレータまで一通り図解で押さえます。

前回のラムダ式までで、関数を値として扱う書き方を一通り見てきました。今回は仕上げとして、「関数に追加の処理を被せる」専用の書き方であるデコレータを整理します。

デコレータとは

デコレータとは、元の関数を変更せずに前後の処理を追加するための仕組みです。「ログを出力する」「実行時間を測る」「キャッシュを効かせる」のように、たくさんの関数に共通して被せたい処理を 1 か所にまとめておけます。

書き方は関数定義の上に @デコレータ名 を 1 行付けるだけ。これは「func = デコレータ名(func) と同じ意味」と Python が解釈してくれます。

@ は「関数を関数に通す」構文
@loggerdef greet():greet = logger(greet)展開
@loggerdef greet(): の上に書くと、Python は内部的に greet = logger(greet) を実行して greet を logger で包んだ新しい関数に置き換える。
# デコレータ本体(関数を受け取って関数を返す高階関数)
def logger(func):
    def wrapper():
        print("=== 開始 ===")
        func()                       # 元の関数を呼ぶ
        print("=== 終了 ===")
    return wrapper

# 使う側: @ を付けるだけ
@logger
def greet(): # → logger(greet)
    print("こんにちは")

greet()
# === 開始 ===
# こんにちは
# === 終了 ===

# 上は内部的にこれと同じ
# def greet():
#     print("こんにちは")
# greet = logger(greet)

基本のデコレータ — wrapper で関数を包む

デコレータ本体の骨格は、外側の関数で func を受け取り、内側の関数(慣習的に wrapper)で func() を呼び出して、wrapper を return するという 3 段構成です。func を覚えたまま動く wrapper は、クロージャそのものです。

func() の前後に書いた処理が、装飾された関数を呼ぶたびに実行されます。

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"[LOG] {func.__name__} を実行")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} 完了")
        return result
    return wrapper

@logger
def greet(name):
    return f"Hello, {name}"

print(greet("Alice"))
# [LOG] greet を実行
# [LOG] greet 完了
# Hello, Alice
logger デコレータが関数を包む流れ
モジュール(グローバル名前空間)
  • greetwrapper 関数に置き換わっている
  • 元の greet 本体は wrapper 内の func として残る
logger(func) のフレーム
  • func に元の greet が入る
  • 中で wrapper を作って return
wrapper(func を覚えたクロージャ)
  • 前処理 → func() → 後処理 を順に実行
  • 外から見るとこれが新しい greet
@logger の効果は「グローバル名前空間の greet を、wrapper という別の関数に差し替える」こと。元の greet 本体は wrapper の中で func として呼び出される。

前後にあいさつを表示する bracket デコレータを作って、関数に被せてみます。

def bracket(func): を定義し、その中に def wrapper(): を作ってください。wrapper の中で print("--- start ---")func()print("--- end ---") の順に実行し、外側で return wrapper を返してください。

@bracket を付けた def introduce(): を定義し、本体には print("私は太郎です") だけを書いてください。

introduce() を呼び出して、本文の前後に --- start --- / --- end --- が挟まれて表示されることを確認してください。

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

*args / **kwargs で任意の引数を素通しする

ここまでの wrapper は引数を取らない関数でした。引数を持つ関数も装飾したいときは、*args / **kwargs を使って任意の引数をそのまま受け取り、そのまま `func` に渡し直す書き方を使います。

これで「どんな引数の関数にも被せられる、汎用デコレータ」になります。add(2, 3) のような位置引数でも、add(2, 3, name="ABC") のようなキーワード引数でも、同じデコレータで捌けます。

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"呼び出し: args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)   # ここでも展開して渡す
        print(f"結果: {result}")
        return result                    # 戻り値もちゃんと返す
    return wrapper

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

print(add(2, 3))
# 呼び出し: args=(2, 3), kwargs={}
# 結果: 5
# 5

print(add(2, b=3))
# 呼び出し: args=(2,), kwargs={'b': 3}
# 結果: 5
# 5
*args / **kwargs で引数を「素通し」する流れ
add(2, b=3)wrapper(*args, **kwargs)args=(2,), kwargs={'b': 3}func(*args, **kwargs)に展開して渡すadd(a, b)元の add(a=2, b=3)5 を returnwrapper もそのまま return受け取り前処理展開計算結果を戻す

戻り値を return し忘れない

wrapper の中で result = func(...) だけ書いて return を忘れると、装飾された関数の戻り値が勝手に None に変わってしまいます。add(2, 3) の結果がいつの間にか None になっていた、という事故の典型なので、デコレータを書くときは return result を必ずセットで書くと覚えておきます。

呼び出し履歴をプリントする log_call デコレータを作り、引数 2 個の関数に被せます。

def log_call(func): を定義し、中に def wrapper(*args, **kwargs): を書いてください。

wrapper の中で print(f"call: args={args}, kwargs={kwargs}") を表示し、result = func(*args, **kwargs) で元の関数を呼んで、最後に return result してください。

③ 外側で return wrapper を返してください。

@log_call を付けた def multiply(a, b): return a * b を定義し、print(multiply(4, 5))print(multiply(2, b=10)) を呼び出して、ログ → 戻り値の順に表示されることを確認してください。

Python エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1次のうち、@loggerdef greet(): ... の上に付けるのと同じ意味になるのはどれですか?

Q2次のコードを実行したときの出力はどれですか?
def deco(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@deco
def plus(a, b):
return a + b
print(plus(3, 4))