분실 신고서
LOST-005
신고인
보안 검색대 운영팀 한 주임
잃어버린 것
검색한 가방 수를 세는 카운터. 맨 위에 scanned = 0이 분명히 있는데, scan()을 부르자마자 UnboundLocalError가 난다.
해결 조건
함수가 왜 바깥 이름을 보지 못했는지 설명하고, 카운터가 제대로 세도록 고친다.
하늘섬 국제공항 수하물 센터
도미란 반장이 벽에 붙은 공항 안내도를 손가락으로 짚습니다. "탑승구, 터미널, 공항, 국가. 출입증은 안쪽부터 확인해. 안쪽에 있으면 바깥은 보지도 않아."
오늘의 신고는 보안 검색대 카운터입니다. 바깥에 scanned = 0을 두고 함수 안에서 하나씩 올리려 했는데, 첫 줄부터 "그런 값이 없다"는 오류가 납니다. 반장은 함수 맨 아래 줄을 가리킵니다. "범인은 여기야. 값을 넣는 줄."
이름을 읽을 때 파이썬은 네 구역을 안쪽부터 차례로 찾습니다. 처음 찾은 곳에서 멈춥니다.
| 순서 | 구역 | 무엇 | 공항으로 치면 |
|---|---|---|---|
| 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번 줄은 실행되지도 않았습니다. 그런데도 파이썬은 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교시 클로저에서 다시 씁니다.
파이썬에서 새 구역을 만드는 것은 함수(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가 함수 전체의 지역 이름이 되어 첫 줄에서 UnboundLocalError. 값을 인자로 받아 돌려주는 방식으로 해결.
boarding 안에 zone = "L"을 print 아래에 한 줄 추가하면 어떻게 될까요? 스코프 지도에서 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
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.