こんにちは、かつコーチです。
Pythonのコードを読んでいると、関数の上に@から始まる記述がついているのを見かけることがあります。
「この@login_requiredみたいなのは何?」
「デコレータって難しそうで避けてきた」
そう感じている方も多いのではないでしょうか。
この記事では、デコレータとは何か、@記法の意味、そして自作デコレータの作り方までを、実行時間計測の実例を交えて解説します。
読み終える頃には、既存のコードにあるデコレータを読めるだけでなく、自分でも作れるようになります。
デコレータとは何か
関数を拡張する仕組み
デコレータとは、既存の関数を変更せずに、その前後に処理を追加できる仕組みです。
「ログを出力したい」「実行時間を計測したい」「権限チェックをしたい」といった、複数の関数に共通する処理を追加するときによく使われます。
デコレータを使わなくても同じことは実現できますが、デコレータを使うと、共通処理を1か所にまとめてすっきり書けます。
関数もオブジェクトである
デコレータを理解する前提として、Pythonでは関数もオブジェクトの一種であることを知っておく必要があります。
関数を変数に代入したり、関数の引数として渡したりできます。
def greet():
print("こんにちは")
say_hello = greet
say_hello()
実行結果は次の通りです。
こんにちは
greetという関数そのものをsay_helloに代入し、say_hello()として実行できています。
この「関数を値として扱える」性質が、デコレータの土台になっています。
@記法の意味
@記法は関数を渡すシンタックスシュガー
@デコレータ名という書き方は、実は次のコードのシンタックスシュガー(簡潔に書くための糖衣構文)です。
@my_decorator
def target_function():
pass
# 上記は以下と同じ意味
def target_function():
pass
target_function = my_decorator(target_function)
つまり@記法は、「対象の関数を、デコレータ関数に渡して、返ってきた関数で置き換える」という処理を、1行で表現したものです。
この仕組みが分かると、デコレータの中身を自分で書けるようになります。
デコレータの最小構成
デコレータは、「関数を受け取り、関数を返す関数」として定義します。
def my_decorator(func):
def wrapper():
print("--- 実行前 ---")
func()
print("--- 実行後 ---")
return wrapper
@my_decorator
def greet():
print("こんにちは")
greet()
実行結果は次の通りです。
--- 実行前 ---
こんにちは
--- 実行後 ---
my_decoratorはfunc(対象の関数)を受け取り、その前後に処理を追加したwrapper関数を返しています。
@my_decoratorをつけたgreetは、実質的にwrapperに置き換わっているイメージです。
自作デコレータの作り方
引数を持つ関数に対応させる
先ほどの例は、引数のない関数にしか使えません。
対象の関数が引数を受け取る場合は、*args(可変長の位置引数)と**kwargs(可変長のキーワード引数)を使って対応させます。
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で引数をそのままfuncに受け渡すことで、どんな関数にも使い回せる汎用的なデコレータになります。
実例:実行時間計測デコレータを作る
実務でよく使われるのが、関数の実行時間を計測するデコレータです。
標準ライブラリのtimeモジュールと組み合わせて作ってみます。
import time
def measure_time(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__}の実行時間: {end - start:.4f}秒")
return result
return wrapper
@measure_time
def heavy_task():
total = 0
for i in range(10_000_000):
total += i
return total
heavy_task()
実行結果は次のようになります(実行時間は環境によって変わります)。
heavy_taskの実行時間: 0.3521秒
このように@measure_timeを関数の上に1行つけるだけで、対象関数の処理内容を一切変更せずに、実行時間の計測を追加できます。
複数の関数に同じ計測処理を入れたい場合でも、@measure_timeをつけるだけで済むのがデコレータの強みです。
functools.wrapsで関数情報を保持する
デコレータを使うと、関数名や docstring(関数の説明文)といった情報が、wrapper関数のものに置き換わってしまう問題があります。
これを防ぐために、標準ライブラリのfunctools.wrapsを使うのが定石です。
import functools
def measure_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 実行時間計測などの処理
return func(*args, **kwargs)
return wrapper
@measure_time
def heavy_task():
"""重い処理を行う関数"""
pass
print(heavy_task.__name__)
print(heavy_task.__doc__)
実行結果は次の通りです。
heavy_task
重い処理を行う関数
@functools.wraps(func)をwrapperの直前につけるだけで、heavy_task本来の関数名やdocstringが保持されます。
自作デコレータを作る際は、忘れずにセットで使う習慣をつけましょう。
よくあるつまずきポイント・エラー対処
wrapperに引数を渡さずTypeErrorになる
筆者が初めてデコレータを自作したとき、引数を受け取らないwrapperを書いてしまい、エラーにハマった経験があります。
❌ Before(エラーになる書き方)
def my_decorator(func):
def wrapper(): # 引数を受け取れない
return func()
return wrapper
@my_decorator
def add(a, b):
return a + b
print(add(2, 3))
実行すると、次のエラーメッセージが表示されます。
TypeError: wrapper() takes 0 positional arguments but 2 were given
add(2, 3)を呼んだつもりが、実際には引数のないwrapper()が呼ばれる形になっており、渡した2と3の行き場がなくなっているのが原因です。
✅ After(正しい書き方)
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a, b):
return a + b
print(add(2, 3))
wrapper側にも*args・**kwargsを用意し、そのままfuncに渡すようにすれば解決します。
「対象の関数がどんな引数を取るか分からない」場合は、常に*args・**kwargsで受けておくのが安全です。
まとめ
この記事のポイント
- デコレータは既存の関数を変更せずに前後の処理を追加する仕組みである
@デコレータ名は「関数をデコレータに渡して置き換える」処理の糖衣構文である- 自作デコレータは「関数を受け取り、関数を返す関数」として定義する
- 引数を持つ関数に対応させるには
*args・**kwargsを使う functools.wrapsを使うと元の関数名やdocstringを保持できる
次に読むべき記事
- クラスとオブジェクト指向の基本
- プロパティ(@property)でgetter/setterを実装する
- ラムダ式とmap・filter・reduceの使い方
タグ: Python, 中級者向け, 基本文法