Python の変数がどこで見つかるかは、コードの読みやすさやバグの発見に直接関係します。特に関数内で外側の変数を扱うとき、LEGB という検索順序とクロージャの仕組みを知っているかどうかで、プログラムの挙動が大きく変わります。
LEGB ルールとは何か
変数名がどこで定義されたかを探すとき、Python は必ず L → E → G → B の順で名前空間を検索します。
この順序が崩れることはありません。よって、同じ名前が複数のスコープで定義されている場合、最も内側に近いものが優先されます。
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 のコードをより安全で読みやすい形に導きます。