0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jupyter Notebookで解剖する、Pythonデコレーター入門

0
Last updated at Posted at 2026-07-05

本記事では、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()
'内側の関数です'

ここで大事なのは、outerinner() の実行結果を返しているのではなく、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)

つまり、引数を取るデコレーターでは、

  1. repeat(3) が実行される
  2. その結果として、本物のデコレーター decorator が返る
  3. decoratorsay_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では関数も変数に入れられる
  • 関数を別の関数に渡せる
  • 関数の中で関数を作れる
  • 関数を受け取って、別の関数を返せる
  • @decoratorfunc = 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導入支援・分析基盤の整備
  • 既存システムの運用改善・可視化支援

といった、「データを使える状態にする」ための活動です。

弊社の企業活動に興味がある方は、ぜひ公式サイトも覗いてみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?