Pythonのクロージャとスコープ:変数の生存期間
Pythonにおけるクロージャとスコープは、プログラムの挙動を理解する上で非常に重要な概念です。特に、変数の生存期間(ライフタイム)と密接に関連しており、これらを正しく把握することで、より堅牢で効率的なコードを書くことが可能になります。
スコープとは
スコープとは、変数や関数などの識別子が参照可能な範囲のことです。Pythonでは、主に以下の4つのスコープが定義されています。
ローカルスコープ (Local Scope)
関数内で定義された変数や関数は、その関数内でのみ有効です。関数が終了すると、ローカルスコープで定義された変数は破棄されます。
def my_function():
x = 10 # xはローカル変数
print(x)
my_function() # 10
# print(x) # NameError: name 'x' is not defined
エンクロージングスコープ (Enclosing Scope) / ノンローカルスコープ (Nonlocal Scope)
ネストされた関数において、外側の関数のスコープを指します。内側の関数から外側の関数の変数を参照できますが、直接代入しようとすると新しいローカル変数として扱われます。ノンローカル変数として参照・代入するにはnonlocalキーワードを使用します。
def outer_function():
y = 20 # yはエンクロージングスコープの変数
def inner_function():
print(y) # yを参照
# nonlocal y
# y = 30 # nonlocalを使用しない場合、inner_function内のyは新しいローカル変数になる
inner_function()
print(y) # 外側のyは変更されない (nonlocalを使用しない場合)
outer_function() # 20, 20
def outer_function_nonlocal():
y = 20
def inner_function():
nonlocal y
y = 30 # nonlocal yにより、outer_function_nonlocalのyが変更される
print(y)
inner_function()
print(y) # 外側のyが変更されている
outer_function_nonlocal() # 30, 30
グローバルスコープ (Global Scope)
モジュールレベルで定義された変数や関数は、そのモジュール全体で有効です。グローバルスコープの変数を関数内で変更するにはglobalキーワードを使用します。
z = 40 # zはグローバル変数
def another_function():
print(z) # zを参照
# global z
# z = 50 # globalを使用しない場合、another_function()内のzは新しいローカル変数になる
another_function() # 40
# print(z) # 40 (変更されていない)
z_global = 40
def change_global_z():
global z_global
z_global = 50 # global z_globalにより、グローバルスコープのz_globalが変更される
print(f"Inside function: {z_global}")
change_global_z() # Inside function: 50
print(f"Outside function: {z_global}") # Outside function: 50
ビルトインスコープ (Built-in Scope)
print(), len(), list()などのPythonの組み込み関数や定数は、どのスコープからでも参照可能です。
Pythonのスコープ解決順序は、LEGBルールとして知られています。これは、Local -> Enclosing -> Global -> Built-in の順に名前(変数名など)を探していくことを意味します。
クロージャとは
クロージャとは、関数とその関数が定義された環境(エンクロージングスコープ)にある変数の束のことを指します。具体的には、以下の条件を満たす関数がクロージャとなります。
- ネストされた関数であること
- 内側の関数が、外側の関数で定義された変数を参照していること
- 外側の関数が、内側の関数を返していること
クロージャは、関数が実行された後も、その関数が参照していた外側のスコープの変数を保持し続けます。これにより、状態を維持しながら関数を実行することが可能になります。これが、クロージャにおける変数の生存期間の重要性です。
クロージャと変数の生存期間
通常のローカル変数は、関数が終了すると破棄されます。しかし、クロージャが参照するエンクロージングスコープの変数は、クロージャ自体が存在する限り、その生存期間が延長されます。たとえ外側の関数が既に実行を終えていたとしても、クロージャはこれらの変数を「記憶」しているのです。
def multiplier_factory(n):
def multiplier(x):
return x * n # nはエンクロージングスコープの変数
return multiplier # multiplier関数を返す
times_3 = multiplier_factory(3)
times_5 = multiplier_factory(5)
print(times_3(2)) # 6
print(times_5(4)) # 20
上記の例では、multiplier_factory関数が実行されても、そのローカル変数nはmultiplier_factoryの実行完了と共に破棄されるはずです。しかし、multiplier_factoryはmultiplier関数を返しており、このmultiplier関数はnを参照しています。返されたtimes_3やtimes_5というクロージャは、それぞれnの値(3や5)を保持しています。これが、クロージャの強力な点であり、変数の生存期間が延長される例です。
times_3はmultiplier関数と、その環境(n=3)の束であり、times_5はmultiplier関数と、その環境(n=5)の束です。このように、クロージャは関数とその関連データをカプセル化するメカニズムを提供します。
クロージャの応用
クロージャは、様々な場面で活用されます。
- 状態の保持: カウンターや、特定の状態を記憶しておきたい場合に役立ちます。
- デコレータ: 関数の前後に処理を追加するデコレータは、クロージャの概念を多用しています。
- コールバック関数: 非同期処理などで、後で実行される関数に状態を渡す際に便利です。
- 関数ファクトリ: 特定のパラメータに基づいた関数を生成する際に利用できます。
まとめ
Pythonのスコープは、変数が参照可能な範囲を定義し、LEGBルールによって解決されます。クロージャは、内側の関数が外側のスコープの変数を参照し、かつその内側の関数が返されるときに形成されます。クロージャが生成されると、参照されている外側のスコープの変数は、クロージャ自体が存在する限り生存期間が延長されます。この変数の生存期間の延長は、クロージャが状態を保持したり、関数ファクトリのような強力なパターンを実現したりするための基盤となります。これらの概念を理解することは、Pythonでより洗練されたコードを書くために不可欠です。
