Tech カテゴリのイメージ

UnboundLocalError: local variable referenced before assignment の原因と直し方[Python]

外側で定義した変数を関数の中で使おうとしただけなのに、こんなエラーが出ることがある。

UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

古いバージョンのPythonだとこう表示される。

UnboundLocalError: local variable 'count' referenced before assignment

一見すると「定義したはずなのに」と混乱するが、Pythonのスコープの決め方を知ると理由がはっきりする。

なぜ起きるのか

Pythonは関数を実行する前に、その関数の中身を見て「この名前はローカル変数かどうか」を先に決めてしまう。判定の基準は単純で、関数の中のどこかにその名前への代入があれば、その名前は最初から最後までローカル変数として扱われる。

count = 0
def add():
print(count) # ここで UnboundLocalError
count = count + 1

print(count)の時点ではまだ代入していないので外側のcountを読みそうに見えるが、関数の後半にcount = ...があるため、Pythonはこの関数内のcountをすべてローカル変数だと判断している。ローカル変数countはまだ値が入っていないので、参照した瞬間にエラーになる。

代入が参照より後ろにあっても関係ないのがポイントになる。位置ではなく、関数全体に代入が存在するかどうかで決まる。

直し方1. 引数と戻り値にする(推奨)

いちばん素直で、あとから読んで混乱しない形になる。

def add(count):
print(count)
return count + 1
count = 0
count = add(count)

外の状態を関数が勝手に書き換えないので、テストもしやすくなる。基本的にはこの形にできないか最初に検討したい。

直し方2. global を使う

どうしてもモジュール全体の変数を書き換えたい場合はglobalで宣言する。

count = 0
def add():
global count
print(count)
count = count + 1

ただしこれは、どこから書き換えられるかが追いにくくなるので、多用しないほうがいい。

直し方3. nonlocal を使う(入れ子の関数の場合)

外側が関数の場合はglobalではなくnonlocalになる。

def outer():
count = 0
def inner():
nonlocal count
count += 1
inner()
return count

globalはモジュールの一番外のスコープを指すので、入れ子関数で使うと目的の変数に届かない。ここは間違えやすい。

つまずきやすいパターン

+= は代入である

読み出しだけのつもりでも、+=は代入なのでローカル判定を引き起こす。

total = 0
def f():
total += 1 # UnboundLocalError

if の中だけで代入している

条件が成立しなかった場合、代入が実行されないまま参照に到達する。これは関数の外側に同名の変数がなくても起きる。

def f(flag):
if flag:
msg = "ok"
print(msg) # flag が False だと UnboundLocalError

先に初期値を入れておけば解決する。

def f(flag):
msg = ""
if flag:
msg = "ok"
print(msg)

try の中でだけ代入している

例外が起きると代入されないままexceptや後続に進む。

def f():
try:
result = risky()
except Exception:
pass
return result # 例外時に UnboundLocalError

これも初期化しておくか、except側でも値を入れる。

def f():
result = None
try:
result = risky()
except Exception:
pass
return result

ループが一度も回らなかった

反復対象が空だと、ループ内の変数は作られない。

def f(items):
for x in items:
last = x
return last # items が空だと UnboundLocalError

確認のしかた

どの名前がローカル扱いになっているかは、コンパイル結果から確認できる。

def f():
print(count)
count = 1
print(f.__code__.co_varnames) # ('count',) と出ればローカル判定されている

ここに名前が出ていれば、その関数内では外側の変数ではなくローカル変数として扱われている。

📘 こうした細かい挙動をまとめて押さえるなら、独習Python

まとめ

・Pythonは関数内に代入があれば、その名前を関数全体でローカル変数と判定する。代入が参照より後ろでも同じ
・まずは引数と戻り値で受け渡す形に直せないかを検討する。いちばん安全で読みやすい
・モジュール変数を書き換えるならglobal、外側の関数の変数ならnonlocalを使う。両者は指す先が違う
+=も代入なので同じエラーを引き起こす
・if、try、forの中でだけ代入している場合、その分岐を通らないと未定義になる。先に初期値を入れておく
f.__code__.co_varnamesでローカル判定された名前を確認できる

※本記事にはアフィリエイト広告を含みます。