Q1次のうち、@logger を def greet(): ... の上に付けるのと同じ意味になるのはどれですか?
デコレータ — @ で関数に処理を後付けする
Python のデコレータを基礎から解説します。@ で関数に処理を後付けする基本形から *args の素通し、引数付きデコレータまで一通り図解で押さえます。
前回のラムダ式までで、関数を値として扱う書き方を一通り見てきました。今回は仕上げとして、「関数に追加の処理を被せる」専用の書き方であるデコレータを整理します。
デコレータとは
デコレータとは、元の関数を変更せずに前後の処理を追加するための仕組みです。「ログを出力する」「実行時間を測る」「キャッシュを効かせる」のように、たくさんの関数に共通して被せたい処理を 1 か所にまとめておけます。
書き方は関数定義の上に @デコレータ名 を 1 行付けるだけ。これは「func = デコレータ名(func) と同じ意味」と Python が解釈してくれます。
@logger を def 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
- greet は wrapper 関数に置き換わっている
- 元の
greet本体はwrapper内のfuncとして残る
funcに元のgreetが入る- 中で
wrapperを作ってreturn
- 前処理 →
func()→ 後処理 を順に実行 - 外から見るとこれが新しい
greet
*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
戻り値を return し忘れない
wrapper の中で result = func(...) だけ書いて return を忘れると、装飾された関数の戻り値が勝手に None に変わってしまいます。add(2, 3) の結果がいつの間にか None になっていた、という事故の典型なので、デコレータを書くときは return result を必ずセットで書くと覚えておきます。
理解度チェック
まずは1問ずつ答えてみましょう。
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))