클로저: 함수가 환경을 기억하는 방법

Python 클로저의 정의, 자유 변수가 캡처되는 원리, LEGB 스코프 규칙, nonlocal 키워드로 외부 변수를 수정하는 방법, 팩토리 패턴 활용을 설명합니다.

· 4 min read · PALDYN Team

지난 글에서 람다 함수를 다뤘다. 이번에는 함수가 자신이 정의된 환경을 기억하는 클로저를 살펴본다.

클로저란

클로저는 외부 함수의 변수를 기억하는 내부 함수다. 외부 함수가 종료된 후에도 내부 함수는 그 변수를 사용할 수 있다.

def make_adder(n):
    def add(x):
        return x + n    # n은 make_adder의 변수
    return add

add5 = make_adder(5)
print(add5(3))    # 8
print(add5(10))   # 15

make_adder(5) 호출이 끝나도 n=5는 메모리에 살아있고, add5를 호출할 때마다 사용된다.

자유 변수(Free Variable)

add 함수 안의 nadd의 지역 변수도 아니고 전역 변수도 아니다. 자유 변수라고 한다. 클로저 객체의 __closure__에 저장된다.

print(add5.__closure__)
# (<cell at 0x...>,)
print(add5.__closure__[0].cell_contents)
# 5

클로저 — 자유 변수와 스코프

LEGB 스코프 규칙

Python에서 변수를 찾는 순서는 LEGB다.

  1. Local — 현재 함수 내부
  2. Enclosing — 둘러싼 함수들의 스코프 (클로저가 여기서 찾음)
  3. Global — 모듈 수준
  4. Built-in — Python 내장

클로저는 E (Enclosing) 단계에서 변수를 찾는다.

nonlocal — 외부 변수 수정

클로저 내에서 외부 변수를 읽기만 할 때는 문제가 없다. 하지만 **수정(할당)**하려면 nonlocal 선언이 필요하다.

def make_counter(start=0):
    count = start
    def inc():
        nonlocal count
        count += 1
        return count
    return inc

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

nonlocal 없이 count += 1을 쓰면 UnboundLocalError가 발생한다. Python이 count를 지역 변수로 간주하기 때문이다.

nonlocal — 외부 변수 수정

팩토리 패턴

클로저의 대표 활용 사례는 팩토리 함수다. 설정값을 기억하는 함수를 만든다.

def make_multiplier(factor):
    return lambda x: x * factor

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

루프에서 클로저 함정

루프 변수를 클로저에서 캡처할 때 흔히 겪는 함정이 있다.

funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])   # [2, 2, 2] — 예상은 [0, 1, 2]

루프가 끝난 후 i2이고, 세 람다 모두 같은 i를 참조하기 때문이다. 해결책은 기본 인수로 값을 고정하는 것이다.

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])   # [0, 1, 2]

정리

클로저는 함수형 프로그래밍의 핵심 개념이다. 데코레이터, 팩토리 함수, 콜백 등 여러 패턴의 기반이 된다.


지난 글: 람다 함수: 익명 함수의 용법과 한계

다음 글: 재귀 깊이 제한: sys.setrecursionlimit과 스택 오버플로


읽어주셔서 감사합니다. 😊