문제 상황
정리할 문제는 아래와 같다.
1. 중첩함수 내에서 int 타입 자유변수를 수정할 방법
2. 중첩함수 내부함수에서 외부함수에 선언된 list는 수정이 되는데 int는 수정하려고 하면 에러가 발생하는 원인
며칠 전, 프로그래머스 배달 문제 풀이 코드가 마음에 들지 않아서 째려보고 있다가 찾아보고 작성하는 글이다.
https://chadireoroonu.tistory.com/243
프로그래머스 12978 배달 파이썬 풀이
난이도 : Lv. 2풀이일 : 2412043https://school.programmers.co.kr/learn/courses/30/lessons/12978 프로그래머스SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프programmers.c
chadireoroonu.tistory.com
처음 작성한 코드는 아래와 같다.
import heapq
def solution(N, road, K):
answer = 0
route = [[] for _ in range(N + 1)]
def djikstra(start):
visited = [int(1e9)] * (N + 1)
visited[start] = 0
queue = []
heapq.heappush(queue, [0, start])
while queue:
cost, now = heapq.heappop(queue)
if cost <= visited[now]:
for add, after in route[now]:
if visited[after] > cost + add:
visited[after] = cost + add
heapq.heappush(queue, [cost + add, after])
for i in range(N):
if visited[i + 1] <= K:
answer += 1 ##### 여기가 문제 #####
return answer
for a, b, c in road:
route[a].append([c, b])
route[b].append([c, a])
answer += djikstra(1)
return answer
중첩함수로 작성하였으니, djikstra 안에서 answer를 직접 수정하려고 했는데, 그렇게는 안 된다.

이런 에러가 발생한다. 할당 되기 전에 로컬 변수 answer가 참조되었댄다.
지난 달에 풀었던 문제에서는 이런 에러가 발생하지 않았었는데? 싶어져서 의문이 들었다.
지난 달에 풀었던 문제는 여행경로 문제
https://chadireoroonu.tistory.com/235
프로그래머스 43164 여행경로 파이썬 풀이
난이도 : Lv. 3풀이일 : 2411144https://school.programmers.co.kr/learn/courses/30/lessons/43164 프로그래머스SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프programmers.c
chadireoroonu.tistory.com
작성했던 풀이 코드는 아래와 같다.
def solution(tickets):
answer = []
visited = [False] * len(tickets)
def DFS(route):
if len(route) == len(tickets) + 1:
answer.append(route) ##### 여기는 또 된다 #####
return
for i in range(len(tickets)):
if tickets[i][0] == route[-1] and not visited[i]:
visited[i] = True
DFS(route + [tickets[i][1]])
visited[i] = False
return
DFS(['ICN'])
answer = min(answer)
return answer
이 코드에서도 DFS를 중첩함수로 작성했었는데, DFS 내에서 answer.append가 가능했다.
분명 됐었는데 싶어서 진짜로 되었었나 이전에 작성한 코드를 찾아보고, 근데 이번에는 왜 안될까 싶어서 검색해봤다.
UnboundLocalError
UnboundLocalError는 함수 본문 안에 할당문을 추가하여 기존 변수를 변경하려고 하는 경우에 발생할 수 있다.
지역변수와 자유변수, 전역변수 등과 관련이 있으며 스코프가 어떤 범위의 변수를 찾고 있는지와 연관이 있다.
아래 두 코드를 비교해보자
# 정상 작동
x = 10
def bar():
print(x)
bar() # 10
# 에러 발생
y = 10
def foo():
print(y)
y += 1
foo() # UnboundLocalEror
bar 함수는 정상적으로 실행되지만 foo 함수는 UnboundLocalError가 발생한다.
foo의 y += 1 부분에서 파이썬 컴파일러는 y를 지역변수로 인식하여 y라는 지역변수를 찾지만, y는 함수 외부에 정의되어 있어 문제가 된다.
파이썬 변수의 범위
지역변수
함수 내에서 선언된 변수
자유 변수
함수가 선언되는 외부 함수에서 선언된 변수
전역 변수
외부에서 선언된 변수
global
global_stmt ::= "global" identifier ("," identifier)*
global x 형식으로 global을 변수 앞에 붙이면 해당 변수를 전역변수로 인식하게 된다.
global 사용 없이 전역 변수에 할당하는건 불가능하지만, 자유 변수는 전역으로 선언되지 않아도 전역을 참조할 수 있다.
global문은 함수나 클래스 본문 전체 범위에 적용되며, 변수가 범위에서 전역 선언 전에 사용되거나 할당될 경우에는 SyntaxError가 발생한다.
사용 예시 코드
x = 10
def fooba():
global x
print(x)
x += 1
foobar() # 10
이건 알고 있다.
싸피에서 알고리즘 풀 때도 global 써와서 이건 알고 있다.
원래 코드에서 answer를 global로도 선언해보았지만 문제가 해결되지 않았었다.
재현코드

NameError로 에러는 바뀌었다.
변수 선언 없이 변수를 사용하려고 했다는 의미인데, solution 함수에 선언된 answer를 인식하지 못하는 것 같았다.
그래서 count 변수를 선언하고 djikstra 함수의 반환값으로 count를 넘겼는데 찾아보다가 nonlocal을 알게되었다.
nonlocal
nonlocal_stmt ::= "nonlocal" identifier ("," identifier)*
nonlocal은 global과 비슷한 일을 하는데, 중첩함수나 중첩클래스 내에서 사용된다.
nonlocal x 형태로 사용하면 이전 nonlocal 범위에서 바인딩된 이름을 참조할 수 있다.
만약, 해당 변수명이 두 개 이상의 nonlocal 범위에서 바인딩 된 경우 가장 가까운 바인딩이 사용되며, nonlocal 범위에서 바인딩 된 적이 없는 변수명이거나 nonlocal 범위가 없는 경우에는 SystaxError가 발생한다.
그러니까 딱 내가 찾던 기능이다.
사용 예시 코드
def foo():
x = 10
def bar():
nonlocal x
print(x)
x += 1
bar() # 10
print(x)
foo() # 11
foo 함수 안에 bar 함수가 중첩함수로 작성되어 있고, x는 foo 내부에 선언되어 있다.
bar 함수에서 foo 함수에 선언된 x에 접근하기 위해 global이 아닌 nonlocal을 사용해 문제를 해결할 수 있다.
재현 코드

프로그래머스 문제는 solution 함수 내에 코드를 작성하게 되어 있어서 global 말고 nonlocal을 사용해주면 된다.
이게 이상한 방법 같아서 풀이 코드는 다시 수정했지만 최초 목표로 한 건 된다.
메모리 할당 방식 차이
nonlocal로 해결 방법을 찾은 건 좋은데 리스트는 nonlocal 없이도 수정 되던데? 하는 생각이 들었다.
그러면 이제 왜 리스트는 nonlocal 없이 수정이 가능하고, int는 안되는지 살펴보자.
파이썬의 메모리 할당 방식 차이 때문에 두 상황이 다른 결과가 된다.
파이썬에는 상태 변경이 가능한 가변변수 Mutable Object와 상태 변경이 불가능한 불변변수 Immutable Object가 있다.
가변 변수는 값이 변해도 id가 유지되며, 불변 변수는 변경될 수 없어 새 객체를 만들어 저장한다.
Mutable Object 가변변수
list, dictionary, set, deque 등이 포함된다.
값이 변해도 id가 유지되어 값 변경 후에도 저장된 주소값이 동일하다.
Immutable Object 불변변수
int, tuple, str 등이 포함된다.
값이 변하면 새로운 메모리 공간을 할당하여 바뀐 값을 저장하기 때문에 값 변경 후에는 주소값이 변한다.
결론
answer = 0 이후 하위 중첩 함수에서 answer를 변경하려고하면 새로운 메모리 주소에 할당을 위해 지역 변수인 answer를 참조하려고 시도하고, 그런 지역 변수가 없어서 에러가 발생한다.
answer = [] 이후 하위 중첩 함수에서 answer를 변경하려고 하면 기존 주소값을 참조하기 때문에 문제가 발생하지 않는다.
참고자료
Programming FAQ
Contents: Programming FAQ- General Questions- Is there a source code level debugger with breakpoints, single-stepping, etc.?, Are there tools to help find bugs or perform static analysis?, How can ...
docs.python.org
https://docs.python.org/ko/3/reference/simple_stmts.html#the-nonlocal-statement
7. Simple statements
A simple statement is comprised within a single logical line. Several simple statements may occur on a single line separated by semicolons. The syntax for simple statements is: Expression statement...
docs.python.org
https://peps.python.org/pep-3104/
PEP 3104 – Access to Names in Outer Scopes | peps.python.org
In most languages that support nested scopes, code can refer to or rebind (assign to) any name in the nearest enclosing scope. Currently, Python code can refer to a name in any enclosing scope, but it can only rebind names in two scopes: the local scope...
peps.python.org
파이썬 중첩 함수와 자유 변수 (Python Nested Function & Free Variables)
함수형 프로그래밍을 위해 파이썬 클로저(Closure)를 공부하다 보면 함수가 중첩으로 선언되는 경우(Nested Function) 변수가 할당되고 인식되는 방식에 대한 의문이 생길 수 있습니다. 아래 코드를 먼
medium.com
https://docs.python.org/ko/3/glossary.html#term-mutable
Glossary
>>>, The default Python prompt of the interactive shell. Often seen for code examples which can be executed interactively in the interpreter.,,..., Can refer to:- The default Python prompt of the i...
docs.python.org