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 のコードをより安全で読みやすい形に導きます。