Pythonのクロージャとスコープ:変数の生存期間

プログラミング

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 の順に名前(変数名など)を探していくことを意味します。

クロージャとは

クロージャとは、関数とその関数が定義された環境(エンクロージングスコープ)にある変数の束のことを指します。具体的には、以下の条件を満たす関数がクロージャとなります。

  1. ネストされた関数であること
  2. 内側の関数が、外側の関数で定義された変数を参照していること
  3. 外側の関数が、内側の関数を返していること

クロージャは、関数が実行された後も、その関数が参照していた外側のスコープの変数を保持し続けます。これにより、状態を維持しながら関数を実行することが可能になります。これが、クロージャにおける変数の生存期間の重要性です。

クロージャと変数の生存期間

通常のローカル変数は、関数が終了すると破棄されます。しかし、クロージャが参照するエンクロージングスコープの変数は、クロージャ自体が存在する限り、その生存期間が延長されます。たとえ外側の関数が既に実行を終えていたとしても、クロージャはこれらの変数を「記憶」しているのです。

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関数が実行されても、そのローカル変数nmultiplier_factoryの実行完了と共に破棄されるはずです。しかし、multiplier_factorymultiplier関数を返しており、このmultiplier関数はnを参照しています。返されたtimes_3times_5というクロージャは、それぞれnの値(3や5)を保持しています。これが、クロージャの強力な点であり、変数の生存期間が延長される例です。

times_3multiplier関数と、その環境(n=3)の束であり、times_5multiplier関数と、その環境(n=5)の束です。このように、クロージャは関数とその関連データをカプセル化するメカニズムを提供します。

クロージャの応用

クロージャは、様々な場面で活用されます。

  • 状態の保持: カウンターや、特定の状態を記憶しておきたい場合に役立ちます。
  • デコレータ: 関数の前後に処理を追加するデコレータは、クロージャの概念を多用しています。
  • コールバック関数: 非同期処理などで、後で実行される関数に状態を渡す際に便利です。
  • 関数ファクトリ: 特定のパラメータに基づいた関数を生成する際に利用できます。

まとめ

Pythonのスコープは、変数が参照可能な範囲を定義し、LEGBルールによって解決されます。クロージャは、内側の関数が外側のスコープの変数を参照し、かつその内側の関数が返されるときに形成されます。クロージャが生成されると、参照されている外側のスコープの変数は、クロージャ自体が存在する限り生存期間が延長されます。この変数の生存期間の延長は、クロージャが状態を保持したり、関数ファクトリのような強力なパターンを実現したりするための基盤となります。これらの概念を理解することは、Pythonでより洗練されたコードを書くために不可欠です。