R

ReflexKR

Reflex公式ドキュメント
名札の空港:Pythonのオブジェクトモデル
5限目

時間 3:20–4:10

区域と通行証:スコープ

紛失届

LOST-005

すぐ隣にあるのに見つからない名前

届出人

保安検査場運営チームのハンさん

なくしたもの

検査したかばんの数を数えるカウンター。いちばん上に scanned = 0 が確かにあるのに、scan() を呼んだとたん UnboundLocalError が出る。

解決の条件

関数がなぜ外側の名前を見られなかったのかを説明し、カウンターが正しく数えるように直す。

ハヌルソム国際空港 手荷物センター

ド・ミラン班長が、壁に貼られた空港の案内図を指でたどります。「搭乗口、ターミナル、空港、国。通行証は内側から確認する。内側にあれば、外側は見もしない」

今日の届けは、保安検査場のカウンターです。外側に scanned = 0 を置き、関数の中で一つずつ増やそうとしたのに、1行目から「そんな値はない」というエラーが出ます。班長は関数のいちばん下の行を指さします。「犯人はここ。値を入れている行だよ」

5-1. 名前を探す順番:LEGB (9分)

名前を読むとき、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 3
  • seat は 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"

探す名前

boarding の中の四つの名前は、それぞれ L、E、G、B で見つかります。broken の中の zone は、外側にあるのに L で止まります。
  • seat @ terminal を選んでみてください。seat は内側の関数 boarding のローカル名なので、外側の terminal からは見えません。探す方向はいつも内から外へです。
  • zone @ broken は L で「ここで見つかった」と表示されます。名前がその関数のローカル名に決まったという意味であって、値が入っているという意味ではありません。

5-2. 値を入れる場所がスコープを決める (10分)

届出人のコードです。

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))
# True
  • co_varnames は、コンパイラが決めておいたローカル名の一覧です。scanned がすでに入っています。
  • だから4行目は「ローカルのタグ 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

実習台を準備中…

いま変わったもの

封印されたかばん(イミュータブル)

一つ目のやり方では、scan 枠には bag のタグしかなく、scanned のタグは空のままです。global は外側のタグを使い、三つ目は値を引数で受け取って返します。

ピンの案内

→ UnboundLocalError を見たら、エラーの出た行より下を見てください。同じ関数のどこかで、その名前に値を入れているはずです。

5-3. 外側のタグを直接付け替える:global と nonlocal (7分)

外側の区域の名前に値を入れたいなら、あらかじめ宣言します。

scanned = 0

def scan(bag):
    global scanned
    scanned = scanned + 1

scan("T1")
scan("T2")
print(scanned)
# 2
def terminal():
    boarded = 0
    def board():
        nonlocal boarded
        boarded += 1
    board()
    board()
    return boarded

print(terminal())
# 2
  • global 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限目のクロージャでまた使います。

5-4. 区域を作るもの、作らないもの (5分)

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)、名前が漏れないという規則は変わりません。

5-5. 組み込みの名前を隠してしまう (5分)

組み込みの名前は、いちばん外側(B)にあります。だから、グローバルに同じ名前を作ると、それ以降はグローバルの名前が先に見つかります。

list = ["T1", "T2"]
print(list("ab"))
# TypeError: 'list' object is not callable

list というタグがグローバルでリストのかばんにぶら下がったので、list("ab") はそのリストを呼ぼうとして失敗します。組み込みの list が消えたわけではありません。隠れただけです。

list = ["T1", "T2"]
del list
print(list("ab"))
# ['a', 'b']

import builtins
print(builtins.list is list, builtins.len("abc"))
# True 3
  • グローバルのタグ list を外せば、また組み込みの list が見えます。
  • 組み込みの名前は builtins モジュールに入っています。隠れていても、builtins.list でいつでも取り出せます。
  • 1限目の練習問題3の答えです。shout = print のあとで del print をすると、NameError: name 'print' is not defined になります。グローバルに print というタグはないので、外すものもないのです。

list、dict、str、id、type、input、max、sum のような名前を変数に使わないでください。そのファイルの中で、組み込み関数を隠してしまいます。

5-6. 区域をのぞく: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() ではなく辞書を使ってください。

事件解決 (10分)

カウンターの直し方は二つあります。外側のタグを直接付け替える(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。値を引数で受け取って返すやり方で解決。

練習問題

  1. 5-1の boarding の中で、print の下に zone = "L" を1行足すとどうなるでしょう? スコープ地図の zone @ broken と比べてみてください。
  2. nonlocal を使わずに、外側の関数のリストに append して個数を数えるカウンターを作ってみてください。なぜ宣言なしでも動くのでしょう?(ヒント:タグの付け替えと、かばんの変更)
  3. モジュールのいちばん上に sum = 0 を置いてから sum([1, 2, 3]) を呼ぶと、どんなエラーになるでしょう? builtins を使って直してみてください。

班長の業務手帳

  • 名前は L → E → G → B の順に探し、最初に見つかったところで止まる。

  • 区域は、どこで呼ぶかではなく、どこに書いたかで決まる。

  • 関数の中のどこかで値を入れると、その名前は関数全体でローカル名。だから UnboundLocalError。

  • global、nonlocal は外側のタグを付け替えるときだけ。読むだけなら不要。

  • ループや if は区域を作らず、関数と内包表記は作る。

  • list = … は組み込みの list を隠す。組み込みの名前は builtins にある。

  • 関数の中の locals() はスナップショット(3.13、PEP 667)。書き換えても変数はそのまま。

← 前へ

4限目 · かばんを渡すとき:関数と引数

次へ →

6限目 · 機内持ち込みかばん:クロージャ

新しい授業・講座のお知らせを受け取る

この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。

収集項目:メールアドレス・目的:この講座の新しい授業・公開のお知らせ・保有期間:解除のご依頼まで。他の目的には使わず、第三者にも提供しません。お知らせメールに返信いただければすぐに削除します。

質問と回答

この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。

リンクや書式なしで、そのままの文字で表示されます。

R

ReflexKR

触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。

ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する

アフィリエイトと支援について
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex