【python】クロージャとは??
クロージャの概念
まず、クロージャをごく簡単に説明すると、、、
クロージャとは、簡単に言えば「記憶力のある関数」。普通の関数は、呼び出されるたびに一から始まるが、クロージャは前回の状態を覚えていられる関数。
クロージャの仕組み
- 関数の中に別の関数を作る(内部関数)。
- この内部関数は、外側の関数の変数を使うことができる。
- 外側の関数が終了した後も、内部関数は外側の関数の変数を覚えている。
- この「変数を覚えている内部関数」がクロージャ。
クロージャの利点とクロージャを使う理由
状態を保持できる: 数字を数え続けたり、設定を記憶したりできる。
カプセル化(データを隠せる): 外から直接変数を変更できないので安全。
柔軟な関数が作れる: 同じ外側の関数から、異なる設定の関数を作れる。
クロージャの概念についてのまとめ
クロージャは、関数の中に定義された関数。
しかし、単なる関数の中の関数というわけではなく、「外側の関数の変数を覚えている内側の関数」。つまり、ある関数の中で定義された内側の関数が、その外側の関数のローカル変数を使える、ということ。また、クロージャは、外側の関数が終了した後も外側の変数を覚えている。
「関数の中に定義された関数」と「クロージャ」の違い
クロージャも関数の中に定義された関数の一種ではあるが、そのうちのどのようなものをクロージャというのかについて、以下に示した。
関数の中の関数(内部関数):
- 単に関数内で別の関数を定義したもの。
- 内部関数が外部関数の変数を参照しない場合もある。(もちろん、参照することもある。内部関数は自身が定義されたスコープ(外部関数のスコープ)にアクセスすることができるので、常に外部関数の変数を参照することができる)
- 定義された関数の中でのみ使用される。
クロージャ:
- 関数内で別の関数を定義し、その内部関数を返すもの。
- 内部関数が外部関数のローカル変数を参照する。
- 外部関数のローカル変数を「覚えて」使用する。
- 外部関数の実行が終わった後も、その変数を保持し続ける。
- 外部関数から返され、外部で使用される。
特徴 | 関数の中の関数(内部関数) | クロージャ |
---|---|---|
定義方法 | 関数内で別の関数を定義するだけ | 関数内で別の関数を定義し、その関数を返す |
外部変数の扱い | 外部変数を使用しても、しなくてもよい | 外部関数の変数を必ず使用する |
変数の記憶 | 外部関数の実行中だけ変数にアクセス可能 | 外部関数の実行後も変数を記憶し続ける |
使用場所 | 定義された関数の中だけで使用 | 定義された関数の外でも使用可能 |
状態の保持 | 通常、状態を保持しない | 外部関数の変数を使って状態を保持する |
返値としての利用 | 通常、返値として使われない | 外部関数の返値として使われる |
存在期間 | 外部関数の実行中だけ存在する | 外部関数の実行が終わった後も存在し続ける |
以下に、関数の中の関数(内部関数)とクロージャのコード例を示す。
def outer_function(x):
def inner_function(y):
return y * 2
return inner_function(x) + 5
print(outer_function(3)) # 出力: 11
def create_adder(x):
def adder(y):
return x + y # 外部のxを参照
return adder
add_5 = create_adder(5)
print(add_5(3)) # 出力: 8
print(add_5(7)) # 出力: 12
上記の例において、「関数の中に定義された関数」と「クロージャ」は、以下のように異なる。
関数の中の関数(outer_function):
-
inner_function
はouter_function
の中で定義され、使用される。 -
inner_function
はouter_function
の変数xを直接使用せず、xを引数として渡される。 -
outer_function
の実行が終われば、inner_function
はアクセスできなくなる。
クロージャ(create_adder):
-
adder
関数はcreate_adder
の中で定義されるが、adder
関数オブジェクトが戻り値として返される。 -
adder
関数はcreate_adder
のパラメータxを直接参照する。 -
create_adder(5)
の実行が終わった後も、返されたadder関数
(add_5)はxの値(5)を覚えている。
もう一つ、クロージャの例を示す。
def create_discount_calculator(discount_rate):
def calculate_discounted_price(original_price):
return original_price * (1 - discount_rate)
return calculate_discounted_price
# 10%割引の計算機を作成
ten_percent_off = create_discount_calculator(0.1)
# 20%割引の計算機を作成
twenty_percent_off = create_discount_calculator(0.2)
# 割引計算機を使用
print(ten_percent_off(1000)) # 出力: 900.0
print(twenty_percent_off(1000)) # 出力: 800.0
# 別の価格で試してみる
print(ten_percent_off(5000)) # 出力: 4500.0
print(twenty_percent_off(5000)) # 出力: 4000.0
上の例では、
-
create_discount_calculator
関数は、割引率を引数に取る。 - 内部で
calculate_discounted_price
関数を定義している。これがクロージャになる。 -
calculate_discounted_price
は外側の関数のdiscount_rate
を使用している。 -
create_discount_calculator
はcalculate_discounted_price
関数を返す。 -
ten_percent_off = create_discount_calculator(0.1)
を実行すると、10%割引を計算する関数が作られる。同様に、twenty_percent_off
は20%割引を計算する。 - つまり、これらのクロージャを使うと、それぞれが覚えている割引率を使って、与えられた価格から割引後の価格を計算することができる。
クロージャを理解するためのポイント
- 外側の関数が終了しても(上の例では
create_discount_calculator
関数)、内部関数(上の例ではten_percent_off = create_discount_calculator(0.1)
)は外側の関数の変数を覚えている。つまり、内部関数create_discount_calculator()
と、内部関数が使用するデータ(外側の関数の変数 (上の例では0.1や0.2))がセットになっている。 - 外部からデータを直接変更できないので、データの安全性が高まる。(ここでの「データの安全性」とは、外部から直接変数を変更できないようにすることを言う。これにより、意図しない変更からデータを守ることができる。)上の例では、
ten_percent_off
(10%割引の計算機)の割引率10%は、直接変更できないようになっている。 - 同じ外部関数から作られた関数でも、別々の状態を持つことができる(たとえば、上記の例の
ten_percent_off
とtwenty_percent_off
)。
クロージャの利点を、上記のコード例に当てはめると、、、
状態を保持できる: 上記の例の割引率のような情報を関数内部に保持できる。
カプセル化(データを隠せる): 上記の例の割引率は外部から直接アクセスできない。これにより、データの整合性が保たれる。
柔軟な関数が作れる: 上記の例では、異なる割引率で複数の税率計算関数を簡単に作成できる。
クロージャの作り方
クロージャを作る基本的な手順は以下の通り。
- 外側の関数を定義する
- その中で内側の関数を定義する
- 内側の関数で外側の関数の変数を使う
- 外側の関数から内側の関数を返す
def outer_function(x):
# 外側の関数のローカル変数
y = 10
# 内側の関数(これがクロージャになる)
def inner_function(z):
# 外側の関数の変数(x, y)を使用
return x + y + z
# 内側の関数を返す
return inner_function
# クロージャを作成
closure = outer_function(5)
# クロージャを使用
result = closure(3)
print(result) # 出力: 18 (5 + 10 + 3)
この例では、outer_function
がinner_function
を返している。inner_function
は、外部関数に変数として渡されたxの値と、外部関数のローカル変数yの値を覚えている。このinner_function
がクロージャとなる。
クロージャを使うべき場面
クロージャは以下のような状況で特に有用。
(1) 状態を保持する必要がある場合
(2) カスタマイズ可能な関数を作成したい場合
(3) コールバック関数を使用する場合
(4) プライベートな変数やメソッドが必要な場合
それぞれの場合について、コードの例を以下に示す。
(1) 状態を保持する場合
def create_score_counter():
score = 0
def update_score(points):
nonlocal score
score += points
return f"現在のスコア: {score}"
return update_score
# スコアカウンターを作成
player_score = create_score_counter()
# スコアを更新
print(player_score(10)) # 出力: 現在のスコア: 10
print(player_score(5)) # 出力: 現在のスコア: 15
print(player_score(-3)) # 出力: 現在のスコア: 12
この例では、クロージャを使ってスコアの状態を保持している。player_score
関数は呼び出されるたびに前回のスコアを覚えている。
(2) カスタマイズ可能な関数を作成する場合
def create_tax_calculator(tax_rate):
def calculate_tax(price):
return price * tax_rate
return calculate_tax
# 異なる税率の計算機を作成
japan_tax = create_tax_calculator(0.10) # 10%の消費税
us_tax = create_tax_calculator(0.08) # 8%の売上税(例)
# 税金を計算
print(japan_tax(1000)) # 出力: 100.0
print(us_tax(1000)) # 出力: 80.0
この例では、クロージャを使って異なる税率の計算機を簡単に作成できる。各計算機は自身の税率を「記憶」している。
(3) コールバック関数を使用する場合
def create_button(label):
click_count = 0
def on_click():
nonlocal click_count
click_count += 1
return f"{label}ボタンが{click_count}回クリックされました。"
return on_click
# ボタンを作成
submit_button = create_button("送信")
cancel_button = create_button("キャンセル")
# ボタンクリックをシミュレート
print(submit_button()) # 出力: 送信ボタンが1回クリックされました。
print(submit_button()) # 出力: 送信ボタンが2回クリックされました。
print(cancel_button()) # 出力: キャンセルボタンが1回クリックされました。
この例では、クロージャをコールバック関数として使用している。各ボタンは自身のラベルとクリック回数を「記憶」している。
(4) プライベートな変数やメソッドが必要な場合
def create_bank_account(initial_balance):
balance = initial_balance
def deposit(amount):
nonlocal balance
balance += amount
return f"入金完了。残高: {balance}円"
def withdraw(amount):
nonlocal balance
if amount <= balance:
balance -= amount
return f"引き出し完了。残高: {balance}円"
else:
return "残高不足です。"
def check_balance():
return f"現在の残高: {balance}円"
return {
"deposit": deposit,
"withdraw": withdraw,
"check_balance": check_balance
}
# アカウントを作成
my_account = create_bank_account(1000)
# アカウントを操作
print(my_account["deposit"](500)) # 出力: 入金完了。残高: 1500円
print(my_account["withdraw"](200)) # 出力: 引き出し完了。残高: 1300円
print(my_account["check_balance"]()) # 出力: 現在の残高: 1300円
この例では、クロージャを使ってbalance
変数をプライベートにし、直接アクセスできないようにしている。操作は提供されたメソッドを通じてのみ可能。
クロージャを用いるべきかどうかの判断基準
クロージャを使用するかどうかを判断する際は、以下の点を考慮するとよい。
1. 状態の保持: 関数呼び出し間で状態を保持する必要があるか?
2. カプセル化: データや機能を隠蔽する必要があるか?
3. 動的関数生成: 実行時に関数をカスタマイズする必要があるか?
4. コードの簡潔さ: クロージャを使用することでコードがより簡潔になるか?
5. パフォーマンス: 大量のインスタンスを作成する必要があるか?(クロージャはクラスより軽量)
1. 状態の保持: 関数呼び出し間で状態を保持する必要があるか?
例えば、以下のような場合は、状態保持するとよい。
-
カウンター機能:
特定の操作の回数を追跡する場合に有用。例えば、ウェブサイトの訪問回数やボタンクリック回数の追跡に使用できる。
具体的なコード例
def create_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
page_views = create_counter()
print(page_views()) # 出力: 1
print(page_views()) # 出力: 2
-
設定の記憶:
一度設定した値を後続の操作で継続して使用する場合に適している。例えば、ユーザー設定やアプリケーションの設定を保持するのに役立つ。
具体的なコード例
def create_configurator(initial_config):
config = initial_config
def update_config(key, value):
nonlocal config
config[key] = value
return config
return update_config
app_config = create_configurator({"theme": "light", "font_size": 12})
print(app_config("theme", "dark")) # 出力: {'theme': 'dark', 'font_size': 12}
-
履歴の追跡:
過去の操作や結果を記録し、新しい操作に反映させる場合に使用する。例えば、計算履歴の保持や最近の操作のログ維持に役立つ。
具体的なコード例
def create_calculator():
history = []
def calculate(operation, x, y):
result = operation(x, y)
history.append((x, y, result))
return result, history
return calculate
calc = create_calculator()
add = lambda x, y: x + y
print(calc(add, 5, 3)) # 出力: (8, [(5, 3, 8)])
print(calc(add, 2, 4)) # 出力: (6, [(5, 3, 8), (2, 4, 6)])
2. カプセル化: データや機能を隠蔽する必要があるか?
例えば、以下のような場合は、カプセル化するとよい。
-
プライベート変数:
外部から直接アクセスや変更を防ぎたいデータを管理する。例えば、パスワードやセンシティブな情報の保護に使用できる。
具体的なコード例
def create_password_manager(initial_password):
password = initial_password
def change_password(old_password, new_password):
nonlocal password
if old_password == password:
password = new_password
return True
return False
return change_password
password_changer = create_password_manager("secret123")
print(password_changer("secret123", "newpass456")) # 出力: True
print(password_changer("wrongpass", "hackpass")) # 出力: False
-
内部ロジックの隠蔽:
複雑な計算過程や内部状態を隠し、シンプルなインターフェースを提供する。例えば、複雑なアルゴリズムの実装を隠蔽できる。
具体的なコード例
def create_prime_checker():
cache = {}
def is_prime(n):
if n in cache:
return cache[n]
if n < 2:
cache[n] = False
elif n == 2:
cache[n] = True
else:
cache[n] = all(n % i != 0 for i in range(2, int(n**0.5) + 1))
return cache[n]
return is_prime
prime_checker = create_prime_checker()
print(prime_checker(17)) # 出力: True
print(prime_checker(24)) # 出力: False
-
データの整合性保護:
データの変更を制御し、不正な操作を防ぐ。例えば、オブジェクトの状態を一貫して保つために使用できる。
具体的なコード例
def create_temperature_monitor(initial_temp):
temperature = initial_temp
def set_temperature(new_temp):
nonlocal temperature
if -273.15 <= new_temp <= 100:
temperature = new_temp
return True
return False
def get_temperature():
return temperature
return set_temperature, get_temperature
set_temp, get_temp = create_temperature_monitor(25)
print(set_temp(30)) # 出力: True
print(get_temp()) # 出力: 30
print(set_temp(1000)) # 出力: False
print(get_temp()) # 出力: 30
3. 動的関数生成: 実行時に関数をカスタマイズする必要があるか?
例えば、以下のような場合は、動的関数を生成するとよい。
-
パラメータ化された関数:
初期設定に基づいて動作が変わる関数を生成する。例えば、異なる割引率を適用する関数を動的に作成できる。
具体的なコード例
def create_discount_calculator(discount_rate):
def apply_discount(price):
return price * (1 - discount_rate)
return apply_discount
ten_percent_off = create_discount_calculator(0.1)
twenty_percent_off = create_discount_calculator(0.2)
print(ten_percent_off(100)) # 出力: 90.0
print(twenty_percent_off(100)) # 出力: 80.0
-
環境に応じた関数:
実行環境やユーザー設定に基づいて挙動が変わる関数を作成する。例えば、デバッグモードに応じてログ出力を変える関数を生成できる。
具体的なコード例
def create_logger(debug_mode):
def log(message):
if debug_mode:
print(f"DEBUG: {message}")
else:
print(f"INFO: {message}")
return log
debug_logger = create_logger(True)
info_logger = create_logger(False)
debug_logger("Test message") # 出力: DEBUG: Test message
info_logger("Test message") # 出力: INFO: Test message
-
カリー化(部分適用):
複数の引数を取る関数を、引数を段階的に受け取る一連の関数に変換する技術。これにより、関数の一部の引数を先に適用し、残りの引数を後で適用することができる。
具体的なコード例
def create_greeting(greeting):
def greet(name):
return f"{greeting}, {name}!"
return greet
say_hello = create_greeting("Hello")
say_hi = create_greeting("Hi")
print(say_hello("Alice")) # 出力: Hello, Alice!
print(say_hi("Bob")) # 出力: Hi, Bob!
4. コードの簡潔さ: クロージャを使用することでコードがより簡潔になるか?
例えば、以下のような場合は、クロージャを用いてコードを簡潔化するとよい。
-
関連する機能のグループ化:
関連する関数やデータを一つのユニットにまとめます。これにより、コードの構造が整理され、保守性が向上します。
具体的なコード例
def create_math_operations():
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
return {"add": add, "subtract": subtract, "multiply": multiply}
math_ops = create_math_operations()
print(math_ops["add"](5, 3)) # 出力: 8
print(math_ops["subtract"](10, 4)) # 出力: 6
print(math_ops["multiply"](2, 6)) # 出力: 12
-
コールバック関数の簡略化:
イベントハンドラやコールバックを簡潔に記述する。これは特にGUIプログラミングやイベント駆動プログラミングで有用。
具体的なコード例
def create_button(label, on_click):
def click_handler():
print(f"Button '{label}' clicked!")
on_click()
return click_handler
def save_action():
print("Saving data...")
save_button = create_button("Save", save_action)
save_button() # 出力: Button 'Save' clicked! \n Saving data...
-
繰り返しコードの削減:
類似した関数を動的に生成し、コードの重複を避ける。これにより、DRY(Don't Repeat Yourself)原則を守りやすくなる。
具体的なコード例
def create_power_function(exponent):
def power(base):
return base ** exponent
return power
square = create_power_function(2)
cube = create_power_function(3)
print(square(4)) # 出力: 16
print(cube(3)) # 出力: 27
5. パフォーマンス: 大量のインスタンスを作成する必要があるか?(クロージャはクラスより軽量)
例えば、以下のような場合は、クロージャを用いることで、効率化される。
-
メモリ効率:
多数の小さな機能単位が必要な場合、クロージャはメモリ使用量を抑えられる。例えば、大量のシンプルなオブジェクトが必要な場合に有効。
具体的なコード例
def create_counter_factory():
counters = {}
def get_counter(name):
if name not in counters:
counters[name] = 0
def counter():
counters[name] += 1
return counters[name]
return counter
return get_counter
counter_factory = create_counter_factory()
counter_a = counter_factory("A")
counter_b = counter_factory("B")
print(counter_a()) # 出力: 1
print(counter_a()) # 出力: 2
print(counter_b()) # 出力: 1
-
初期化の速度:
クラスのインスタンス化よりも、クロージャの生成のほうが一般的に高速。大量のオブジェクトを作成する必要がある場合に有効。
具体的なコード例
import time
def create_timer():
start_time = time.time()
def get_elapsed():
return time.time() - start_time
return get_elapsed
timers = [create_timer() for _ in range(1000000)]
print(timers[0]()) # 経過時間を出力
-
動的な関数生成:
実行時に多数の類似関数を効率的に生成する場合に使用する。例えば、異なるパラメータを持つ多数のフィルター関数を生成する場合などに有効。
具体的なコード例
def create_threshold_filter(threshold):
def filter_func(value):
return value > threshold
return filter_func
thresholds = [10, 20, 30, 40, 50]
filters = [create_threshold_filter(t) for t in thresholds]
data = [15, 25, 35, 45, 55]
for i, f in enumerate(filters):
print(f"Values above {thresholds[i]}: {list(filter(f, data))}")
クロージャの実践的な使用例
クロージャの効果的な使用例として、より実践的な例を以下に挙げる。
1. カリー化(部分適用)
カリー化は、複数の引数を取る関数を、より少ない引数を取る関数のチェーンに変換するテクニック。
def multiply(x):
def multiply_by(y):
return x * y
return multiply_by
# カリー化された関数を使用
double = multiply(2)
triple = multiply(3)
print(double(5)) # 出力: 10
print(triple(5)) # 出力: 15
# 部分適用の例
numbers = [1, 2, 3, 4, 5]
doubled_numbers = list(map(double, numbers))
tripled_numbers = list(map(triple, numbers))
print(doubled_numbers) # 出力: [2, 4, 6, 8, 10]
print(tripled_numbers) # 出力: [3, 6, 9, 12, 15]
この例では、クロージャを使って乗算関数をカリー化している。これにより、特定の倍数で数値を掛ける関数を簡単に作成でき、mapなどの高階関数と組み合わせて使用できる。
2. 状態を持つジェネレータ
クロージャを使用して、状態を持つカスタムジェネレータを作成できる。
def create_fibonacci_generator():
a, b = 0, 1
def fibonacci():
nonlocal a, b
a, b = b, a + b
return a
return fibonacci
# フィボナッチ数列ジェネレータを作成
fib = create_fibonacci_generator()
# 最初の10個のフィボナッチ数を生成
for _ in range(10):
print(fib(), end=" ")
# 出力: 1 1 2 3 5 8 13 21 34 55
print("\n")
# 新しいジェネレータを作成(状態はリセットされる)
fib2 = create_fibonacci_generator()
for _ in range(5):
print(fib2(), end=" ")
# 出力: 1 1 2 3 5
この例では、クロージャを使ってフィボナッチ数列のジェネレータを作成している。各ジェネレータは自身の状態(aとb)を保持し、呼び出されるたびに次のフィボナッチ数を生成する。
3. 再利用可能で設定可能なバリデータ関数
クロージャを使用して、再利用可能で設定可能なバリデータ関数を作成できる。
def create_range_validator(min_val, max_val):
def validate(value):
return min_val <= value <= max_val
return validate
def create_length_validator(min_len, max_len):
def validate(value):
return min_len <= len(value) <= max_len
return validate
# バリデータを作成
age_validator = create_range_validator(0, 120)
password_validator = create_length_validator(8, 20)
# バリデータを使用
print(age_validator(25)) # 出力: True
print(age_validator(150)) # 出力: False
print(password_validator("password123")) # 出力: True
print(password_validator("pass")) # 出力: False
# 複数のバリデータを組み合わせる
def validate_user_input(age, password):
return age_validator(age) and password_validator(password)
print(validate_user_input(30, "secure_password")) # 出力: True
print(validate_user_input(150, "weak")) # 出力: False
この例では、クロージャを使って再利用可能で設定可能なバリデータ関数を作成している。これにより、異なる条件のバリデータを簡単に作成し、組み合わせることができる。
4. メモ化(キャッシュ)を用いた関数の最適化
フィボナッチ数列の計算など、再帰的な関数呼び出しが多い場合、クロージャを使ったメモ化により大幅にパフォーマンスを向上させることができる。
def memoize_fibonacci():
cache = {}
def fibonacci(n):
if n in cache:
return cache[n]
if n <= 1:
result = n
else:
result = fibonacci(n-1) + fibonacci(n-2)
cache[n] = result
return result
return fibonacci
fib = memoize_fibonacci()
# 使用例
print(fib(10)) # 出力: 55
print(fib(100)) # 出力: 354224848179261915075 (通常の再帰だと非常に時間がかかる)
この例では、クロージャを使用することで以下の利点がある。
-
cache
ディクショナリが関数の外部から隠蔽され、安全に管理される。 - 一度計算した結果が保存されるため、再計算を避けられ、大幅な性能向上が見込める。
- 通常の再帰方法と比べて、はるかに大きな数のフィボナッチ数を高速に計算できる。
5. イベント駆動型の簡易タスクスケジューラ
特定の条件が満たされた時にタスクを実行するシンプルなスケジューラを実装する例。
import time
def create_scheduler():
tasks = []
def add_task(condition, action):
tasks.append((condition, action))
def run():
while tasks:
for condition, action in tasks[:]:
if condition():
action()
tasks.remove((condition, action))
time.sleep(1) # 1秒ごとにチェック
return add_task, run
# 使用例
add_task, run_scheduler = create_scheduler()
# 5秒後に実行されるタスク
add_task(
condition=lambda: time.time() % 5 == 0,
action=lambda: print("5秒ごとのタスク実行")
)
# 10秒後に実行されるタスク
add_task(
condition=lambda: time.time() % 10 == 0,
action=lambda: print("10秒ごとのタスク実行")
)
# スケジューラの実行
run_scheduler()
この例では、クロージャを使用することで以下の利点がある。
- タスクリスト(
tasks
)が外部から直接アクセスできないため、安全に管理される。 - 新しいタスクの追加が簡単で、各タスクが独立して条件とアクションを持てる。
- 条件チェックとタスク実行のロジックが一箇所にまとまり、管理が容易。
まとめ
- クロージャは、関数内で定義され、外部関数の変数を記憶する特殊な関数。これにより、状態の保持、データのカプセル化、動的な関数生成が可能となる。
- クロージャの主な特徴は、外部関数の実行後もその変数を保持し続け、外部から直接アクセスできない点。
- 使用例として、カウンター機能、カスタマイズ可能な計算機、コールバック関数などがある。クロージャは、状態保持が必要な場合、データの隠蔽が必要な場合、実行時に関数をカスタマイズする必要がある場合に特に有用。
Discussion