LEGB 스코프 탐색 규칙과 global·nonlocal 키워드의 올바른 사용법을 완전히 이해한다.
🎯 이 강의에서 배우는 것
파이썬에서 변수 이름을 조회할 때 어디서부터 찾는가? 그 답이 LEGB 규칙입니다. Local → Enclosing → Global → Built-in 순서로 탐색합니다. 이 규칙을 이해하면 NameError, UnboundLocalError, 섀도잉 문제를 즉시 진단할 수 있습니다.
🗺️ 4가지 스코프와 LEGB 탐색 순서
x = "전역" # G: Global 스코프 (모듈 최상위)
def outer():
x = "바깥 함수" # E: Enclosing 스코프
def inner():
x = "안쪽 함수" # L: Local 스코프
print(x) # L에서 찾음: "안쪽 함수"
inner()
print(x) # E에서 찾음: "바깥 함수"
outer()
print(x) # G에서 찾음: "전역"
# 각 함수가 독립적인 지역 스코프를 가짐
def func_a():
value = 10
print(value)
def func_b():
value = 20 # func_a의 value와 무관한 별개의 변수
print(value)
func_a() # 10
func_b() # 20
# B: Built-in 스코프 — 파이썬 내장 이름
print(len([1, 2, 3])) # len은 Built-in
print(type(42)) # type도 Built-in
print(abs(-5)) # abs도 Built-in
# 내장 함수를 섀도잉하면 위험!
len = 5 # ← 전역 변수 len이 내장 함수 len을 가림
# print(len([1, 2, 3])) ← TypeError: 'int' is not callable
del len # 전역 변수 삭제 → 내장 len 다시 사용 가능
🚨 UnboundLocalError — 가장 흔한 스코프 버그
count = 0
def increment_buggy():
# 함수 본문에 count += 1이 있으므로 count는 지역 변수로 취급됨
# 그런데 += 1은 count를 먼저 읽어야 함 → 아직 지역 변수에 값이 없음!
count += 1 # UnboundLocalError!
# 왜 그런가?
# 파이썬은 함수를 컴파일할 때 할당문(=)이 있는 이름을 지역 변수로 표시
# count += 1 은 count = count + 1 과 같음 → 할당이 있음 → 지역 변수!
# 하지만 right-hand side의 count를 읽을 때는 아직 지역 count가 없음 → 에러
# 해결 1: global 선언
def increment_global():
global count # count는 전역 변수다!
count += 1
increment_global()
print(count) # 1
# 해결 2: 함수 인자로 받고 반환 (권장)
def increment_pure(n):
return n + 1
count = increment_pure(count)
print(count) # 2
# 또 다른 UnboundLocalError 예시
def tricky():
if False:
x = 10 # 실행 안 되지만 컴파일 시 x를 지역 변수로 표시
print(x) # UnboundLocalError: local variable 'x' referenced before assignment
# 조건부 할당이 있는 경우, 항상 초기화 먼저!
def safe():
x = None # 초기화
if False:
x = 10
print(x) # None (안전)
🌐 global 선언 — 사용과 남용
db_connection = None # 전역 상태
def connect_db(host):
global db_connection # 전역 변수를 수정하겠다는 선언
db_connection = f"connected to {host}"
connect_db("localhost")
print(db_connection) # "connected to localhost"
# global의 문제점
request_count = 0
def handle_request():
global request_count
request_count += 1
return f"처리 완료 (총 {request_count}건)"
print(handle_request()) # 처리 완료 (총 1건)
print(handle_request()) # 처리 완료 (총 2건)
# 테스트 어려움: 테스트 간 상태가 공유됨
# 병렬 실행 시 경쟁 조건(race condition) 위험
# 권장 대안: 클래스로 상태 캡슐화
class RequestCounter:
def __init__(self):
self.count = 0
def handle(self):
self.count += 1
return f"처리 완료 (총 {self.count}건)"
counter = RequestCounter()
print(counter.handle()) # 처리 완료 (총 1건)
# 각 테스트에서 새 counter 인스턴스 생성 → 독립적
🔗 nonlocal — 중첩 함수에서 바깥 변수 수정
def make_counter():
count = 0 # Enclosing 스코프의 변수
def increment():
nonlocal count # Enclosing 스코프의 count를 수정하겠다는 선언
count += 1
return count
def reset():
nonlocal count
count = 0
return increment, reset
inc, rst = make_counter()
print(inc()) # 1
print(inc()) # 2
print(inc()) # 3
rst()
print(inc()) # 1 ← 리셋 후
# nonlocal 없이 읽기만 할 때는 선언 불필요
def make_greeter(prefix):
def greet(name):
return f"{prefix}, {name}!" # prefix 읽기만 — nonlocal 불필요
return greet
hello = make_greeter("안녕하세요")
print(hello("홍길동")) # 안녕하세요, 홍길동!
# nonlocal vs global 비교
total_global = 0
def outer_func():
total_enclosing = 0
def inner_modify():
nonlocal total_enclosing # outer_func의 total_enclosing 수정
global total_global # 모듈 최상위 total_global 수정
total_enclosing += 1
total_global += 1
inner_modify()
inner_modify()
print(f"enclosing: {total_enclosing}") # 2
outer_func()
print(f"global: {total_global}") # 2
🔬 스코프 검사 도구
x = 10
y = 20
def my_func():
z = 30
# 현재 지역 스코프의 변수 딕셔너리
local_vars = locals()
print("지역:", local_vars) # {'z': 30}
# 전역 스코프의 변수 딕셔너리 (매우 큼!)
global_vars = globals()
print("x in globals:", "x" in global_vars) # True
my_func()
# 변수가 어느 스코프에 있는지 확인
import sys
def where_is(name):
frame = sys._getframe(1) # 호출자 프레임
if name in frame.f_locals:
print(f"{name!r}: Local")
elif name in frame.f_globals:
print(f"{name!r}: Global")
elif name in dir(__builtins__):
print(f"{name!r}: Built-in")
else:
print(f"{name!r}: 없음")
where_is("x") # Global
where_is("len") # Built-in
where_is("zzz") # 없음
⚠️ 자주 하는 실수
- UnboundLocalError 이해 못 함: 함수 안에
x += 1이 있으면 x 전체가 지역 변수가 됩니다. 전역 변수를 사용하려면global x선언이 필요하고, 더 좋은 방법은 함수 인자로 받고 반환하는 것입니다. - 섀도잉 실수:
list = [1, 2, 3]처럼 내장 함수 이름을 변수명으로 사용하면 해당 스코프에서 내장 함수를 사용할 수 없습니다. 변수명은 내장 이름과 충돌하지 않도록 하세요. - nonlocal과 global 혼동: nonlocal은 바로 바깥 함수의 변수를, global은 모듈 최상위 변수를 가리킵니다. 중첩 함수 안에서 모듈 변수를 수정하려면 global, 바깥 함수 변수를 수정하려면 nonlocal을 사용하세요.
📝 정리 및 다음 강의 예고
- LEGB: Local → Enclosing → Global → Built-in 순서로 이름을 탐색합니다.
- 함수 안에 할당(=)이 있으면 그 이름은 지역 변수 — 전역 변수를 수정하려면
global선언 필요. nonlocal은 중첩 함수에서 바깥 함수 변수를 수정할 때 사용합니다.global남용은 테스트와 추론을 어렵게 만들므로, 대신 인자/반환값이나 클래스를 사용하세요.
다음 강의: 클로저 — 함수가 자신이 정의된 환경을 기억하는 방법. nonlocal과 클로저의 관계, 팩토리 패턴, 메모이제이션을 배웁니다.
관련 주제
- LEGB 스코프 탐색순서
- 지역·전역·내장 스코프
- UnboundLocalError
- global 키워드
- nonlocal 키워드
- 변수 섀도잉
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
파이썬 기초 40강 — 처음 배우는 프로그래밍
이 강의가 속한 시리즈는 총 32강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
0/1000
불러오는 중...
