파이썬 클로저(Closure)
함수가 값을 기억한다는 것의 의미
파이썬을 사용하다 보면 데코레이터, 콜백, 미들웨어 같은 구조에서 클로저(Closure)라는 개념을 자주 접하게 된다. 처음에는
“함수가 값을 기억한다”
정도로 이해하고 넘어가지만, 실제로는 변수가 언제, 어떻게, 무엇을 기억하는지를 정확히 이해하지 않으면 late binding 문제나 예기치 않은 버그를 만나기 쉽다. 이 글에서는 클로저를 정의 → 동작 원리 → 자주 틀리는 문제 → 해결 방법 → 실무 연결 순서로 정리한다.
1. 클로저란 무엇인가
한 줄 정의
클로저란, 내부 함수가 자신이 생성될 당시의 외부 함수 지역 변수를 기억하고 있는 구조이다.
중요한건 값(value)이 아니라 변수(variable)이다.
2. 가장 기본적인 클로저 예제
def outer():
x = 10
def inner():
return x
return inner
f = outer()
print(f())
출력 결과: 10
왜 이렇게 되는가?
- outer() 함수는 이미 실행이 끝났다
- 일반적으로 지역 변수 x는 사라져야 한다
- 하지만 inner 함수가 x를 사용하고 있기 때문에
- 파이썬은 x를 inner 함수와 함께 묶어서 유지한다
클로저는 내부 함수가 외부 함수의 지역 변수를 참조하고, 외부 함수 실행이 끝나도 해당 변수가 유지되도록 하는 구조이다.
3. 클로저가 성립하는 조건 3가지
다음 조건을 모두 만족해야 클로저가 된다.
- 함수 내부에 또 다른 함수가 존재할 것 - def inner():
- 내부 함수가 외부 함수의 지역 변수를 사용할 것 - x
- 내부 함수가 반환되어 외부에서 호출 가능할 것 - return x
def outer(x):
def inner():
return x
return inner
4. 클로저는 값을 저장하지 않는다
클로저에서 햇갈리는 점은 다음과 같다.
“클로저는 값을 복사해서 저장한다”
클로저는 값이 아니라 변수를 참조한다.
5. late binding 문제
late binding이란 : 함수가 참조하는 변수가 “정의 시점”이 아니라 “호출 시점”에 값이 결정되는 동작 방식이다.
즉 함수를 만들 때 값이 정해지는 것이 아니라 함수를 실행할 때 현재 변수의 값이 사용된다
def outer():
funcs = []
for i in range(3):
def inner():
return i
funcs.append(inner)
return funcs
fs = outer()
print([f() for f in fs])
왜 이런 결과가 나오는가?
- i는 루프 동안 하나의 변수
- 값만 0 → 1 → 2로 변경됨
- inner 함수 3개는 모두 같은 변수 i를 참조
- 함수 호출 시점에 i == 2
즉, 클로저는 정의 시점이 아니라 호출 시점의 값을 사용한다.
이를 late binding이라고 한다.
6. late binding 문제 해결 방법
late binding 자체는 버그가 아니다. 하지만 다음 상황에서 의도와 다른 결과를 만든다.
- 반복문 안에서 함수 생성할때
- 각 함수가 서로 다른 값을 기억하길 기대할 때
해당상황들을 원하지 않을때는 다음과 같은 방법을 사용하여 late binding문제를 해결한다.
1. 기본 인자(default argument) 사용
def outer():
funcs = []
for i in range(3):
def inner(i=i): #변경점
return i
funcs.append(inner)
return funcs
print([f() for f in outer()])
출력 결과 : [0, 1, 2]
기본 인자는 함수 정의 시점에 값이 평가되기 때문에 late binding을 회피할 수 있다.
2. 클로저를 한 번 더 감싸기
def outer():
funcs = [] for i in range(3):
def make_inner(x):
def inner():
return x
return inner
funcs.append(make_inner(i))
return funcs
7. nonlocal 키워드와 클로저
클로저에서 외부 변수를 읽는 것은 가능하지만, 재할당하려면 문제가 발생한다.
def counter():
count = 0
def inc():
count += 1
return count
return inc
실행 시 에러 : UnboundLocalError
이유
- count += 1은 재할당
- 파이썬은 count를 지역 변수로 판단
- 아직 초기화되지 않은 지역 변수를 참조하게 됨
해결 방법
def counter():
count = 0
def inc():
nonlocal count #변경점
count += 1
return count
return inc
8. 클로저는 outer 호출 단위로 생성된다
def make_box():
x = []
def inner(v):
x.append(v)
return x
return inner
b1 = make_box()
b2 = make_box()
print(b1(1))
print(b1(2))
print(b2(9))
출력 결과 : [1] [1, 2] [9]
핵심 포인트
- make_box()를 호출할 때마다 새로운 x 생성
- b1과 b2는 서로 다른 클로저
- 클로저는 함수가 아니라 외부 함수 실행 단위 기준
9. lambda와 클로저
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs])
출력 결과 : [2, 2, 2]
lambda도 함수이기 때문에 동일한 클로저 규칙과 late binding 문제가 그대로 적용된다.
10. 결론
클로저는 값을 기억하지 않고 변수를 기억한다. 그리고 그 변수의 값은 호출 시점에 결정되는데 이것을 late binding이라고 한다.
해결하려면 기본인자를 사용하거나 클로저로 한번더 묶어주어야한다. 값을 읽는것이 아닌 재할당을 하고싶다면 nolocal을 사용하면된다.