はじめに
pythonコードを書いている時、こんな状態に遭遇した。
パターン1
def aaa(): print(a) a = 1 aaa() #実行でき、1が標準出力される。
パターン2
def aaa(): a = a + 1 a = 1 aaa() #UnboundLocalError: local variable 'a' referenced before assignment
関数の中で外部の変数を参照することは出来るのに、代入することは出来ないのである。
だが、変数の型をリストにすると、
パターン3
def bbb(): print(b) b = [1,2,3] bbb() #実行でき、[1,2,3]が標準出力される。
パターン4
def bbb(): b[0] = 10 print(b) b = [1,2,3] bbb() #実行でき、[10,2,3]が標準出力される。
外部の変数を参照することも代入することもできる。
Pythonでは関数内からグローバル変数を扱うときはglobal宣言、関数内関数から親関数の変数を扱うときはnonlocal宣言をする必要があり、これらの宣言をすればパターン2のような局面でもエラー無く実行できる。
だが、パターン1~4の結果を見比べて、以下のような疑問が生まれた。
- パターン1において、global宣言をしていないのになぜprintは出来たのか?
- パターン3,4において、global宣言をしていないのになぜprintも代入も出来たのか?
本記事ではこれらの疑問に対する答えを調査し、書いていこうと思う。
1.パターン1において、global宣言をしていないのになぜprintは出来たのか?
Pythonドキュメントで関連する箇所を見てみた。
関数を 実行 (execution) するとき、関数のローカル変数のために使われる新たなシンボルテーブル (symbol table) が用意されます。 もっと正確にいうと、関数内で変数への代入を行うと、その値はすべてこのローカルなシンボルテーブルに記憶されます。 一方、変数の参照を行うと、まずローカルなシンボルテーブルが検索され、次にさらに外側の関数のローカルなシンボルテーブルを検索し、その後グローバルなシンボルテーブルを調べ、最後に組み込みの名前テーブルを調べます。 従って、関数の中では (グローバル変数が global 文で指定されていたり、外側の関数の変数が nonlocal 文で指定されていない限り) グローバル変数や外側の関数の変数に直接値を代入できませんが、参照することはできます。
https://docs.python.org/ja/3/tutorial/controlflow.html?highlight=nonlocal
上記部分によると、
変数の参照時はローカルのシンボルテーブルから組み込みの名前テーブルまで遡って検索してくれるが、
変数の代入時はローカルのシンボルテーブルまでしか検索しないため、そんな変数は存在しないと言われる
ようであった。
2.パターン3,4において、global宣言をしていないのになぜprintも代入も出来たのか?
これをはっきりさせるため、追加で実験を行った。
パターン5
def bbb(): b[0] = 10 print("locals", locals()) print("globals", globals()) b = [1, 2, 3] bbb() print(b) #locals {} #globals {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'bbb': <function bbb at 0x104e156a8>, 'b': [10, 2, 3]} #[10, 2, 3]
locals(),globals()でローカル変数リスト、global変数リストを確認している。
list型だからといってローカル変数に自動で組み込まれる、などということは無いことが確認できた。
更にもう一つ実行してみた。
パターン6
def bbb(): b = b.append(10) print(b) print("locals", locals()) print("globals", globals()) b = [1, 2, 3] bbb() #UnboundLocalError: local variable 'b' referenced before assignment
これらの結果から、
パターン5ではb[0]と要素の指定をしている際に変数の参照を行っており、自動でローカルのシンボルテーブルから組み込みの名前テーブルまで遡って検索されたため、その後の代入も行うことができた。
パターン6ではb.append(10)と変数の参照を行っていないためローカルのシンボルテーブルのみ検索され、ローカルのシンボルテーブルには変数bが存在しなかったため、エラーとなった。
のではないかと思う。