함수 네임스페이스와 스코프
이 토픽을 마치면
Python이 변수 이름을 어떤 순서로 찾는지(LEGB 규칙) 설명할 수 있고, 지역 변수와 전역 변수가 충돌하는 상황을 올바르게 처리할 수 있습니다.
같은 이름, 다른 값
x = 10
def foo(): x = 20 print(x)
foo() # 20print(x) # 10함수 안의 x와 바깥의 x는 이름은 같지만 서로 다른 변수입니다. 이것을 이해하려면 네임스페이스와 스코프를 알아야 합니다.
네임스페이스란
네임스페이스(namespace)는 이름과 객체의 매핑을 담는 공간입니다. Python에서는 딕셔너리처럼 {이름: 객체} 형태로 동작합니다.
x = 10name = "철수"# 모듈 네임스페이스: {"x": 10, "name": "철수", ...}def greet(): msg = "안녕" # 함수 네임스페이스: {"msg": "안녕"}함수를 호출하면 새로운 네임스페이스가 생기고, 함수가 끝나면 사라집니다. 그래서 함수 안에서 만든 변수는 밖에서 쓸 수 없습니다.
스코프와 LEGB 규칙
스코프(scope)는 코드에서 이름이 보이는 범위입니다. Python은 변수를 찾을 때 LEGB 순서로 탐색합니다:
L — Local 함수 내부
E — Enclosing 감싸고 있는 함수 (중첩 함수)
G — Global 모듈 최상위
B — Built-in Python 내장 (print, len, ...)x = "global" # G
def outer(): x = "enclosing" # E def inner(): x = "local" # L print(x) inner()
outer() # "local" — L에서 찾았으므로 더 이상 탐색 안 함가장 안쪽(L)부터 찾고, 없으면 바깥으로 나갑니다. 찾는 즉시 멈춥니다.
Local — 함수 내부 변수
def calculate(): result = 42 # Local 변수 temp = result * 2 return temp
calculate()print(result) # NameError: name 'result' is not defined함수 안에서 대입(=)한 변수는 자동으로 Local 변수가 됩니다. 함수 밖에서는 존재하지 않습니다.
Global — 모듈 최상위 변수
counter = 0 # Global
def increment(): print(counter) # ✅ 읽기는 가능 (LEGB로 G에서 발견) def reset(): counter = 0 # ⚠️ 새로운 Local 변수가 생김! Global의 counter와 무관
reset()print(counter) # 여전히 0 (Global은 변경 안 됨)함수 안에서 Global 변수를 읽는 것은 자유롭게 가능합니다. 하지만 대입하면 Python은 그것을 새로운 Local 변수로 인식합니다.
global 키워드
Global 변수를 함수 안에서 수정하려면 global 키워드를 씁니다:
counter = 0
def increment(): global counter counter += 1
increment()increment()print(counter) # 2global counter는 "이 함수 안의 counter는 Local이 아니라 Global의 것"이라는 선언입니다.
하지만 global을 남용하면 어디서 값이 바뀌는지 추적하기 어려워집니다. 가능하면 함수의 매개변수와 반환값으로 데이터를 주고받는 것이 좋습니다.
Enclosing — 중첩 함수와 nonlocal
def outer(): count = 0 def inner(): nonlocal count count += 1 return count print(inner()) # 1 print(inner()) # 2
outer()nonlocal은 "이 변수는 Local이 아니라 바로 바깥 함수(Enclosing)의 것"이라는 선언입니다. 클로저를 만들 때 사용합니다.
흔한 실수 — UnboundLocalError
x = 10
def broken(): print(x) # ❌ UnboundLocalError! x = 20
broken()에러가 발생합니다. Python은 함수 전체를 미리 분석해서, x = 20이 있으면 x를 Local 변수로 결정합니다. 그런데 print(x) 시점에는 아직 Local x에 값이 할당되지 않았으므로 에러가 납니다.
해결 방법:
def fixed_1(): x = 20 # 먼저 할당 print(x)
def fixed_2(): global x # Global임을 명시 print(x) x = 20Built-in — Python 내장 이름
print(len([1, 2, 3])) # 3 — len은 Built-in
# 실수로 내장 이름을 덮어쓰면list = [1, 2, 3] # ⚠️ Built-in list를 덮어씀new_list = list("abc") # TypeError!list, dict, print, len 같은 이름은 Built-in 네임스페이스에 있습니다. 이 이름을 변수로 쓰면 Built-in이 가려져서 원래 기능을 쓸 수 없게 됩니다.
# 복구del list # Local/Global의 list를 삭제하면 Built-in이 다시 보임클로저 — Enclosing의 실전 활용
def make_multiplier(factor): def multiply(x): return x * factor return multiply
double = make_multiplier(2)triple = make_multiplier(3)
print(double(5)) # 10print(triple(5)) # 15multiply 함수가 반환된 뒤에도 factor를 기억합니다. 이것이 클로저(closure) — 함수가 자신이 정의된 스코프의 변수를 "닫아서 가지고 다니는 것"입니다.
def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment
counter = make_counter()print(counter()) # 1print(counter()) # 2print(counter()) # 3클래스를 만들지 않고도 상태를 유지하는 함수를 만들 수 있습니다.
네임스페이스 디버깅
def debug_scope(): x = 10 y = 20 print(locals()) # {'x': 10, 'y': 20}
debug_scope()print(globals().keys()) # 전역 네임스페이스의 모든 이름locals()와 globals()는 각각 현재 스코프의 네임스페이스를 딕셔너리로 반환합니다. 변수가 어디에 속하는지 확인하고 싶을 때 유용합니다.
import builtinsprint(dir(builtins)) # Built-in 네임스페이스의 모든 이름핵심 정리
| 키워드 | 의미 |
|---|---|
| (없음) | 대입 → Local, 참조 → LEGB 순서 탐색 |
global | "이 변수는 Global의 것" |
nonlocal | "이 변수는 Enclosing 함수의 것" |
탐색 순서: Local → Enclosing → Global → Built-in
대입 규칙: 함수 안에서 = 을 쓰면 Local로 간주모듈과 네임스페이스
import mathfrom os import path
# import는 모듈 이름으로 네임스페이스를 분리print(math.pi) # math 네임스페이스의 piprint(path.exists) # os.path의 exists
# from import *는 네임스페이스를 오염시킴from math import * # ⚠️ 모든 이름이 현재 Global에 쏟아짐print(pi) # 작동하지만, 어디서 온 건지 불명확import math는 math라는 이름 하나만 Global에 추가합니다. from math import *는 수십 개의 이름을 Global에 쏟아부어서 이름 충돌 위험이 커집니다.
데코레이터와 스코프
def log_calls(func): count = 0 def wrapper(*args, **kwargs): nonlocal count count += 1 print(f"[{count}번째 호출] {func.__name__}") return func(*args, **kwargs) return wrapper
@log_callsdef greet(name): return f"안녕, {name}"
greet("철수") # [1번째 호출] greetgreet("영희") # [2번째 호출] greet데코레이터는 클로저의 실전 활용입니다. count는 log_calls의 Local(= wrapper의 Enclosing)에 살면서, nonlocal로 매 호출마다 증가합니다.
네임스페이스 충돌로 인한 버그는 디버깅이 까다롭습니다. "이 변수가 어디 소속인지"를 항상 의식하면 이런 실수를 피할 수 있습니다.