本記事では、Pythonプログラムで、 @ から始まる記述・デコレーターを、解説します。
@something
def my_func():
...
デコレーターの本質は、
関数を受け取って、少し加工した別の関数として返す仕組み
です。
この記事では、Jupyter Notebookを使って、セルをひとつずつ実行しながら、普通の関数からデコレーターが完成するまでの流れを解剖していきます。
対象読者
- Flask、FastAPI、pytestなどのPythonプログラムで、デコレーターを見たことがある人
-
@decoratorの意味を、コードを動かしながら正確に理解したい人
すぐに使えるチートシートはこちら Google Colab版
すぐに実行して、試せるコードレシピはこちら。
この記事でやること
最終的には、次のようなデコレーターを自作します。
@add_log
def greet(name):
return f"{name}さん、こんにちは"
実行すると、関数の前後にログが出るようにします。
greet("田中")
出力イメージは次のようになります。
greet を開始します
greet を終了しました
'田中さん、こんにちは'
いきなりこの形を覚えるのではなく、普通の関数から少しずつ作っていきます。
1. まずは普通の関数を書く
デコレーターの本質は、「関数」です。
そこでまずは、何の変哲もない関数を作ります。
def say_hello():
return "こんにちは"
実行してみます。
say_hello()
'こんにちは'
ここまでは普通です。
2. 関数も「値」として扱える
Pythonでは、関数も変数に入れることができます。
hello_func = say_hello
この時点では、関数を実行しているわけではありません。
say_hello() ではなく、say_hello と書いている点が重要です。
hello_func()
'こんにちは'
say_hello という関数を、hello_func という別名でも呼べるようになりました。
関数は、文字列や数値と同じように、変数に入れたり、別の関数に渡したりできます。
3. 関数を引数として受け取る
次に、関数を引数として受け取る関数を作ってみます。
def run_func(func):
result = func()
return result
この run_func は、引数として関数を受け取り、その関数を中で実行します。
run_func(say_hello)
'こんにちは'
ここでやっていることは、次のようなイメージです。
func = say_hello
result = func()
つまり、関数を別の関数に渡すことができています。
4. 関数の前後に処理を追加してみる
ここから少しデコレーターに近づきます。
関数を実行する前後に、ログを出してみます。
def run_with_log(func):
print("処理を開始します")
result = func()
print("処理を終了しました")
return result
実行します。
run_with_log(say_hello)
処理を開始します
処理を終了しました
'こんにちは'
これで、元の say_hello の前後に処理を追加できました。
ただし、この書き方だと毎回こう呼び出す必要があります。
run_with_log(say_hello)
本当は、いつも通りこう呼びたいです。
say_hello()
そこで、次のステップに進みます。
5. 関数の中で関数を作る
Pythonでは、関数の中に関数を書くことができます。
def outer():
def inner():
return "内側の関数です"
return inner
outer() を実行してみます。
result = outer()
この result には、inner 関数そのものが入っています。
result()
'内側の関数です'
ここで大事なのは、outer が inner() の実行結果を返しているのではなく、inner という関数そのものを返していることです。
return inner
もし次のように書くと、意味が変わります。
return inner()
これは、inner をその場で実行して、その結果を返す書き方です。
デコレーターでは、多くの場合、関数そのものを返したいので () をつけません。
6. 関数を受け取り、別の関数を返す
ここで、いよいよデコレーターの原型を作ります。
def add_log(func):
def wrapper():
print("処理を開始します")
result = func()
print("処理を終了しました")
return result
return wrapper
この add_log は、次のような関数です。
- 関数
funcを受け取る - 中で
wrapperという新しい関数を作る -
wrapperの中で、元の関数funcを実行する - 最後に
wrapper関数そのものを返す
まずは普通に使ってみます。
logged_say_hello = add_log(say_hello)
この時点で、logged_say_hello には wrapper 関数が入っています。
実行してみます。
logged_say_hello()
処理を開始します
処理を終了しました
'こんにちは'
これで、元の say_hello にログ機能を追加したような関数ができました。
7. 元の関数名に上書きする
次に、少し大胆なことをします。
say_hello = add_log(say_hello)
これは、次のような意味です。
say_helloを、ログ付きのsay_helloに置き換える
実行してみます。
say_hello()
処理を開始します
処理を終了しました
'こんにちは'
これで、普通に say_hello() と呼ぶだけで、前後にログが出るようになりました。
このように、Pythonでは関数の中に関数を書くことができます。
したがって、デコレーターの内部構造は、「関数のなかで、関数を入れ子構造にしたもの」といえます。
8. 結局、やりたいことはなんだったのか? コードの見方を変えてみる
細かい・ややこしい話が多くなりましたが、ここまでが、デコレーターの内部構造の話です。
ただ、プログラミングをする人の意図・思考に沿って言えば、普通、「関数の中に関数を入れ子にしよう」などと考えながら、コードを書く人はいないと思います。
むしろ、本当にやりたいことは、もっと素朴で、すでにある関数
def say_hello():
return "こんにちは"
に対して、その前後にログ出力を"追加"したい、というものです。
つまり、「入れ子」というよりは、「仕掛けの付け足し・補強」といった捉え方をするほうが、プログラムを読んだり・書いたりする人にとっては、より自然で、違和感も小さいと言えそうです。
この認識に忠実にコードを書くなら、本当は次のように書けると自然です。
@add_log
def say_hello():
return "こんにちは"
見た目とプログラマーの意図が一致し、読みやすく・書きやすいコードになりました。
一方で、内部的には次のような変換が行われています。
def say_hello():
return "こんにちは"
say_hello = add_log(say_hello)
このように、ある処理をより読みやすく、意図に近い形で書けるようにした構文を、シンタックスシュガー、日本語では糖衣構文と呼びます。
9. デコレーターとして完成させる
ここまでの内容をまとめると、ログを追加するデコレーターは次のように書けます。
def add_log(func):
def wrapper():
print("処理を開始します")
result = func()
print("処理を終了しました")
return result
return wrapper
使う側はこうです。
@add_log
def say_hello():
return "こんにちは"
実行します。
say_hello()
処理を開始します
処理を終了しました
'こんにちは'
これで、最初のデコレーターが完成しました。
10. 引数がある関数に対応する
ここまでの add_log には、まだ問題があります。
次のように、引数を取る関数に使ってみます。
@add_log
def greet(name):
return f"{name}さん、こんにちは"
実行すると、エラーになります。
greet("田中")
TypeError: wrapper() takes 0 positional arguments but 1 was given
理由は、wrapper が引数を受け取れない形になっているからです。
現在の wrapper はこうです。
def wrapper():
...
しかし、greet("田中") と呼ぶと、実際にはログ付きに置き換えられた wrapper("田中") が呼ばれます。
そのため、wrapper 側も引数を受け取れるようにする必要があります。
11. *args と **kwargs を使う
どんな引数にも対応できるように、*args と **kwargs を使います。
def add_log(func):
def wrapper(*args, **kwargs):
print("処理を開始します")
result = func(*args, **kwargs)
print("処理を終了しました")
return result
return wrapper
もう一度、引数ありの関数に使ってみます。
@add_log
def greet(name):
return f"{name}さん、こんにちは"
greet("田中")
処理を開始します
処理を終了しました
'田中さん、こんにちは'
これで、引数がある関数にも対応できるようになりました。
複数の引数でも動きます。
@add_log
def add(a, b):
return a + b
add(3, 5)
処理を開始します
処理を終了しました
8
キーワード引数でも動きます。
@add_log
def introduce(name, age):
return f"{name}さんは{age}歳です"
introduce(name="佐藤", age=30)
処理を開始します
処理を終了しました
'佐藤さんは30歳です'
12. 関数名をログに出す
せっかくなので、どの関数を実行しているのかも表示してみます。
関数名は func.__name__ で取得できます。
def add_log(func):
def wrapper(*args, **kwargs):
print(f"{func.__name__} を開始します")
result = func(*args, **kwargs)
print(f"{func.__name__} を終了しました")
return result
return wrapper
使ってみます。
@add_log
def multiply(a, b):
return a * b
multiply(4, 7)
multiply を開始します
multiply を終了しました
28
だいぶ実用的になってきました。
13. ただし、このままだと関数名が失われる
ここで、少し細かい問題を見てみます。
multiply.__name__
'wrapper'
本当は multiply という関数名を期待したいところですが、実際には wrapper になっています。
なぜなら、デコレーターを使ったあと、multiply は元の関数ではなく wrapper に置き換わっているからです。
これは小さな問題に見えますが、実務では意外と重要です。
たとえば、以下のような場面で困ることがあります。
- エラーログを見るとき
- テストツールを使うとき
- FlaskやFastAPIなどのフレームワークで関数情報を使うとき
- ドキュメント生成ツールを使うとき
そこで、functools.wraps を使います。
14. functools.wraps を使う
functools.wraps を使うと、元の関数名やドキュメント文字列などをなるべく引き継げます。
from functools import wraps
デコレーターを修正します。
from functools import wraps
def add_log(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"{func.__name__} を開始します")
result = func(*args, **kwargs)
print(f"{func.__name__} を終了しました")
return result
return wrapper
もう一度使ってみます。
@add_log
def multiply(a, b):
"""2つの数値を掛け算する関数"""
return a * b
multiply.__name__
'multiply'
multiply.__doc__
'2つの数値を掛け算する関数'
これで、元の関数情報を保ったまま、処理を追加できました。
実用上は、デコレーターを書くときには @wraps(func) をつける習慣にしておくとよいです。
15. 処理時間を測るデコレーターを作る
ログだけだと少し地味なので、処理時間を測るデコレーターも作ってみます。
from functools import wraps
from time import perf_counter, sleep
def measure_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = perf_counter()
result = func(*args, **kwargs)
end = perf_counter()
elapsed = end - start
print(f"{func.__name__} の処理時間: {elapsed:.3f}秒")
return result
return wrapper
試しに、1秒待つ関数を作ります。
@measure_time
def slow_task():
sleep(1)
return "完了"
slow_task()
slow_task の処理時間: 1.001秒
'完了'
このように、デコレーターを使うと、関数本体には手を入れずに共通処理を追加できます。
16. 複数の関数に同じ処理を追加する
デコレーターの便利なところは、同じ処理を複数の関数に簡単に適用できることです。
@measure_time
def task_a():
sleep(0.5)
return "A完了"
@measure_time
def task_b():
sleep(1.2)
return "B完了"
@measure_time
def task_c():
sleep(0.8)
return "C完了"
順番に実行します。
task_a()
task_a の処理時間: 0.501秒
'A完了'
task_b()
task_b の処理時間: 1.201秒
'B完了'
task_c()
task_c の処理時間: 0.801秒
'C完了'
各関数の中に、毎回 start = perf_counter() や end = perf_counter() を書かなくて済みます。
共通処理を関数の外側に切り出せるのが、デコレーターの強みです。
17. デコレーターを重ねる
デコレーターは複数重ねることもできます。
@add_log
@measure_time
def heavy_task():
sleep(1)
return "重い処理が終わりました"
実行します。
heavy_task()
heavy_task を開始します
heavy_task の処理時間: 1.001秒
heavy_task を終了しました
'重い処理が終わりました'
この場合、次のように変換されていると考えることができます。
heavy_task = add_log(measure_time(heavy_task))
下に近いデコレーターから先に関数へ適用され、その結果をさらに上のデコレーターが包みます。
18. よくある間違い1:return wrapper() と書いてしまう
デコレーターを書くときにやりがちな間違いがあります。
def bad_decorator(func):
def wrapper(*args, **kwargs):
print("開始")
result = func(*args, **kwargs)
print("終了")
return result
return wrapper()
最後が return wrapper() になっています。
これは、wrapper 関数そのものを返しているのではなく、その場で wrapper を実行してしまっています。
デコレーターでは、多くの場合、次のように書きます。
return wrapper
() をつけないのがポイントです。
19. よくある間違い2:元の関数の戻り値を返し忘れる
次のコードもよくあるミスです。
def bad_log(func):
def wrapper(*args, **kwargs):
print("開始")
func(*args, **kwargs)
print("終了")
return wrapper
このデコレーターは、元の関数を実行していますが、戻り値を返していません。
試してみます。
@bad_log
def add(a, b):
return a + b
result = add(3, 5)
print(result)
開始
終了
None
本当は 8 が返ってほしいのに、None になってしまいました。
正しくは、元の関数の実行結果を受け取って返します。
def good_log(func):
def wrapper(*args, **kwargs):
print("開始")
result = func(*args, **kwargs)
print("終了")
return result
return wrapper
20. よくある間違い3:引数に対応していない
これもよくあります。
def simple_log(func):
def wrapper():
print("開始")
result = func()
print("終了")
return result
return wrapper
引数なしの関数には使えます。
@simple_log
def hello():
return "hello"
hello()
開始
終了
'hello'
しかし、引数ありの関数には使えません。
@simple_log
def add(a, b):
return a + b
add(1, 2)
TypeError: wrapper() takes 0 positional arguments but 2 were given
汎用的なデコレーターにしたい場合は、基本的には次の形にします。
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
21. 引数を取るデコレーター
最後に、もう一段だけ進んで、引数を取るデコレーターも見てみます。
たとえば、関数を指定回数だけ実行するデコレーターを作ります。
使う側は、こう書けるようにしたいとします。
@repeat(3)
def say_hi():
print("Hi")
この場合、repeat(3) の時点で一度関数が呼ばれます。
そのため、構造が一段増えます。
from functools import wraps
def repeat(n):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
使ってみます。
@repeat(3)
def say_hi():
print("Hi")
say_hi()
Hi
Hi
Hi
少し複雑ですが、分解するとこうです。
@repeat(3)
def say_hi():
print("Hi")
これは、ざっくり次のような処理です。
decorator = repeat(3)
say_hi = decorator(say_hi)
つまり、引数を取るデコレーターでは、
-
repeat(3)が実行される - その結果として、本物のデコレーター
decoratorが返る -
decoratorがsay_hiを包む
という流れになります。
22. デコレーターの基本形
ここまでを踏まえると、基本的なデコレーターの形は次のようになります。
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 関数の前に追加したい処理
result = func(*args, **kwargs)
# 関数の後に追加したい処理
return result
return wrapper
まずはこの形を覚えておくと、多くのデコレーターを読めるようになります。
まとめ
デコレーターは、最初に見ると少し不思議な構文に見えます。
しかし、順番に分解すると、やっていることは次の通りです。
- Pythonでは関数も変数に入れられる
- 関数を別の関数に渡せる
- 関数の中で関数を作れる
- 関数を受け取って、別の関数を返せる
-
@decoratorはfunc = decorator(func)の省略形 - 実用上は
*args,**kwargs,functools.wrapsを使うことが多い
デコレーターの本質は、
関数の中身を書き換えずに、外側から共通処理を追加すること
です。
ログ出力、処理時間の計測、認証チェック、リトライ処理、キャッシュなど、いろいろな場面で使われます。
Jupyter Notebookでセルをひとつずつ実行していくと、普通の関数が少しずつデコレーターに近づいていく過程が見えます。
完成形だけを見ると難しく感じるものでも、途中の失敗や変形の過程を残しながら確認できるのは、Notebookで学ぶ大きなメリットだと思います。
余談1:AIプロダクト・Difyのコードに見つかる、山盛りのデコレーター
AIサービスのDifyは、オープンソース(OSS)であるため、ソースコードがすべてGitHubで開示されています。
実は、DifyのバックエンドはFlaskで作られており、大量のデコレーターが登場します。
セキュリティの安全を担保する、認証・認可の仕組みを、デコレーターが担っています。
余談2:なぜこの記事を書こうと思ったか
上記Difyのコードが好例ですが、デコレーターで実装されるコードは、ログイン判定・権限判定など、セキュリティに深くかかわるクリティカルな処理も多いため、ミスが許されない場面も少なくないと思います。
加えて、本記事でみてきたように、デコレーターは、内部構造が意外と複雑で、コードを読んでレビューしたり、不具合がでないように実装するのが、意外と大変になりがちです。
ミス・間違いが許されない実装ほど、"職人芸化"してしまうというのは、本来望ましくないことでもあると思います。
しかし現代では、Pythonで実装されるWebアプリケーションは多く、実務上、頻出であることも確かです。したがって、本記事で論じたようなデコレーターの内部構造を正確に把握し、正しく使いこなすことが重要なのだと思います。
Pythonのデコレーターの解説記事は、本記事以外にも、すでに多くあります。しかしあえて本記事を書きたいと思ったのは、デコレーターを正確に理解することの重要性・実装ミスによるトラブルの懸念に言及したいと考えたためです。
弊社について
本記事を書いている 合同会社インクルーシブソリューションズ は、データ基盤構築・分析基盤設計・システム改善支援を中心に活動している小規模IT法人です。
主な領域は、
- データマート設計・データパイプライン構築
- SQL / Python を用いたデータ処理設計
- BI導入支援・分析基盤の整備
- 既存システムの運用改善・可視化支援
といった、「データを使える状態にする」ための活動です。
弊社の企業活動に興味がある方は、ぜひ公式サイトも覗いてみてください。