프로그래밍백엔드 개발자

파이썬에서 클로저(closures)가 어떻게 작동하는지 설명하고, 일반 함수와의 차이점과 실용적인 적용 사례는 무엇인지 설명하세요.

Hintsage AI 어시스턴트로 면접 통과

답변.

질문의 배경

"클로저"라는 용어는 함수형 프로그래밍에서 유래되었으며 파이썬에 도입된 지 오래되었습니다. 클로저는 함수가 생성된 환경을 기억할 수 있게 하여, 그 환경 밖에서 호출되더라도 유지되게 합니다. 이 개념은 유연성을 제공하며, 함수 팩토리 및 지연 계산과 같은 여러 패턴을 구현할 수 있게 합니다.

문제

파이썬에서 함수는 1급 객체입니다. 때때로 내부 함수가 포함 함수의 범위에 있는 변수를 사용해야 하는 경우가 있습니다. 일반적인 렉시컬 범위는 함수가 반환될 때 이를 보장하지 않습니다. 만약 그러한 함수가 생성 환경의 변수를 참조하게 된다면 클로저가 발생합니다.

해결책

클로저는 내부 함수가 외부에서 정의된 변수를 참조하고, 그 외부 함수가 내부 함수를 외부로 반환할 때 발생합니다. 이는 함수 팩토리를 생성하거나, 클래스 없이 상태를 캡슐화하거나, "즉시" 파라미터를 가지는 함수를 구성할 때 자주 사용됩니다.

코드 예시:

def make_multiplier(factor): def multiplier(x): return x * factor return multiplier mul2 = make_multiplier(2) mul3 = make_multiplier(3) print(mul2(10)) # 20 print(mul3(10)) # 30

주요 특징:

  • 클로저는 외부 함수가 종료된 후에도 환경 변수의 값을 유지합니다.
  • 내부 함수의 상태는 본질적으로 프라이빗하여, 외부에서 직접 수정할 수 없습니다.
  • 외부 함수의 변수를 변경하기 위해 클로저 내에서 nonlocal 키워드를 사용해야 합니다.

트릭 질문들.

클로저가 호출 간에 변경 가능한 상태를 유지할 수 있나요, 만약 변수가 내부 함수에서 변경된다면?

네, 내부 함수에서 nonlocal 키워드를 사용하면 가능합니다. nonlocal 없이 대입하면 새로운 지역 변수를 생성하여 외부 변수를 변경하지 않습니다.

def counter(): count = 0 def inc(): nonlocal count count += 1 return count return inc c = counter() print(c()) # 1 print(c()) # 2

클로저를 사용해 파이썬에서 클래스를 대신해 프라이빗 변수를 구현할 수 있나요?

네, 클로저는 외부에서 접근할 수 없는 "프라이빗" 변수를 단순하게 구현하는 방법을 제공합니다. 만약 내부 함수에 getter/setter가 제공되지 않는다면, 외부에서 접근할 수 없습니다.

클로저는 함수에만 적용되나요? 파이썬에서 람다를 사용해 클로저를 만들 수 있나요?

네, 클로저는 람다 표현식을 사용하여도 형성될 수 있으며, 이는 변수의 렉시컬 바인딩 면에서 def와 유사합니다.

def make_power(n): return lambda x: x ** n square = make_power(2) cube = make_power(3) print(square(4)) # 16 print(cube(2)) # 8

일반적인 실수 및 안티 패턴

  • 클로저가 내부에서 외부 변수를 변경할 때 자동으로 변경한다고 기대하지 않기.
  • 클로저에서 변경 가능한 객체를 잡혀서 수정할 때 디버깅의 어려움을 겪을 수 있음.
  • 변수 바인딩이 올바르지 않은 반복문에서 클로저를 사용하여 함수를 생성하기 (함수 모두가 변수의 마지막 값을 보는 함정).

실제 사례

부정적 케이스

루프에서 핸들러를 형성하는 함수 팩토리는 루프 변수를 내부 클로저에서 사용하는 경우:

handlers = [] for i in range(3): def handler(x): return x + i handlers.append(handler) print([h(10) for h in handlers]) # [12, 12, 12]

장점:

  • 간단하고 코드가 적음.

단점:

  • 모든 핸들러가 동일한 변수를 참조하므로 마지막 값인 2를 사용하게 되어 예상치 못한 동작이 발생함.

긍정적 케이스

디폴트 인자를 사용하여 "고정" 값을 설정:

handlers = [] for i in range(3): def handler(x, j=i): return x + j handlers.append(handler) print([h(10) for h in handlers]) # [10, 11, 12]

장점:

  • 필요한 값 바인딩.
  • 예측 가능한 동작.

단점:

  • 이 미세한 사항을 기억하여 클로저를 수동으로 수정해야 하므로 코드의 유지 관리성이 복잡해짐.