今回は、クロージャーとデコレーターについてと、その関係性について説明をしていこうと思います。
クロージャーとは
クロージャーとは、関数に機能を追加するための関数。
…これだとちょっと不親切ですね。
実際のコード例も交えて噛み砕いて説明をしていこうと思います。
def outer(msg):
def inner():
print(f"メッセージ: {msg}")
return inner
func = outer("こんにちは")
func()
# 結果
メッセージ: こんにちは
上記コードは何をしているのか一つずつ解説していきます
1. def outer(msg):
これは、関数outerを定義しているだけです。
引数msgを受け取り、その中で inner() を定義して return しています。
2. func = outer("こんにちは")
ここで実行されるのは outer("こんにちは")。
- msg = "こんにちは" として outer 関数が呼び出される
- その中で定義された inner 関数が return される
- でもこの inner 関数は、msg を覚えている(閉じ込めている)
なので、この時点で func には「msg = 'こんにちは' を記憶した inner 関数」が入ります。
3. func()
ここで inner() を実行していることになります。
- inner() の中では print(f"メッセージ: {msg}") が呼ばれる
- msg は outer() の引数だったけど、ちゃんと覚えてる!
ここまで見てくれたらわかると思うのですが、このクロージャーというのは、
外側の関数で定義された変数(スコープ内の値)を、内側の関数が覚えていて、それを後から使える機能のこと。
クロージャーを用いることで、次のようなメリットがあります。
メリット | 説明 |
---|---|
✅ データの隠蔽 | 変数を外部から直接いじれないので安全 |
✅ 独立性 | 複数の関数インスタンスがそれぞれ状態を持てる |
✅ 軽量な構造 | クラスを使わず状態を保持できる |
✅ 副作用が少ない | 他の処理と変数が干渉しにくい |
✅ テストしやすい | テスト時に状態がローカルに閉じていて予測可能 |
なるほど、機能はわかりました!…いやこれどこ使うの?
当然の疑問ですよね(´・ω・`)
このクロージャーが生きる具体例をお見せしましょう。
メモ化
「前に同じことをしたよね?だったら同じ処理なんかせず過去の結果を使い回せばよくね?」っていう考え。つまり、過去処理した結果がすでにあるのであれば、同じ処理はしないようにしましょうっていう考え方。
→ このメモ化を使用することで、処理の重複回避やパフォーマンス向上につながります。
では、実際のサンプルコードを見てみましょう。
import random
def memoized_square():
cache= {}
def square(n):
print(f'今回はこの数字を処理するよ! {n}')
if n in cache:
print(f'{n}は過去に計算済み!過去の計算結果を使いまわすよ!')
return cache[n]
else:
print(f'{n}はまだ計算したことがないみたい!新規で計算するね!')
result = n * n
cache[n] = result
return result
return square
square = memoized_square()
for _ in range(1,100):
random_num = random.randint(1,300)
print(square(random_num))
上記は1〜300までのランダムな数字の掛け算を100回繰り返すというものです。
上記プログラムの具体的なメリットを見ていきましょう。
- 「変数
cache
」が外から参照できないため、意図せずこの変数の中身が変わってしまい汚染されるリスクがなくなります。 - memoized_square() を複数回呼び出せば、それぞれ独自の cache を持つ別インスタンスが作れる
つまり、異なる条件下(200〜10000までのランダムな数字の掛け算を5000回繰り返す)を設定できる。
カスタムフィルターやソートキー : 条件に応じた動的な関数を作る
もう一つ、具体例を出したいと思います。
先のメモ化の例が「状態保持」にフォーカスしたのに対し、
今回の例は「設定パラメータ付きの関数を動的生成」という実用パターンです。
まずは、具体例を見てください。
import random
def make_min_filter(min_value):
def filter_func(x):
return x >= min_value
return filter_func
filter_10000 = make_min_filter(10000)
filter_141990 = make_min_filter(14990)
nums = []
for n in range(5000):
random_num = random.randint(1,15000)
nums.append(random_num)
result1 = list(filter(filter_10000, nums))
print(result1)
print(len(result1))
result2 = list(filter(filter_141990, nums))
print(result2)
print(len(result2))
上記関数は、「関数ごとに独自の min_value を内部に保持した状態のフィルタ関数が生成」をしています。
-
make_min_filter()
は、ある最小値の条件でフィルタ関数を作るファクトリ関数。 -
filter_func()
は、min_value
を覚えた関数として返される(←ここがクロージャー)
✅ クロージャーのポイント
-
make_min_filter(14490)
を実行した結果、min_value=10000
を覚えたfilter_func
が作られ、filter_10000
に代入されます。 -
同様に、
min_value=14990
を覚えた別の関数filter_141990
も作られます。
つまり、関数ごとに独自の min_value
を内部に保持した状態のフィルタ関数が生成されています。
このように、クロージャーで具体的な処理を書いておけば、後で個別で呼び出して使用することができます。
デコレーターとは
随分前置きが長くなってしまいましたが、デコレーターについての説明です。
デコレーターとは、関数に機能を追加するための関数
…具体例を出してもう少し噛み砕いて説明します。
def simple_decorator(func):
def wrapper():
print("前処理: 関数を呼ぶよ")
func()
print("後処理: 関数を呼び終わったよ")
return wrapper
@simple_decorator
def say_hello():
print("こんにちは!")
- 関数を引数として受け取る関数
デコレーターは、対象となる関数を引数として受け取る関数です。
def simple_decorator(func):
def wrapper():
print("前処理: 関数を呼ぶよ")
func()
print("後処理: 関数を呼び終わったよ")
return wrapper
ここで wrapper が元の関数です。
2. 関数をラップ(包み込む)
処理の前後や、条件に応じて処理を挟み込むことができる。
@simple_decorator
def say_hello():
print("こんにちは!")
これは、pythonの構文として、下記と同じ意味になります。
つまり say_hello 関数は、「前処理・後処理付きの関数(=wrapper)」に置き換わっているのです。
def say_hello():
print("こんにちは!")
say_hello = simple_decorator(say_hello)
上記を見てると、デコレーター名に@
をつけています。
これは、普通のpythonの構文にはありませんでしたよね?
では、なぜこのように書くのか説明をしていきたいと思います。
✅「@デコレーター名」は必須ではないが、省略すると不便です。
🔽 つまりこういうこと:
✔ デコレーターの使い方は2通り
① @記法(デコレーター構文)← よく使う・読みやすい
@simple_decorator
def say_hello():
print("こんにちは!")
② 関数を直接渡す ← 実はこっちでも動く
def say_hello():
print("こんにちは")
hello = simple_decorator(hello)
この2つは完全に同じことをしています。
📌 @を使うメリット
項目 | 内容 |
---|---|
✅ 可読性 | 「この関数にはデコレーターがついてる」と一目でわかる |
✅ 一貫性 | 多くのPythonコードで標準的に使われている |
✅ 手間削減 | 毎回 関数 = デコレーター(関数) と書かなくていい |
❗ 結論
- @デコレーター名 を書くのは便利な糖衣構文(シンタックスシュガー)。
- 実際には 関数 = デコレーター(関数) を簡単に書くためのもの。
- 必須ではないが、実務では @ を使うのが普通です。
引数を指定する場合
デコレーターは、さまざまな関数に適用される可能性があります。
そのため、引数の数や種類が異なる関数全てに対応できるようにする必要があります
その手段がその手段が *args(位置引数
)と **kwargs(キーワード引数)
です。
記法 | 役割 | 例 |
---|---|---|
*args |
任意の数の「位置引数」をタプルで受け取る |
func(1, 2, 3) → args = (1, 2, 3)
|
**kwargs |
任意の数の「キーワード引数」を辞書で受け取る |
func(x=10, y=20) → kwargs = {'x':10, 'y':20}
|
なぜ必要か(実例比較)
def wrapper(a, b): # 固定された2引数しか受け取れない
return func(a, b)
この場合、以下のような関数には適用できません:
@logger
def greet(name):
print(f"こんにちは{name}さん")
なぜなら greet は引数が1つなので、wrapper(a, b) がエラーになります。
でも、*args, **kwargs を使えば…
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
- 引数1つの関数 (greet(name))
- 引数なしの関数 (say_hello())
- 引数3つ以上の関数 (add(a, b, c))
- キーワード引数付き関数 (search(query="Python", limit=5))
すべてに対応できるのです。
デコレーターを書くときは、「相手がどんな関数か分からない」という前提で設計する。
だから *args, **kwargs が不可欠なんですね。
引数を指定した場合のコード例を下記に記載します。
このコードは「add関数が呼ばれたときに、その実行前後でログを出力する
」ためのものです。
そのために 「デコレーター(decorator)
」 というPythonの機能を使っています。
def logger(func):
def wrapper(*args, **kwargs):
print(f"{func.__name__} を呼び出します。引数: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} の戻り値: {result}")
return result
return wrapper
@logger
def add(a, b):
return a + b
add(3, 5)
じゃあ、クロージャーとデコレーターって何が違うの???
項目 | クロージャー | デコレーター |
---|---|---|
🔍 定義 | 関数内で定義された関数 + その外の変数を記憶している構造 | 関数に別の関数を渡して、機能を拡張する構文 |
🎯 主な目的 | 状態を保持する関数を作る | 既存の関数に機能を追加する |
🧱 構成 | 外側関数 + 内側関数(+外の変数) | 外側関数 + 内側関数(wrapper) + @記法
|
💬 例えるなら | 状態付き関数(電卓、カウンターなど) | 関数に装飾(ログ追加、認証処理など) |
共通点
- 両方とも「関数を返す関数」という高階関数
- 内部関数(wrapper, adderなど)を使う
- スコープと名前解決(LEGB ルール)を利用している
観点 | クロージャー | デコレーター |
---|---|---|
記憶するもの | 変数(状態) | 関数そのもの |
ユースケース | 状態を持つ関数、関数ファクトリ | ログ、認証、キャッシュ、計測などの処理追加 |
書き方 | 明示的に return して使う |
@デコレーター名 で簡潔に適用可能 |
関数の操作対象 | 外の変数を使う関数を返す | 他の関数そのものを操作する関数を返す |
まとめ:どう使い分けるか?
✅ 「状態付き関数を作りたい」→ クロージャー
- 例:カウンター、スコア管理、累積計算
✅ 「関数に機能を追加したい」→ デコレーター
- 例:ログ出力、実行時間測定、認可チェック、リトライ制御