紛失届
LOST-005
届出人
保安検査場運営チームのハンさん
なくしたもの
検査したかばんの数を数えるカウンター。いちばん上に scanned = 0 が確かにあるのに、scan() を呼んだとたん UnboundLocalError が出る。
解決の条件
関数がなぜ外側の名前を見られなかったのかを説明し、カウンターが正しく数えるように直す。
ハヌルソム国際空港 手荷物センター
ド・ミラン班長が、壁に貼られた空港の案内図を指でたどります。「搭乗口、ターミナル、空港、国。通行証は内側から確認する。内側にあれば、外側は見もしない」
今日の届けは、保安検査場のカウンターです。外側に scanned = 0 を置き、関数の中で一つずつ増やそうとしたのに、1行目から「そんな値はない」というエラーが出ます。班長は関数のいちばん下の行を指さします。「犯人はここ。値を入れている行だよ」
名前を読むとき、Pythonは四つの区域を内側から順に探します。最初に見つかったところで止まります。
| 順番 | 区域 | 何か | 空港でいえば |
|---|---|---|---|
| L | ローカル(Local) | いま実行中の関数の中 | 搭乗口 |
| E | 外側の関数(Enclosing) | その関数を囲む外側の関数たち | ターミナル |
| G | グローバル(Global) | 関数が定義されたモジュール | 空港 |
| B | 組み込み(Built-in) | len、print、list のような基本の名前 | 国 |
zone = "G"
def terminal():
gate = "E"
def boarding():
seat = "L"
print(seat, gate, zone, len("abc"))
boarding()
terminal()
# L E G 3seat は boarding の中(L)で、gate は外側の関数 terminal(E)で見つかります。zone はモジュール(G)で、len は組み込み(B)で見つかります。NameError です。boarding を別の場所から呼んでも、gate は terminal で探します。下のスコープ地図は、コードを実行せずに読むだけで、名前ごとにどの区域で見つかるかを示します。最後の broken 関数は、次の節の主役です。
スコープ地図
1
zone = "G"
2
3
def terminal():
4
gate = "E"
5
def boarding():
6
seat = "L"
7
print(seat, gate, zone, len("abc"))
8
boarding()
9
10
def broken():
11
print(zone)
12
zone = "L"
探す名前
seat @ terminal を選んでみてください。seat は内側の関数 boarding のローカル名なので、外側の terminal からは見えません。探す方向はいつも内から外へです。zone @ broken は L で「ここで見つかった」と表示されます。名前がその関数のローカル名に決まったという意味であって、値が入っているという意味ではありません。届出人のコードです。
scanned = 0
def scan(bag):
print(bag, scanned)
scanned = scanned + 1
scan("T1")
# UnboundLocalError: cannot access local variable 'scanned' where it is not associated with a valueエラーは5行目ではなく、4行目の print で出ます。5行目はまだ実行すらされていません。それでもPythonは、4行目の scanned を外側で探しません。
規則は一つです。関数の中のどこかで名前に値を入れると、その名前は関数全体でローカル名になります。この決定は実行時ではなく、関数をコンパイルするときに下されます。
def scan(bag):
print(bag, scanned)
scanned = scanned + 1
print(scan.__code__.co_varnames)
# ('bag', 'scanned')
print(issubclass(UnboundLocalError, NameError))
# Trueco_varnames は、コンパイラが決めておいたローカル名の一覧です。scanned がすでに入っています。scanned」を探し、そのタグはまだどのかばんにもぶら下がっていません。エラーメッセージが言っているのはまさにそのことです。UnboundLocalError は NameError の一種です。実行されない行も同じように数えられます。
x = 10
def f():
print(x)
if False:
x = 1
f()
# UnboundLocalError: cannot access local variable 'x' where it is not associated with a value「値を入れる」のは = だけではありません。+=、for x in ...、import x、def x():、class x:、with ... as x、except ... as x、del x も、すべてその関数のローカル名を作ります。
下のタグボードで三つのやり方を比べてみてください。一つ目は届出人のコードで、最後のステップにエラーが表示されます。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
ピンの案内
→ UnboundLocalError を見たら、エラーの出た行より下を見てください。同じ関数のどこかで、その名前に値を入れているはずです。
global と nonlocal (7分)外側の区域の名前に値を入れたいなら、あらかじめ宣言します。
scanned = 0
def scan(bag):
global scanned
scanned = scanned + 1
scan("T1")
scan("T2")
print(scanned)
# 2def terminal():
boarded = 0
def board():
nonlocal boarded
boarded += 1
board()
board()
return boarded
print(terminal())
# 2global x:この関数では、x はモジュールの名前です。値を入れると、モジュールのタグが付け替わります。nonlocal x:この関数では、x はいちばん近い外側の関数の名前です。モジュールの名前には使えません。nonlocal を付けた名前が外側の関数にないと、実行前に SyntaxError: no binding for nonlocal 'x' found になります。global x と書くと、SyntaxError: name 'x' is assigned to before global declaration です。読むだけなら宣言はいりません。5-1の boarding は、宣言なしで gate と zone を読んでいました。宣言が必要なのは値を入れるときだけです。
global は関数の外の状態をこっそり変えるので、関数を単独でテストしにくくします。たいていは、値を引数で受け取って新しい値を返すほうがよいでしょう。nonlocal は6限目のクロージャでまた使います。
Pythonで新しい区域を作るのは、関数(def、lambda)、内包表記、クラス本体です。if、for、while、with、try のブロックは区域を作りません。
for i in range(3):
pass
print(i)
# 2
if True:
inside = "if"
print(inside)
# if
sq = [n * n for n in range(3)]
print(sq)
# [0, 1, 4]
print(n)
# NameError: name 'n' is not defined| 作る | 作らない |
|---|---|
def、lambda、リスト・集合・辞書の内包表記、ジェネレーター式、class 本体 | if、for、while、with、try |
for が終わっても、ループ変数 i は最後の値(2)のまま残ります。ループの中で作った名前も同じです。n は外に漏れません。3.12からCPythonは内包表記を別の関数にせず、展開して実行しますが(PEP 709)、名前が漏れないという規則は変わりません。組み込みの名前は、いちばん外側(B)にあります。だから、グローバルに同じ名前を作ると、それ以降はグローバルの名前が先に見つかります。
list = ["T1", "T2"]
print(list("ab"))
# TypeError: 'list' object is not callablelist というタグがグローバルでリストのかばんにぶら下がったので、list("ab") はそのリストを呼ぼうとして失敗します。組み込みの list が消えたわけではありません。隠れただけです。
list = ["T1", "T2"]
del list
print(list("ab"))
# ['a', 'b']
import builtins
print(builtins.list is list, builtins.len("abc"))
# True 3list を外せば、また組み込みの list が見えます。builtins モジュールに入っています。隠れていても、builtins.list でいつでも取り出せます。shout = print のあとで del print をすると、NameError: name 'print' is not defined になります。グローバルに print というタグはないので、外すものもないのです。list、dict、str、id、type、input、max、sum のような名前を変数に使わないでください。そのファイルの中で、組み込み関数を隠してしまいます。
locals()、globals()、vars()、dir() (4分)globals():モジュールの区域の辞書。モジュールで呼べば、本物の保管場所そのものです。locals():いまの区域の名前たち。モジュールでは globals() と同じです。vars(obj):オブジェクトの __dict__。引数なしで呼ぶと locals() と同じです(7限目)。dir():名前の一覧だけを並べ替えて返します。関数の中の locals() は、3.13から意味がはっきりしました(PEP 667)。呼ぶたびにその瞬間のスナップショットを新しい辞書として返します。その辞書を書き換えても、ローカル変数は変わりません。
def check():
y = 1
locals()["y"] = 99
print(y, locals()["y"])
snap = locals()
y = 2
print(snap["y"], locals()["y"])
check()
# 1 1
# 1 2
def where(a):
b = 2
return dir(), vars()
print(where(1))
# (['a', 'b'], {'a': 1, 'b': 2})locals()["y"] = 99 は、いま作ったスナップショットを書き換えるだけです。変数 y は1のままです。snap は撮った瞬間の値(1)を保っています。もう一度呼んだ locals() で、ようやく2が見えます。locals() ではなく辞書を使ってください。カウンターの直し方は二つあります。外側のタグを直接付け替える(global)か、値を受け取って新しい値を返すかです。
scanned = 0
def scan(bag):
global scanned
print(bag, scanned)
scanned = scanned + 1
scan("T1")
scan("T2")
print(scanned)
# T1 0
# T2 1
# 2
def scan_count(bag, count):
print(bag, count)
return count + 1
total = 0
total = scan_count("T1", total)
total = scan_count("T2", total)
print(total)
# T1 0
# T2 1
# 2ハンさんは二つ目のやり方を選びます。検査場が何台に増えても、台ごとに別々に数えられるからです。班長が付け加えます。「区域ごとに自分のタグがある。よその区域のタグを付け替えたいなら、まず通行証を見せないとね」
解決報告 — LOST-005。関数の下のほうにある scanned = ... のせいで scanned が関数全体のローカル名になり、1行目で UnboundLocalError。値を引数で受け取って返すやり方で解決。
boarding の中で、print の下に zone = "L" を1行足すとどうなるでしょう? スコープ地図の zone @ broken と比べてみてください。nonlocal を使わずに、外側の関数のリストに append して個数を数えるカウンターを作ってみてください。なぜ宣言なしでも動くのでしょう?(ヒント:タグの付け替えと、かばんの変更)sum = 0 を置いてから sum([1, 2, 3]) を呼ぶと、どんなエラーになるでしょう? builtins を使って直してみてください。班長の業務手帳
名前は L → E → G → B の順に探し、最初に見つかったところで止まる。
区域は、どこで呼ぶかではなく、どこに書いたかで決まる。
関数の中のどこかで値を入れると、その名前は関数全体でローカル名。だから UnboundLocalError。
global、nonlocal は外側のタグを付け替えるときだけ。読むだけなら不要。
ループや if は区域を作らず、関数と内包表記は作る。
list = … は組み込みの list を隠す。組み込みの名前は builtins にある。
関数の中の locals() はスナップショット(3.13、PEP 667)。書き換えても変数はそのまま。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。