Q1次のうち、@loggerをdef greet(): ...の上に付けるのと同じ意味になるのはどれですか?
デコレータ — @ で関数に処理を後付けする
@loggerはgreet = logger(greet)と同じ意味。def → wrapper → return wrapperの3段構成と、*args / **kwargsでどんな引数の関数にも被せられる素通しパターンを扱います。
前回のラムダ式までで、関数を値として扱う書き方を一通り見てきました。今回は仕上げとして、「関数に追加の処理を被せる」専用の書き方であるデコレータを整理します。
デコレータとは
デコレータとは、元の関数を変更せずに前後の処理を追加するための仕組みです。「ログを出力する」「実行時間を測る」「キャッシュを効かせる」のように、たくさんの関数に共通して被せたい処理を 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))