본문 바로가기

카테고리 없음

Python : 클로저(Closure)에 대한 모든 것

  클로저(Closure)는 자신이 선언될 당시의 환경(context)을 기억하는 함수를 말합니다. 클로저는 함수 내부에서 정의된 함수가 외부 함수의 변수에 접근할 수 있는 기능을 제공합니다. 이러한 구조는 데이터 은닉(data hiding)이나 상태 유지(state retention)가 필요할 때 유용하며, 특히 고급 함수형 프로그래밍에서 자주 사용됩니다.

  • 데이터 은닉: 외부에서 접근할 수 없는 데이터 캡슐화를 가능하게 합니다.
  • 상태 유지: 외부 함수가 종료된 이후에도 변수 상태를 유지할 수 있습니다.
  • 코드 간결화: 재사용성과 간결성을 높이는 패턴을 제공합니다.

변수의 범위

클로저를 이해하기 위해서는 먼저 변수의 범위에 대하여 알고 있어야 합니다.  파이썬에서 변수의 범위는 LEGB 규칙을 따릅니다:

  1. Local: 함수 내부에서 정의된 변수.
  2. Enclosing: 중첩 함수에서 외부(상위) 함수의 변수.
  3. Global: 함수 외부에서 정의된 전역 변수.
  4. Built-in: 파이썬에서 기본 제공하는 내장 변수.

클로저에서 중요한 범위는 Enclosing입니다. 중첩 함수는 상위 함수의 Local 변수를 기억하여, 상위 함수가 종료된 이후에도 해당 변수에 접근할 수 있습니다.

def outer_function():
    message = "I am an enclosing variable"  # Enclosing 변수
    
    def inner_function():
        print(message)  # 외부 함수의 변수 참조
    
    return inner_function

# inner_function을 반환받아 실행
closure = outer_function()
closure()  # 출력: I am an enclosing variable

 

위 예제에서 outer_function 안의 message 변수는 Enclosing 변수입니다. inner_functionmessage 변수를 참조하고 있고,  outer_function은 inner_function을 반환하고 있습니다. 본래 함수안의 변수는 함수가 수행되고 종료된 후에는 사라지게 됩니다만, message 변수는 inner_function에 의하여 언제든지 참조될 수 있습니다. 

 

클로저 사용방법

  위 예제처럼 클로저는 외부 함수에서 선언된 변수를 내부 함수가 캡처(capture)합니다. 그래서 외부 함수가 실행을 마쳐도, 내부 함수는 캡처된 데이터를 계속 사용할 수 있습니다.  

  클로저는 서버프로그램의 동시성에서 필요한 개념으로 교착상태(Deadlock)을 회피하기 위해 사용됩니다. 다수의 쓰레드가  메모리를 공유하지 않고 메시지 전달로 특정 작업을 처리함으로써 교착상태에 빠지는 위험을 방지할 수 있습니다.  

 주로 상태를 저장하는 데 사용되며, 콜백 함수나 데코레이터를 구현할 때 활용됩니다.

def outer_function(message):
    def inner_function():
        print(message)  # 외부 함수 변수 참조
    return inner_function

closure = outer_function("Hello, Closure!")
closure()  # 출력: Hello, Closure!

 

예제1 : 클래스로 구현

 누적평균을 구하는 클래스를  먼저 구현해 봅니다. 

class cal_average():
    def __init__(self) -> None:
        self._series = [] # 멤버변수
    def __call__(self, number): #클래스를 함수처럼 호출하게 한다.
        self._series.append(number)
        return sum(self._series) / len(self._series)

cumul_avg = cal_average() #인스턴스를 생성
print(cumul_avg(10)) # 출력 10 
print(cumul_avg(20)) # 출력 15 <- (10 + 15)/2

 

위 클래스는 _series 라는 리스트형 멤버 변수에 입력 값을 계속 저장하고 호출할때마다  누적 평균을 구해 반환합니다.  매직 함수인 __call__ 을  오버라이딩 하면 클래스를 함수처럼 호출할 수 있게됩니다. 

 

위 클래스를 클로저 함수로 구현해 봅니다.

예제2: 클로저 함수로 구현

def cal_average():
    series = [] # 자유변수 영역
    def exec_avg(number):
        series.append(number) 
        return sum(series) / len(series)
    return exec_avg

cumul_avg = cal_average() # 함수호출. 
print(cumul_avg(10)) # 출력 10 
print(cumul_avg(20)) # 출력 15 <- (10 + 15)/2

 

 위 예제에서 외부 함수인 cal_average는 호출이 완료 되었지만, 자유변수 영역의 series 변수는 내부 함수인 exec_avg에 의해 여전히 접속가능합니다.  

 

잘못된 클로저의 예

클로저 사용 시, 변수의 변경 가능한 속성 때문에 의도치 않은 동작이 발생할 수 있습니다.

def make_multipliers():
    multipliers = []
    for i in range(5):
        def multiplier(x):
            return x * i  # i는 루프의 마지막 값(4)만 참조
        multipliers.append(multiplier)
    return multipliers

functions = make_multipliers()
print([f(2) for f in functions])  # 출력: [8, 8, 8, 8, 8]

 

수정된 코드

i 값을 기본 매개변수로 넘겨 문제를 해결할 수 있습니다.

def make_multipliers():
    multipliers = []
    for i in range(5):
        def multiplier(x, i=i):
            return x * i  # i를 매개변수로 캡처
        multipliers.append(multiplier)
    return multipliers

functions = make_multipliers()
print([f(2) for f in functions])  # 출력: [0, 2, 4, 6, 8]

 

이 글에서는 클로저의 정의와 중요성, 변수 범위, 클로저의 개념과 동작 방식, 예제, 그리고 잘못된 클로저 사용 사례를 살펴보았습니다. 클로저는 코드를 더 간결하고 강력하게 만들지만, 잘못 사용할 경우 오류를 일으킬 수 있으므로 신중하게 활용해야 합니다.