프로그래밍의 본질은 '데이터를 효과적으로 관리하고 조작하는 기술'이라고 할 수 있습니다. 이 데이터는 '변수'라는 이름표를 달고 메모리 어딘가에 저장됩니다. 그런데 이 변수들은 프로그램의 아무 곳에서나 마음대로 접근할 수 있는 것이 아닙니다. 모든 변수에는 자신만의 '활동할 수 있는 영역' 또는 '집 주소'가 정해져 있는데, 이를 스코프(Scope)라고 부릅니다.
파이썬을 배우다 보면 함수 안에서 변수를 만들고 사용하는 데 익숙해집니다. 하지만 함수 안에 또 다른 함수를 만드는 '중첩 함수' 구조를 만나게 되면, 변수의 주소록은 한층 더 복잡해집니다.
바로 이 복잡한 주소 체계 속에서 "안쪽 집에서는 바깥쪽 집의 가구(변수)를 보기만 할 수 있고, 위치를 바꿀 수는 없다"는 독특한 규칙과 마주하게 됩니다. 이 규칙을 어기려고 할 때 우리는 UnboundLocalError
라는 당혹스러운 에러를 만나게 됩니다.
nonlocal
키워드는 바로 이 문제를 해결하기 위해 등장한 '마스터 키'입니다. 이 키워드는 변수의 기본 주소 규칙을 더 세밀하게 제어하여, 중첩된 함수 구조에서 발생하는 까다로운 문제들을 우아하게 풀어냅니다. nonlocal
은 단순히 에러를 피하는 기술을 넘어, '상태를 기억하는 함수(클로저)'와 같은 강력하고 세련된 프로그래밍 패턴을 구현하는 핵심적인 도구입니다.
LEGB 규칙이라는 기본 원리부터 시작하여, nonlocal
이 왜 필요한지, global
과는 무엇이 다른지, 그리고 실제 코드에서 어떻게 그 힘을 발휘하는지까지 차근차근 탐험할 것입니다. 이 여정이 끝날 때쯤, 여러분은 변수의 '주소'를 완벽하게 이해하고 nonlocal
이라는 마스터 키를 자신 있게 사용하여 더 깔끔하고 강력한 파이썬 코드를 작성하는 개발자로 거듭나게 될 것입니다.
nonlocal
의 존재 이유를 파악하려면, 먼저 파이썬이 변수 이름을 어떻게 찾고 관리하는지에 대한 근본적인 규칙을 이해해야 합니다. 이 규칙의 핵심이 바로 '스코프(Scope)'입니다.
스코프는 변수가 코드 내에서 유효하게 존재하고 접근할 수 있는 범위를 정의하는 규칙의 집합입니다. "모든 변수는 자신만의 '활동 구역'을 갖는다"고 생각하면 쉽습니다. 어떤 변수는 특정 함수 안에서만 쓸 수 있는 반면, 어떤 변수는 프로그램 전체에서 접근할 수 있습니다. 스코프는 이처럼 변수의 생명주기와 가시성(visibility)을 제어합니다.
초기 프로그래밍 언어 중 일부는 오직 전역(global) 스코프만 가지고 있었습니다. 이는 프로그램의 규모가 커질수록 서로 다른 코드 조각에서 사용된 같은 이름의 변수가 충돌하여 예상치 못한 버그를 일으키는 원인이 되었습니다. 파이썬은 이러한 혼란을 막기 위해 변수를 각자의 스코프에 격리시키는 정교한 시스템을 채택했습니다.
파이썬 인터프리터가 코드에서 특정 변수 이름을 만났을 때, 그 변수가 어떤 값을 가리키는지 찾기 위해 정해진 순서에 따라 여러 스코프를 탐색합니다. 이 순서를 LEGB 규칙이라고 부릅니다. LEGB는 다음과 같은 네 가지 스코프의 앞 글자를 딴 것입니다.
nonlocal
키워드가 활약하는 주 무대가 바로 이곳입니다..py
파일 하나)의 최상위 레벨에 정의된 변수들이 속하는 공간입니다. 함수나 클래스 바깥에 선언된 모든 변수는 전역 변수가 되며, 코드 어디서든 접근할 수 있습니다. '우리 동네'처럼 넓은 활동 범위를 가집니다.print()
, len()
, range()
와 같은 내장 함수나 SyntaxError
, NameError
같은 예외 이름들이 여기에 속합니다. 이들은 '온 세상' 어디에나 존재하는 것처럼, 별도의 선언 없이 언제나 사용할 수 있습니다.파이썬은 변수 이름을 해석할 때 항상 L → E → G → B
순서, 즉 가장 좁은 스코프에서 시작하여 점차 넓은 스코프로 확장하며 검색을 진행합니다. 검색 과정에서 해당 이름을 가진 변수를 가장 먼저 발견하는 즉시 검색을 멈추고 그 변수를 사용합니다. 만약 내장 스코프까지 모두 확인했는데도 변수를 찾지 못하면, NameError
예외가 발생합니다.
이 LEGB 규칙은 단순한 검색 순서를 넘어, '이름 충돌을 해결하고 변수의 영향 범위를 제한하는' 파이썬의 핵심적인 이름 해석(Name Resolution) 메커니즘입니다.
스코프는 변수들을 각자의 공간에 안전하게 격리시키고, LEGB 규칙은 이 격리된 공간들 사이에서 변수를 찾아 나갈지에 대한 명확한 지도를 제공합니다.
따라서 LEGB를 이해하는 것은 nonlocal
을 배우기 위한 준비운동일 뿐만 아니라, 파이썬 코드의 동작 방식을 근본적으로 이해하는 첫걸음입니다.
nonlocal
키워드가 왜 필요한지를 이해하려면, 이 키워드가 사용되는 특별한 환경인 '중첩 함수' 구조를 먼저 알아야 합니다. 이 구조에서 바로 LEGB 규칙의 'E', 즉 Enclosing 스코프가 탄생하기 때문입니다.
이름 그대로, 하나의 함수 내부에 또 다른 함수를 정의하는 프로그래밍 구조를 말합니다. 바깥쪽 함수를 외부 함수(outer function), 안쪽 함수를 내부 함수(inner function)라고 부릅니다.
def outer():
print("여기는 외부 함수입니다.")
def inner():
print("여기는 내부 함수입니다.")
inner() # 외부 함수 안에서 내부 함수를 호출
outer()
위 코드에서 inner
함수는 outer
함수 안에서만 정의되고 호출될 수 있습니다. outer
함수 바깥에서 inner()
를 직접 호출하려고 하면 NameError
가 발생합니다. 이는 inner
함수 자체가 outer
함수의 지역 스코프(Local Scope)에 속하기 때문입니다.
이러한 중첩 구조는 특정 기능을 외부로부터 숨겨서 오직 특정 함수 내에서만 사용되는 '도우미 함수(Helper Function)'를 만들 때 유용합니다. 전역 스코프를 불필요한 함수들로 어지럽히지 않고, 코드의 논리적 구조를 더 명확하게 만들어주는 장점이 있습니다.
중첩 함수 구조가 만들어지는 순간, 스코프에도 새로운 층이 생겨납니다. 외부 함수(outer
)가 호출되면 그 안의 변수들을 위한 지역 스코프가 생성됩니다. 그런데 이 스코프는 그 안에 있는 내부 함수(inner
)의 입장에서는 자신을 둘러싸고 있는 Enclosing 스코프가 됩니다.
내부 함수는 자신을 감싸는 외부 함수의 변수를 자유롭게 '읽을(read)' 수 있습니다. 이는 LEGB 규칙에 따른 자연스러운 동작입니다. 다음 예제를 살펴보겠습니다.
def outer():
message = "Hello from outer" # Enclosing 스코프의 변수
def inner():
# inner의 지역(Local) 스코프에는 message가 없음
# 따라서 한 단계 위인 Enclosing 스코프에서 message를 찾아 사용
print(message)
inner()
outer()
실행 결과:
Hello from outer
이 코드가 문제없이 실행되는 이유는 LEGB 규칙 때문입니다. inner
함수가 print(message)
를 실행할 때, 파이썬은 먼저 inner
함수의 지역(Local) 스코프에서 message
변수를 찾습니다. 하지만 없으므로, 다음 단계인 Enclosing 스코프로 이동합니다. outer
함수의 스코프에 message
변수가 존재하므로, 그 값을 가져와 성공적으로 출력합니다.
이처럼 내부 함수가 외부 함수의 변수를 참조(읽기)하는 것은 매우 직관적이고 간단합니다. 하지만 문제가 발생하는 지점은 바로 이 변수의 값을 '수정(modify)'하려고 할 때입니다.
UnboundLocalError
의 함정외부 함수의 변수를 읽는 것은 자유롭지만, 그 값을 바꾸려고 시도하는 순간 파이썬은 예상치 못한 에러를 발생시킵니다. 이 현상은 많은 초보 개발자들을 혼란에 빠뜨리는 주범이며, nonlocal
이 필요한 이유를 가장 극적으로 보여주는 사례입니다.
2부의 예제를 약간 수정하여, 내부 함수에서 외부 함수의 변수 값을 변경해 보겠습니다. 방문 횟수를 세는 간단한 카운터 함수를 가정해 봅시다.
def outer():
count = 0 # 방문 횟수를 기록할 변수
def inner():
# 외부 함수의 count 값을 1 증가시키려고 시도
count += 1 # 여기서 문제가 발생한다!
print(f"방문 횟수: {count}")
inner()
outer()
이 코드를 실행하면 방문 횟수: 1
이 출력될 것이라고 기대하기 쉽습니다. 하지만 실제 결과는 다음과 같은 에러 메시지입니다.
에러 발생:
UnboundLocalError: local variable 'count' referenced before assignment
"지역 변수 'count'가 할당되기 전에 참조되었습니다"라는 이 에러 메시지는 언뜻 보기에 이상합니다. count
는 분명히 outer
함수에 0
으로 할당되어 있는데 왜 이런 에러가 발생하는 것일까요?
이 에러의 근본적인 원인은 파이썬이 변수의 스코프를 결정하는 방식에 있습니다.
=
, +=
, =
, =
, /=
)을 사용하여 값을 할당받는 변수는 무조건 그 함수의 '지역 변수(Local Variable)'로 간주합니다. 이는 코드가 실제로 실행되기 전, 함수 정의를 읽어들이는 시점에 결정됩니다.count += 1
의 함정: inner
함수 안에 count += 1
이라는 코드가 있습니다. 이 코드는 count = count + 1
과 동일한 의미를 가지는 명백한 할당문입니다. 파이썬 인터프리터는 inner
함수를 훑어보다가 이 할당문을 발견하고, "아하, count
는 inner
함수의 지역 변수구나!"라고 결론 내립니다.count
가 inner
의 지역 변수로 '낙인'찍히고 나면, 파이썬은 inner
함수 안에서 count
라는 이름을 만날 때 더 이상 바깥쪽 스코프(Enclosing 또는 Global)를 쳐다보지 않습니다. 오직 inner
함수 내부에서만 count
를 찾으려고 합니다.count = count + 1
의 오른쪽 부분, 즉 count
의 현재 값을 읽어오려는 시점에 문제가 발생합니다. 파이썬은 inner
함수의 지역 스코프에서 count
의 값을 찾지만, 아직 아무런 값도 할당된 적이 없습니다. 따라서 "값이 할당되기도 전에 참조하려고 했다"는 UnboundLocalError
가 발생하는 것입니다.결론적으로, 이 에러는 '외부 변수 수정 시도의 실패'가 아니라, '지역 변수로 잘못 간주된 변수를 초기값 없이 사용하려는 시도'에서 비롯된 것입니다. count += 1
이라는 코드 한 줄 때문에 outer
함수에 있는 count
는 inner
함수에게 완전히 보이지 않는 존재가 되어버린 셈입니다. 이 미묘하지만 결정적인 차이를 이해하는 것이 nonlocal
의 필요성을 깨닫는 열쇠입니다.
nonlocal
키워드앞서 살펴본 UnboundLocalError
는 파이썬의 스코프 규칙 때문에 발생하는, 피할 수 없는 함정처럼 보입니다. 하지만 파이썬은 이 문제를 해결할 명쾌한 방법을 제공합니다. 바로 nonlocal
키워드입니다.
nonlocal
의 등장nonlocal
은 파이썬 인터프리터에게 보내는 명시적인 선언입니다. "지금부터 내가 사용하려는 이 변수는 내 방(Local 스코프)에 새로 만드는 것이 아니다. 우리 집 거실(Enclosing 스코프)에 있는 바로 그 변수를 가져다 쓰겠다"고 알려주는 역할을 합니다. 이 선언을 통해 파이썬이 변수를 지역 변수로 오인하는 것을 막고, 의도대로 외부 함수의 변수에 접근하여 수정할 수 있게 됩니다.
3부에서 에러를 일으켰던 코드를 nonlocal
을 사용하여 수정한 버전과 나란히 비교해 보면 그 차이가 극명하게 드러납니다.
# Before (Error 발생)
def outer_before():
count = 0
def inner():
# 파이썬은 이 할당문 때문에 count를 inner의 지역 변수로 착각
count += 1
print(f"방문 횟수: {count}")
inner()
# After (성공!)
def outer_after():
count = 0
def inner():
nonlocal count # "count는 지역 변수가 아니야!" 라고 선언
count += 1
print(f"방문 횟수: {count}")
inner()
# outer_before() # UnboundLocalError 발생
outer_after() # 정상 실행
outer_after()
실행 결과:
방문 횟수: 1
nonlocal
의 역할outer_after
함수에서 inner
함수의 시작 부분에 추가된 nonlocal count
한 줄이 모든 것을 바꿉니다. 이 선언은 파이썬 인터프리터에게 다음과 같이 지시합니다 :
"이 inner 함수 안에서 count라는 이름에 값을 할당하더라도, 새로운 지역 변수를 만들지 마라. 대신, 이 함수를 둘러싸고 있는 가장 가까운 외부 함수(Enclosing Scope)에서 count라는 이름의 변수를 찾아 그것을 사용해라."
이 지시 덕분에, 파이썬은 count += 1
을 보더라도 count
를 지역 변수로 간주하지 않습니다. 대신 LEGB 규칙의 'E' 단계로 바로 넘어가 outer_after
함수에 있는 count
변수를 찾아내고, 그 변수의 값을 성공적으로 1 증가시킵니다.
이처럼 nonlocal
은 파이썬의 기본 스코프 결정 규칙(함수 내 할당문 = 지역 변수)을 개발자가 의도적으로 재정의할 수 있게 해주는 '명시적 선언'입니다. 이는 "암묵적인 것보다 명시적인 것이 낫다(Explicit is better than implicit)"는 파이썬의 핵심 철학(Zen of Python)과도 맞닿아 있습니다. nonlocal
키워드를 사용함으로써, 개발자는 "나는 지금 의도적으로 바깥 스코프의 변수를 수정하고 있다"는 명확한 '깃발'을 코드에 꽂는 셈입니다. 이는 코드를 읽는 다른 사람(혹은 미래의 나 자신)에게 변수의 흐름과 의도를 명확하게 전달하는 중요한 역할을 합니다.
nonlocal
vs. global
nonlocal
을 배우는 과정에서 많은 이들이 global
키워드와 혼동을 겪습니다. 두 키워드 모두 함수 내부에서 바깥 스코프의 변수를 수정할 때 사용된다는 공통점이 있지만, 그들이 가리키는 '바깥'의 정의는 완전히 다릅니다. 이 둘의 차이점을 명확히 이해하는 것은 스코프를 제대로 다루기 위해 필수적입니다.
global
: 전역 스코프global
키워드는 이름 그대로 오직 전역(Global) 스코프, 즉 모듈 최상위 레벨에 있는 변수만을 가리킵니다. 함수가 아무리 깊게 중첩되어 있더라도
global
선언은 모든 중간 스코프(Enclosing)를 무시하고 곧장 맨 꼭대기 층으로 올라가 변수를 찾습니다.
nonlocal
: 가장 가까운 스코프반면, nonlocal
키워드는 바로 한 단계 위 스코프부터 시작하여 자신을 감싸고 있는 가장 가까운 Enclosing 스코프의 변수만을 찾습니다. 만약 가장 가까운 Enclosing 스코프에 해당 변수가 없다면 그보다 더 바깥쪽의 Enclosing 스코프를 순차적으로 탐색합니다. 중요한 점은,
nonlocal
은 아무리 탐색해도 전역 스코프의 변수는 절대 건드리지 않는다는 것입니다.
세 개의 다른 스코프(Global, Enclosing, Local)에 모두 같은 이름의 변수 x
가 있을 때, nonlocal
과 global
이 각각 어떤 변수를 수정하는지 살펴보면 그 차이가 명확해집니다.
x = "나는 전역(Global) 변수"
def outer():
x = "나는 둘러싸는(Enclosing) 변수"
def inner_nonlocal():
nonlocal x # Enclosing 스코프의 x를 가리킴
x = "nonlocal에 의해 수정됨"
print(f" [inner_nonlocal] x = {x}")
def inner_global():
global x # Global 스코프의 x를 가리킴
x = "global에 의해 수정됨"
print(f" [inner_global] x = {x}")
print(f"[outer] 시작 시 x = {x}")
inner_nonlocal()
print(f"[outer] nonlocal 호출 후 x = {x}") # Enclosing 변수가 바뀜
inner_global()
print(f"[outer] global 호출 후 x = {x}") # Enclosing 변수는 영향 없음
print(f"[전역] 시작 시 x = {x}")
outer()
print(f"[전역] 모든 호출 후 x = {x}") # Global 변수가 바뀜
실행 결과:
[전역] 시작 시 x = 나는 전역(Global) 변수 [outer] 시작 시 x = 나는 둘러싸는(Enclosing) 변수 [inner_nonlocal] x = nonlocal에 의해 수정됨 [outer] nonlocal 호출 후 x = nonlocal에 의해 수정됨 [inner_global] x = global에 의해 수정됨 [outer] global 호출 후 x = nonlocal에 의해 수정됨 [전역] 모든 호출 후 x = global에 의해 수정됨
결과에서 볼 수 있듯이, inner_nonlocal
은 outer
함수의 x
값을 변경했고, inner_global
은 모듈 최상단의 전역 x
값을 변경했습니다. outer
함수 내의 x
는 inner_global
호출에 전혀 영향을 받지 않았습니다.
nonlocal
로 지정된 변수는 반드시 Enclosing 스코프에 미리 존재해야 합니다. 만약 존재하지 않는 변수를 nonlocal
로 선언하면 SyntaxError
가 발생합니다. 반면, global
은 전역 스코프에 해당 변수가 없더라도 에러를 발생시키지 않으며, 그 함수 내에서 값을 할당하는 순간 새로운 전역 변수를 생성합니다.
nonlocal
vs. global
심층 비교두 키워드의 핵심적인 차이점을 다음 표로 정리할 수 있습니다.
특징 | nonlocal | global |
---|---|---|
목표 스코프 | 가장 가까운 Enclosing(둘러싸는) 스코프 | 최상위 Global(전역) 스코프 |
검색 방식 | 안쪽에서 바깥쪽으로 가장 가까운 것 하나만 찾음 | 스코프 계층을 무시하고 바로 전역으로 이동 |
변수 사전 존재 | 반드시 Enclosing 스코프에 존재해야 함 | 존재하지 않으면 새로 생성 가능 |
사용 가능 위치 | 중첩 함수 내부에서만 의미 있음 | 모든 함수 내부에서 사용 가능 |
주요 사용처 | 클로저(Closure), 팩토리 함수 등 상태 관리 | 모듈 전체에서 공유되는 설정 값, 상태 변수 |
이처럼 nonlocal
과 global
은 비슷해 보이지만, 변수를 찾는 범위와 규칙에서 근본적인 차이를 가집니다. nonlocal
은 중첩된 구조 내에서의 '지역적' 상태 변경을, global
은 프로그램 전체의 '전역적' 상태 변경을 담당한다고 이해하면 명확합니다.
nonlocal
의 실전 활용 사례지금까지 nonlocal
을 UnboundLocalError
를 해결하기 위한 문법적 도구로 살펴보았다면, 이제부터는 nonlocal
이 어떻게 더 높은 차원의 프로그래밍 패턴을 구현하는 강력한 도구가 되는지 알아보겠습니다. nonlocal
의 진정한 힘은 '상태를 기억하고 성장하는 함수'를 만들 때 드러납니다.
클로저란 무엇인가?
클로저는 "자신이 정의될 때의 환경(스코프)을 기억하는 함수"입니다. 조금 더 풀어서 설명하면, 어떤 함수가 자신의 본문 밖에서 정의된, 그러나 전역 변수는 아닌 변수를 참조할 때, 그 함수와 변수가 함께 묶여 하나의 단위처럼 동작하는 것을 말합니다. 외부 함수는 이미 실행이 끝나 사라졌지만, 그 외부 함수가 가지고 있던 변수(자유 변수, free variable)들은 내부 함수에 의해 계속 '붙잡혀서' 살아있는 상태가 됩니다. 마치 '레시피(함수 코드)와 특별한 재료(환경 변수)를 함께 담아둔 밀키트'와 같습니다.
대표 예제: 카운터 함수nonlocal
을 사용한 카운터 함수는 클로저의 가장 대표적인 예제입니다.
def counter():
count = 0 # Enclosing 스코프의 변수. 이 값이 기억될 것임.
def increment():
nonlocal count # 'count'는 지역 변수가 아님을 선언
count += 1
return count
return increment # 내부 함수 자체를 반환
# 카운터 인스턴스 생성
counter1 = counter()
counter2 = counter()
print(f"counter1 첫 번째 호출: {counter1()}") # 출력: 1
print(f"counter1 두 번째 호출: {counter1()}") # 출력: 2
print(f"counter1 세 번째 호출: {counter1()}") # 출력: 3
print(f"counter2 첫 번째 호출: {counter2()}") # 출력: 1
print(f"counter2 두 번째 호출: {counter2()}") # 출력: 2
이 코드는 다음과 같은 단계로 동작합니다.
counter1 = counter()
가 호출되면, counter
함수가 실행됩니다. count
변수가 0
으로 초기화되고, increment
함수가 정의됩니다.counter
함수는 increment
함수 객체를 반환하고 자신의 생을 마감합니다.increment
함수는 자신이 만들어진 환경, 즉 count = 0
이라는 변수와의 연결을 '기억'한 채로 counter1
변수에 저장됩니다. count
변수는 사라지지 않고 counter1
에 의해 붙잡혀 살아남습니다.counter1()
이 호출될 때마다, increment
함수가 실행됩니다. nonlocal count
선언 덕분에, 이 함수는 새로 count
를 만드는 대신 '살아남은' 바로 그 count
변수에 접근하여 값을 1씩 증가시킵니다.counter2 = counter()
를 통해 새로운 카운터를 만들면, counter2
는 counter1
과는 완전히 독립적인 자신만의 count
변수를 가진 클로저가 됩니다. 따라서 두 카운터는 서로의 상태에 영향을 주지 않습니다.nonlocal
이 없었다면, 클로저는 자신의 환경을 '기억'은 할 수 있어도 '변경'할 수는 없었을 것입니다. nonlocal
은 클로저가 단순한 기억을 넘어 '성장(상태 변경)'할 수 있게 만드는 핵심 동력입니다.
함수 팩토리는 특정 설정값을 받아 그에 맞는 '맞춤형 함수'를 동적으로 생성하여 반환하는 함수입니다. 클로저는 이러한 함수 팩토리를 구현하는 자연스러운 방법입니다.
예제: 메시지 생성기
다양한 종류의 로그 메시지를 출력하는 함수를 만드는 팩토리 함수 예제입니다.
def message_factory(prefix):
# 'prefix'는 반환될 'logger' 함수의 Enclosing 스코프에 저장됨
def logger(content):
# nonlocal이 필요 없는 이유: prefix를 '읽기만' 하기 때문
print(f"[{prefix}] {content}")
return logger
log_error = message_factory("ERROR")
log_info = message_factory("INFO")
log_error("파일을 찾을 수 없습니다.")
log_info("데이터 처리 중입니다.")
실행 결과:
파일을 찾을 수 없습니다. [INFO] 데이터 처리 중입니다.
log_error
와 log_info
는 각각 "ERROR"
와 "INFO"
라는 prefix
값을 기억하는 서로 다른 클로저입니다.
예제: 비밀번호 검증기 (상태 저장)nonlocal
을 사용하면 상태를 저장하고 변경하는, 한층 더 발전된 팩토리 함수를 만들 수 있습니다. 예를 들어, 비밀번호 시도 횟수를 제한하는 검증기를 만들어 보겠습니다.
def create_validator(correct_password, max_attempts=3):
attempts_left = max_attempts
def validate(password):
nonlocal attempts_left
if attempts_left <= 0:
print("계정이 잠겼습니다. 더 이상 시도할 수 없습니다.")
return False
if password == correct_password:
print("인증 성공!")
return True
else:
attempts_left -= 1
print(f"비밀번호가 틀렸습니다. 남은 시도 횟수: {attempts_left}")
return False
return validate
validator_A = create_validator("pa$$w0rd")
validator_A("wrong_pass")
validator_A("another_wrong")
validator_A("pa$$w0rd")
validator_A("too_late")
실행 결과:
비밀번호가 틀렸습니다. 남은 시도 횟수: 2 비밀번호가 틀렸습니다. 남은 시도 횟수: 1 인증 성공! 비밀번호가 틀렸습니다. 남은 시도 횟수: 0
이 예제에서 validator_A
는 attempts_left
라는 상태를 내부적으로 유지하며, 호출될 때마다 nonlocal
을 통해 이 상태를 변경합니다. 이처럼 nonlocal
과 클로저를 활용하면, 객체 지향 프로그래밍의 클래스(Class)가 제공하는 '상태 캡슐화'를 함수형 프로그래밍 스타일로도 우아하게 구현할 수 있습니다.
nonlocal
은 강력한 도구이지만, 잘못 사용하면 에러를 발생시키거나 코드의 의도를 흐릴 수 있습니다. nonlocal
사용 시 발생할 수 있는 일반적인 실수와 함정을 미리 알아두고, 언제 사용하는 것이 좋은지에 대한 가이드라인을 따르는 것이 중요합니다.
nonlocal
사용하기nonlocal
은 자신을 둘러싼 Enclosing 스코프의 변수만을 찾습니다. 전역 스코프는 Enclosing 스코프가 아니므로, 전역 변수를 nonlocal
로 참조하려고 하면 에러가 발생합니다.
# 잘못된 사용 예
x = 100
def my_func():
# SyntaxError: no binding for nonlocal 'x' found
# 'x'를 둘러싼 Enclosing 스코프가 없기 때문
nonlocal x
x = 200
전역 변수를 수정하고 싶을 때는 global
키워드를 사용해야 합니다.
nonlocal
사용하기nonlocal
은 Enclosing 스코프가 존재하는, 즉 중첩된 함수 구조 안에서만 의미가 있습니다. 일반 함수나 모듈의 최상위 레벨에서 nonlocal
을 사용하면 SyntaxError
가 발생합니다.
# 잘못된 사용 예
def my_func():
# SyntaxError: nonlocal declaration not allowed at module level
# (Jupyter Notebook 등에서는 다른 에러 메시지가 나올 수 있음)
# 이 함수는 중첩 함수가 아니므로 Enclosing 스코프가 없음
nonlocal y
y = 10
nonlocal
이 필요 없다?리스트(list), 딕셔너리(dict), 세트(set)와 같은 변경 가능한(mutable) 객체의 경우, nonlocal
선언 없이도 내부 함수에서 그 객체의 '내용물'을 수정할 수 있습니다. 이는 혼동을 일으키기 쉬운 지점입니다.
def outer():
my_list =
def inner_append():
# my_list 변수 자체를 바꾸는 것이 아니라,
# 그 변수가 가리키는 리스트 객체의 내용을 수정하는 것
my_list.append(4)
def inner_reassign():
# 여기서 UnboundLocalError 발생!
# my_list라는 변수 자체에 새로운 리스트를 할당하려 하기 때문
my_list =
def inner_reassign_fixed():
nonlocal my_list
# nonlocal 선언이 있으면 변수 자체를 재할당할 수 있음
my_list =
inner_append()
print(f"append 후: {my_list}") # 출력:
inner_reassign_fixed()
print(f"재할당 후: {my_list}") # 출력:
핵심은 객체의 내용을 변경하는 것(mutation)과 변수에 다른 객체를 재할당하는 것(rebinding)의 차이를 이해하는 것입니다. .append()
, .pop()
, my_dict['key'] = value
와 같은 연산은 변수가 가리키는 객체는 그대로 둔 채 내용만 바꾸므로 nonlocal
이 필요 없습니다. 하지만 my_list = [...]
와 같이 변수 자체가 다른 객체를 가리키도록 하려면 반드시 nonlocal
선언이 필요합니다.
nonlocal
은 코드의 흐름을 조금 더 복잡하게 만들 수 있습니다. 단순히 값을 전달하는 것이 목적이라면, 함수 인자(argument)로 넘겨주고 반환값(return)을 받는 것이 더 명확하고 직관적인 방법일 수 있습니다.nonlocal
을 사용하여 여러 변수의 상태를 복잡하게 관리해야 한다면, 이는 클래스(Class)를 사용해야 할 신호일 수 있습니다. 클래스는 상태(속성, attribute)와 그 상태를 조작하는 행위(메서드, method)를 하나의 단위로 묶어주므로, 복잡한 상태 관리에 더 구조적이고 명확한 해결책을 제공합니다. nonlocal
과 클로저는 가벼운 상태 캡슐화에 적합하고, 클래스는 더 무겁고 체계적인 상태 관리에 적합합니다.지금까지 우리는 파이썬의 nonlocal
키워드를 중심으로 변수가 살아가는 세상, 즉 스코프의 복잡하고 정교한 규칙들을 탐험했습니다. 이 여정을 통해 우리는 다음과 같은 핵심적인 사실들을 확인했습니다.
nonlocal
은 중첩된 무대 사이를 연결하는 특별한 '통로'입니다. 기본적으로 내부 함수는 외부 함수의 변수를 수정할 수 없지만, nonlocal
은 이 장벽을 넘어 가장 가까운 외부 스코프의 변수에 대한 수정 권한을 명시적으로 부여합니다.nonlocal
은 단순한 에러 해결사가 아닙니다. UnboundLocalError
를 해결하는 열쇠일 뿐만 아니라, 클로저와 팩토리 함수 같은 강력한 프로그래밍 패턴을 통해 '상태를 기억하고 성장하는 함수'를 만드는 창의적인 도구입니다. 이를 통해 함수만으로도 객체와 유사한 캡슐화를 구현할 수 있습니다.nonlocal
과 global
은 목표가 다릅니다. nonlocal
이 중첩된 함수 구조 내의 '가까운' 상태를 다루는 데 집중한다면, global
은 프로그램 전체에 걸친 '먼' 상태를 다룹니다. 둘의 차이를 명확히 아는 것은 코드의 의도를 정확하게 표현하는 데 필수적입니다.