Python の名前解決とスコープ: LEGB ルールとクロージャを正しく理解する

Python の変数がどこで見つかるかは、コードの読みやすさやバグの発見に直接関係します。特に関数内で外側の変数を扱うとき、LEGB という検索順序とクロージャの仕組みを知っているかどうかで、プログラムの挙動が大きく変わります。

LEGB ルールとは何か

変数名がどこで定義されたかを探すとき、Python は必ず L → E → G → B の順で名前空間を検索します。

Local(ローカルスコープ)
Enclosing(外側関数スコープ)
Global(モジュールスコープ)
Built-in(Python 組みこみスコープ)

この順序が崩れることはありません。よって、同じ名前が複数のスコープで定義されている場合、最も内側に近いものが優先されます。

Local(ローカル)の挙動

関数の内部で新しく名前を代入すると、その名前はローカル扱いになります。外側に同名の変数があっても、ローカルで再定義した瞬間に外側のものは参照されません。これは「代入を検知したらローカルとみなす」という Python の仕様によるものです。

x = 10

def f():
    x = 3
    print(x)

f()  # 3 と表示される

この例では、関数 f 内の x はローカル変数であり、外側の x とは別物です。

Enclosing(外側関数)とクロージャ

関数内に関数を定義すると、内側の関数は外側のローカル変数を参照できます。これが Enclosing スコープ です。

def outer():
    msg = 'hello'

    def inner():
        print(msg)  # outer の変数を参照

    return inner

f = outer()
f()  # hello

このように「関数が定義されたときの外側スコープの情報を保持したもの」が クロージャ です。inner が outer の実行終了後も msg を覚えているのは、クロージャによって変数環境が保存されているためです。

クロージャは状態を束ねて渡す手段として便利ですが、過剰な利用は可読性を下げるため注意が必要です。

グローバル変数と global 文

外側の変数を変更したいとき、単純に代入するとローカル扱いになるため、global を宣言しない限りグローバル変数を更新できません。

count = 0

def inc():
    global count
    count += 1

global を使うと、「代入対象をグローバルと明示する」という意図がはっきりします。一方、global の多用はバグにつながりやすく、設計としては避けられるべきです。

nonlocal は Enclosing の変数を書き換える

外側関数スコープの変数を変更したい場合は nonlocal を使います。global と似ていますが、対象が「モジュールスコープ」ではなく「外側関数のスコープ」である点が違います。

def counter():
    n = 0

    def add():
        nonlocal n
        n += 1
        return n

    return add

f = counter()
print(f())  # 1
print(f())  # 2

nonlocal により、n は外側スコープで一つだけ保持され続けます。ここでもクロージャの効果が働いています。

Python の名前解決で陥りやすいポイント

代入があるとローカル扱いになる

ローカルに代入がある場合、参照もすべてローカルとみなされます。このため、外側の変数を参照したいのに「代入があるせいでローカルになる」ケースが起こります。

ループ内クロージャの落とし穴

for ループ内で関数を作ると、ループ変数が「最後の値で束縛される」動作になります。これはクロージャが変数名を記憶するのであって、値をコピーしないために起こります。

LEGB とクロージャを整理すると見える設計指針

スコープの理解は文法知識に留まりません。関数の設計、変数の管理、テストの容易さなどに影響します。

ローカル中心関数の副作用が減り、テストが容易になる
グローバル依存を減らす意図しない書き換えを防ぐ
クロージャの利用必要な情報だけを閉じ込め、関数を小さく保つ

このように、LEGB に従った変数の整理は Python のコードをより安全で読みやすい形に導きます。