스코프와 클로저(Closure)

박태정·2026년 5월 17일

Python Deep Dive

목록 보기
8/9

저번 편에서는 파이썬 함수가 일급 객체라는 것을 정리했다.

오늘(5월 2일)은 그 연장선상에서 스코프 문제를 먼저 짚고, 이를 해결하는 핵심 개념인 클로저까지 공부했다.


스코프(Scope)

클로저를 이해하려면 스코프를 먼저 제대로 알아야 한다.

학부때 언어를 배울 때면 항상 지역 변수, 전역 변수를 배우면서 스코프의 중요성을 강조했었던 기억이난다.

# b가 어디에도 없으니 당연히 에러
def func_v1(a):
    print(a)
    print(b)  # NameError

# E전역변수 b가 있으면 정상 동작
b = 20
def func_v2(a):
    print(a)
    print(b)  # 20 정상 출력

여기까지는 직관적으로 이해가 되는 부분이다. 근데 아래 케이스가 살짝 함정이다.

c = 30

def func_v3(a):
    print(a)
    print(c)  # UnboundLocalError 발생!
    c = 40

c는 전역변수로 선언되어 있는데 왜 에러가 나지? 파이썬은 함수를 실행하기 전에 코드를 한 번 전체적으로 훑는다. 이때 함수 내부에 c = 40이 있는 걸 보고 c지역변수로 등록해버린다. 그래서 print(c) 시점에는 지역변수 c가 아직 정의되지 않은 상태가 되어버리는 것이다.

  • 이 부분에서 헷갈렸다. 너무 단순하게 인터프리터 언어는 코드를 위에서 아래로 실행한다는 생각을 해서 함수 전체를 훑는다는 생각을 못했다.

코딩테스트 풀다가 이 에러를 정말 많이 만났었는데, 원인을 제대로 몰라서 그냥 변수명 바꾸거나 전역변수 위치를 옮기는 식으로 때웠던 기억이 난다. 이제야 이유를 알았다.

해결 방법은 global 키워드를 써서 전역변수임을 명시해주는 것이다.

def func_v3_1(a):
    global c
    print(a)
    print(c)
    c = 40

클로저(Closure) 사용 이유

클로저를 왜 쓰는지부터 먼저 정리했다.

  • 스코프가 닫혀도 값을 기억한다: 함수가 종료되면 내부 변수는 사라져야 하는데, 클로저는 그 값을 기억하고 있다. -> 이게 포인트

  • 동시성(Concurrency) 제어에 유리: 파이썬에서는 메모리를 공유하지 않고 메시지 전달 방식으로 처리한다. 클로저는 이 방식과 잘 맞는다. -> 06챕터에서 자세히 할거 같다.

  • 불변 상태 유지: 클로저는 공유하되 변경되지 않는 불변 상태를 적극적으로 활용한다. 함수형 프로그래밍의 핵심이다.

한마디로 정리하면 클로저는 상태를 기억한다. 그것도 불변 상태를.

이것만 읽고 이해안되는게 너무 당연하다. 일반적인 구조가 아니다.


Class의 __call__로 클로저 개념 먼저 이해하기

클로저를 바로 구현하기 전에, 클래스의 __call__ 을 이용해서 개념을 먼저 잡았다.

class Averager():
    def __init__(self):
        self.series = []

    def __call__(self, v):
        self.series.append(v)
        print(f'inner >> {self.series} / {len(self.series)}')
        return sum(self.series) / len(self.series)

averager_cls = Averager()

print(averager_cls(10))   # 10.0
print(averager_cls(30))   # 20.0
print(averager_cls(50))   # 30.0

__call__을 정의하면 인스턴스를 averager_cls(10)처럼 함수처럼 호출할 수 있다. 중요한 건 호출할 때마다 self.series에 값이 누적된다는 것이다. 이게 바로 클로저가 하려는 것과 완전히 같은 동작이다. 클로저를 클래스로 먼저 표현해본 것이라고 보면 된다.

나는 이걸 클래스로 구현하면서 클로저를 왜 쓰지? 라는 생각이 들었다. 솔직히 아직도 의문이다. 근데 아래와 같은 이유가 있다고 한다.

  • 클래스보다 가볍다.
  • 특정 개념의 밑바당이 된다.

진짜 클로저 구현

이제 클래스 없이 함수만으로 같은 동작을 구현해본다.

def closure_ex1():
    series = []  # 자유변수 (Free Variable)

    def averager(v):
        series.append(v)
        print(f'inner >>> {series} / {len(series)}')
        return sum(series) / len(series)

    return averager  # 함수 자체를 반환 (실행 X)

avg_closure1 = closure_ex1()
# closure_ex1()의 반환값 averager 함수가 반환되고 series가 선언된거다.

print(avg_closure1(10))  # 10.0
print(avg_closure1(30))  # 20.0  <- series = [10, 30] 을 기억하고 있음!

처음 이 코드를 봤을 때 "어떻게 series를 기억하지?"라는 의문이 들었다. 이유는 avg_closure1 = closure_ex1()을 할 때 함수 자체를 반환받았기 때문이다. closure_ex1()이 실행되면서 내부의 seriesaverager 함수가 묶인 채로 반환된다. 이 묶음이 바로 클로저다.

클로저 내부 검사

print(avg_closure1.__code__.co_freevars)
# ('series',) -> 자유변수 확인

print(avg_closure1.__closure__[0].cell_contents)
# [10, 30] -> 현재 저장된 값 확인

__code__.co_freevars로 어떤 변수가 자유변수인지 확인할 수 있고, __closure__로 그 변수에 실제로 저장된 값을 직접 들여다볼 수 있다. 클로저가 series를 기억하고 있다는 걸 코드로 증명하는 방법이다.


잘못된 클로저와 nonlocal

클로저를 쓰다 보면 또 스코프 문제가 발생한다.

def closure_ex2():
    cnt = 0
    total = 0

    def averager(v):
        cnt += 1    # UnboundLocalError 발생!
        total += v
        return total / cnt

    return averager

cnt += 1cnt = cnt + 1과 같다. 즉, 재할당이 발생한다. 앞서 배운 스코프 문제와 똑같다. 파이썬 인터프리터가 cnt를 지역변수로 간주해버리는 것이다.

해결책은 nonlocal 키워드다.

def closure_ex3():
    cnt = 0
    total = 0

    def averager(v):
        nonlocal cnt, total  # "나 지역변수 아니야"라고 선언
        cnt += 1
        total += v
        return total / cnt

    return averager

avg_closure3 = closure_ex3()
print(avg_closure3(10))  # 10.0
print(avg_closure3(30))  # 20.0

nonlocal은 "이 변수는 내 바깥 함수에서 온 거야"라고 인터프리터에게 알려주는 것이다. global이 전역변수를 명시하는 거라면, nonlocal바로 위 외부 함수의 변수를 명시하는 것이다.

  • nonlocal은 내가 프로그래머스같이 함수로 코테 문제가 나오는 곳에서 문제를 풀 때 dfs 같은 함수를 정의할 때 많이 사용한다.

오늘 공부에서 가장 인상 깊었던 건 UnboundLocalError의 원인을 드디어 제대로 이해했다는 거다. 코딩테스트에서 자주 걸리던 에러였는데 그냥 감으로 피하고 있었던 것이었다.

클로저 자체도 처음에는 "그냥 클래스 쓰면 되는 거 아닌가?"라는 생각이 들었는데, 클래스보다 훨씬 가볍게 상태를 유지할 수 있고 더 나아가서 함수형 프로그래밍(뭔지 아직 모른다.) 스타일과도 잘 맞는다는 점에서 충분히 쓸 이유가 있다는 걸 알았다.

다음에는 이 클로저를 기반으로 만들어지는 데코레이터로 넘어간다. 위에서 말했던 클래스가 아닌 클로저가 있어야하는 이유다.

0개의 댓글