Python nonlocal은 언제 필요한가: 중첩 함수의 변수 재바인딩

반응형

Python의 nonlocal은 중첩 함수 안에서 가장 가까운 바깥 함수 스코프의 변수를 재바인딩할 때 쓴다. 단순히 값을 읽는 데는 필요하지 않다. count += 1처럼 기존 값을 읽은 뒤 새 값을 대입할 때 필요하다.

핵심은 “바깥 변수에 접근한다”보다 이름이 어느 스코프의 객체를 가리키게 할 것인가다.

읽기와 재바인딩을 구분한다

중첩 함수는 바깥 함수의 지역 변수를 읽을 수 있다.

def outer():
    message = "hello"

    def read_message():
        return message

    return read_message()

read_messagemessage를 대입하지 않으므로 Python이 바깥 스코프에서 이름을 찾는다. 이때는 nonlocal이 필요 없다.

반면 다음 코드는 오류가 난다.

def outer():
    count = 0

    def increase():
        count += 1
        return count

    return increase()

함수 안에 count += 1이라는 대입이 있으므로 Python은 기본적으로 countincrease의 지역 변수로 판단한다. 하지만 오른쪽에서 아직 값이 없는 지역 변수 count를 먼저 읽으려 하므로 UnboundLocalError가 발생한다.

nonlocal로 가장 가까운 바깥 변수를 선택한다

def make_counter():
    count = 0

    def increase():
        nonlocal count
        count += 1
        return count

    return increase


counter = make_counter()
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3

nonlocal countincrease 바깥의 가장 가까운 함수 스코프에서 이미 바인딩된 count를 사용하겠다는 선언이다. make_counter 호출이 끝난 뒤에도 반환된 함수가 그 환경을 기억하므로 값이 계속 유지된다. 이것이 클로저를 이용한 간단한 상태 관리다.

nonlocal로 선언할 이름은 바깥의 비전역 스코프에 이미 존재해야 한다.

def outer():
    def inner():
        nonlocal missing

이처럼 바깥 함수 어디에도 missing이 바인딩돼 있지 않으면 코드를 실행하기 전 구문 분석 단계에서 SyntaxError가 난다. nonlocal은 새로운 바깥 변수를 만드는 문법이 아니다.

객체를 바꾸는 것과 이름을 바꾸는 것은 다르다

가변 객체의 내부를 수정할 때는 이름 자체를 재바인딩하지 않으므로 nonlocal이 없어도 된다.

def make_collector():
    items = []

    def add(item):
        items.append(item)
        return list(items)

    return add

items.append(item)items가 가리키는 리스트를 변경한다. 반면 새 리스트를 대입하면 이름을 재바인딩하므로 nonlocal이 필요하다.

def make_collector():
    items = []

    def reset():
        nonlocal items
        items = []

딕셔너리의 state["count"] += 1도 딕셔너리 내부 값을 바꾸는 것이어서 state 이름을 재바인딩하지 않는다. 그렇다고 가변 객체로 nonlocal을 무조건 피하는 것이 더 좋은 설계라는 뜻은 아니다. 무엇이 변경되는지 읽기 쉬운 쪽을 고른다.

global과 nonlocal의 차이

선언 대상으로 삼는 스코프 대표 용도
선언 없음 현재 함수의 지역 스코프에 대입 함수 내부 임시 상태
nonlocal 가장 가까운 바깥 함수 스코프 클로저가 공유하는 상태 재바인딩
global 모듈 전역 스코프 모듈 수준 이름 재바인딩

전역 변수는 여러 호출과 테스트가 같은 상태를 공유하게 만들어 흐름을 추적하기 어려워질 수 있다. 작은 카운터나 한정된 설정을 캡슐화할 때는 nonlocal 클로저가 더 적절할 수 있다.

다만 상태가 여러 개로 늘거나 제공해야 할 동작이 많아지면 클래스나 dataclass가 의도를 더 잘 드러낸다. 여러 실행 흐름이 동시에 같은 클로저를 호출한다면 동시성 제어도 별도로 고려해야 한다. nonlocal이 상태를 자동으로 안전하게 보호해 주는 것은 아니다.

정리하면 다음 세 질문으로 판단할 수 있다.

  1. 바깥 변수의 값을 읽기만 하는가? 선언이 필요 없다.
  2. 바깥 함수의 이름을 새 객체로 재바인딩하는가? nonlocal이 필요하다.
  3. 모듈 전역 이름을 재바인딩하는가? 그때만 global을 검토한다.

클로저의 기본 구조는 파이썬의 일급 객체와 클로저에서, 클로저로 상태를 감추는 예시는 파이썬 클로저로 이해하는 데이터 은닉에서 이어서 볼 수 있다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글