파이썬 함수에서 UnboundLocalError가 발생하면 먼저 같은 이름의 할당문 때문에 변수가 지역변수로 해석됐는지, 그리고 실행된 경로에서 값이 할당됐는지를 확인해야 합니다. 함수 바깥에 같은 이름의 값이 있어도 함수 안에서 할당이 일어나면 그 이름은 기본적으로 지역변수로 취급됩니다. 따라서 자동화 코드에서는 global을 먼저 붙이기보다 상태를 인자로 받고 결과를 반환하는 구조를 우선 검토하는 편이 추적하기 쉽습니다.
왜 바깥에 값이 있는데도 UnboundLocalError가 날까
UnboundLocalError는 함수나 메서드의 지역변수를 참조했지만 값이 바인딩되지 않았을 때 발생하는 예외입니다. Python 공식 FAQ는 함수 본문 어디에서든 할당된 이름이 명시적인 global 또는 nonlocal 선언이 없으면 지역변수로 취급된다고 설명합니다.
count = 10
def increase():
count += 1
return count
이 함수에서 count += 1은 전역 count를 읽고 더하는 문장처럼 보이지만, 함수 안의 할당 때문에 count를 지역변수로 해석합니다. 증감 연산은 읽기와 할당이 결합된 형태이므로, 아직 값이 바인딩되지 않은 지역변수를 먼저 읽으려다 오류가 납니다. 이 규칙은 Python Programming FAQ와 assignment statements 문서에서 확인할 수 있습니다.
먼저 확인할 세 가지
오류가 난 줄만 고치기 전에 다음 순서로 범위를 좁히면 좋습니다.
- 함수 전체에서 같은 이름에 할당하는 문장이 있는지 찾습니다.
=,+=,-=, 반복문의 대상,with ... as,except ... as도 이름을 바인딩할 수 있습니다. - 오류가 난 참조보다 앞서 모든 실행 경로에서 값이 할당되는지 봅니다. 조건문 안에서만 초기화했다면 조건이 거짓인 경로가 남습니다.
try안에서만 결과를 만들었다면 예외가 발생한 뒤except또는 함수 끝에서 그 결과를 사용하는 경로가 있는지 확인합니다.
예를 들어 다음 코드는 파일을 읽는 데 성공한 경우에만 result가 생깁니다.
def load_status(path):
try:
result = read_status(path)
except OSError:
print("상태 파일을 읽지 못했습니다.")
return result
여기서 핵심은 전역변수 여부가 아니라 OSError 경로에서 result가 바인딩되지 않는다는 점입니다. 예외를 정상적인 결과로 처리할 것인지, 호출자에게 예외를 전달할 것인지 먼저 결정하고 반환 계약을 정해야 합니다. 예를 들어 실패를 명시적인 값으로 표현한다면 다음처럼 시작할 수 있습니다.
def load_status(path):
result = None
try:
result = read_status(path)
except OSError:
pass
return result
다만 None이 실제 데이터로도 쓰일 수 있다면 호출자가 성공과 실패를 구분하기 어렵습니다. 그런 경우에는 예외를 다시 올리거나, 성공 여부와 값을 함께 반환하는 등 함수의 결과 형태를 분명히 정하는 편이 낫습니다.
자동화 상태는 인자와 반환값으로 먼저 전달하기
예약 작업이나 배치 함수가 설정값·카운터·처리 결과를 사용한다면 호출부에서 입력을 넘기고 함수가 새 상태를 반환하도록 만들 수 있습니다.
def process_batch(items, processed_count):
current_count = processed_count
for item in items:
handle(item)
current_count += 1
return current_count
processed_count = process_batch(items, processed_count)
이 방식은 함수 안에서 바깥 이름을 몰래 바꾸지 않고, 어떤 값이 들어와 어떤 값이 나가는지 호출부에서 확인하게 해 줍니다. Python 함수는 인자를 받고 return으로 값을 돌려줄 수 있으며, 함수 끝에 도달하거나 표현식 없는 return은 None을 반환합니다. 이 동작은 Python 공식 함수 정의 문서의 인자와 반환값 설명을 기준으로 확인할 수 있습니다.
전달할 상태가 여러 개라면 딕셔너리나 별도의 설정 객체처럼 의미를 묶은 값을 인자로 넘기는 방법도 있습니다. 중요한 것은 전역변수를 없애는 것 자체가 아니라, 상태 변경의 주체와 반환 결과를 코드에서 읽을 수 있게 만드는 것입니다.
global과 nonlocal은 언제 선택할까
함수 안에서 전역 이름을 실제로 변경해야 한다면 global을 선언할 수 있습니다.
count = 0
def increase_global():
global count
count += 1
global은 현재 스코프의 이름을 전역으로 해석하게 하고, 그 이름에 대한 할당이 전역 바인딩을 변경하도록 합니다. 문법적으로는 해결되지만 여러 작업이 같은 상태를 공유하면 호출 순서와 변경 지점을 따라가야 합니다. 따라서 작은 단일 진입점의 상태처럼 전역 공유가 의도된 경우에만 선택하고, 일반적인 자동화 처리 결과에는 인자와 반환값을 먼저 검토하는 것이 좋습니다. 이 판단은 Python 문서가 global보다 다른 방식을 항상 권장해서가 아니라, 함수의 입력·출력 계약을 분명히 하려는 설계 원칙에 따른 것입니다.
중첩 함수가 바깥 함수의 지역변수를 다시 바인딩해야 하는 경우에는 nonlocal이 대상입니다.
def make_counter():
count = 0
def next_count():
nonlocal count
count += 1
return count
return next_count
nonlocal은 가장 가까운 바깥 함수 스코프에 이미 바인딩된 이름을 가리킵니다. 전역 상태를 공유하는 global과 적용 범위가 다르므로, 중첩 함수의 클로저 상태를 바꿀 때만 고려하면 됩니다. 두 선언의 규칙은 Python simple statements 문서에서 확인할 수 있습니다.
운영 코드에 적용하는 선택 순서
다음 순서로 결정하면 오류 수정과 구조 개선을 분리할 수 있습니다.
- 오류 줄의 이름이 함수 안에서 할당되는지 확인합니다.
- 조건문·반복문·예외 처리의 모든 경로에서 초기화되는지 확인합니다.
- 한 번의 호출에 필요한 입력과 결과라면 인자와 반환값으로 바꿉니다.
- 중첩 함수가 바깥 함수의 상태를 유지해야 할 때만
nonlocal을 검토합니다. - 여러 함수가 의도적으로 하나의 프로세스 전역 상태를 변경해야 할 때만
global을 선택하고, 변경 지점을 문서화합니다.
핵심은 global을 금지하거나 모든 변수를 무조건 초기화하는 데 있지 않습니다. 먼저 Python의 이름 바인딩 규칙과 실제 실행 경로를 확인하고, 자동화 작업의 상태가 함수 경계를 넘어야 하는지 판단해야 합니다. 다음으로는 함수 인자와 반환값으로 자동화 상태를 넘기는 법을 별도 주제로 이어서 살펴볼 수 있습니다.