파이썬 클로저 : 개념, 사용 이유, 사용 법, 장단점

파이썬 클로저 개념, 사용 이유, 사용 법, 장단점에 대해 포스팅 합니다.

클로저자유 변수를 기억하며 상태를 유지하는 함수로, 함수 내부에서 정의된 함수가 외부 변수를 참조할 때 사용합니다.

이는 함수가 자신의 스코프 외부에 있는 변수를 “캡처(capture)”하여 상태를 기억하는 메커니즘입니다.

해당 개념을 이해 하기 위한 사전 개념은 파이썬 함수의 특징, 스코프 입니다. 간단한 개념이니 모르신다면 해당 포스트를 확인하시면 됩니다.

처음 들으면 한번에 흡수되는 개념이 아니니, 여러번 곱씹는 게 좋은 게 좋습니다 ㅎㅎ

클로저 함수 형태는 아래와 같습니다. 코드를 한번 보고 설명을 이해해 봅시다.

# 클로저(Closure) 사용
def closure_ex1():
    # 자유 변수
    series = []
    # 클로저 영역
    def averager(v):
        series.append(v)
        print('inner: {} / {}'.format(series, len(series)))
        return sum(series) / len(series)
    
    return averager
    
avg_closure1 = closure_ex1()

print(avg_closure1(15))  # inner: [15] / 1반환 15.0
print(avg_closure1(35))  # inner: [15, 35] / 2반환 25.0
print(avg_closure1(40))  # inner: [15, 35, 40] / 3반환 30.0

클로저 주요 특징

자유 변수(free variable): 내부 함수가 참조하지만 내부 함수의 로컬 스코프에 속하지 않는 변수. 예를 들어, 외부 함수의 로컬 변수.

캡처와 수명 연장: 외부 함수가 반환되면 자유 변수의 수명이 연장되어 클로저가 호출될 때마다 유지됩니다.

용도: 상태를 유지하는 함수를 만들 때 유용합니다. 예를 들어, 카운터, 누적 계산기, 또는 데코레이터에서 자주 사용됩니다.

파이썬 클로저 개념

Python의 클로저는 함수가 해당 스코프 외부에서 호출되는 경우에도 자유 변수에 액세스할 수 있는 함수 개체를 나타냅니다.

우선 클로저를 사용하지 않는 일반 함수에서는 상태가 저장되지 않는다는 것을 보겠습니다.

def averager(v):
    series = []  # 매번 새로 초기화됨
    series.append(v)
    print('inner: {} / {}'.format(series, len(series)))
    return sum(series) / len(series)

print(averager(15))  # inner: [15] / 115.0
print(averager(35))  # inner: [35] / 135.0 (상태 초기화됨)
print(averager(40))  # inner: [40] / 140.0 (상태 초기화됨)

일반 함수는 멀티스레딩에 안전하지만, 아래 보이는 클로저처럼 공유 상태를 숨기지 않으므로 디버깅이 쉬울 수 있습니다.

아래의 클로저 함수의 코드를 보면 11번 줄에서 내부 함수를 반환하는 것을 볼 수 있습니다.

13번 줄을 보면 외부 함수를 변수로 할당 하는 것을 볼 수 있습니다.

# 클로저(Closure) 사용
def closure_ex1():
    # 자유 변수
    series = []
    # 클로저 영역
    def averager(v):
        series.append(v)
        print('inner: {} / {}'.format(series, len(series)))
        return sum(series) / len(series)
    
    return averager
    
avg_closure1 = closure_ex1()

print(avg_closure1(15))  # inner: [15] / 1 -> 15.0
print(avg_closure1(35))  # inner: [15, 35] / 2 -> 25.0
print(avg_closure1(40))  # inner: [15, 35, 40] / 3 -> 30.0

15~17 코드의 실행 결과를 보면 함수가 종료되었음에도 자유 변수인 series가 계속 메모리에 저장되어 그 다음 함수 호출에도 이용되는 것을 볼 수 있습니다.

클로저 기능을 클래스로 표현하면 아래와 같습니다.

init으로 초기화하고, call로 함수처럼 호출 가능하게 만듭니다.

이는 클로저와 유사하지만 객체 지향적으로 구현됩니다.

class Averager:
    def __init__(self):
        self.series = []  # 인스턴스 속성으로 상태 초기화

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

avg_class = Averager()
print(avg_class(15))  # inner: [15] / 115.0
print(avg_class(35))  # inner: [15, 35] / 225.0
print(avg_class(40))  # inner: [15, 35, 40] / 330.0

그럼 클래스로 다 해결하면 되는게 아닌가 싶습니다.

클로저는 언제 쓰는거지?

클로저와 클래스를 비교하면 아래와 같습니다.

클로저는 함수형 스타일로 가볍고, 클래스보다 적은 오버헤드를 가집니다.

하지만 클래스는 타입 힌팅이나 IDE 지원이 더 좋습니다. 또한 클래스는 상태를 명시적으로 관리해 테스트가 용이합니다.

따라서 간단한 경우 클로저, 복잡한 경우 클래스를 선택합니다.
(파이썬 커뮤니티 베스트 프랙티스: PEP 20 – Zen of Python, “Simple is better than complex”).

파이썬 클로저 사용 이유

그럼 클로저는 언제 어떻게 사용되는지 몇 가지 정리해보겠습니다.

다음은 Python에서 클로저를 사용하는 일반적인 사용 사례와 이점입니다.

1. 상태 저장 기능

클로저는 함수 호출 사이의 상태를 유지합니다. 이는 함수를 여러 번 호출할 때 일부 정보를 보존하려는 경우 유용할 수 있습니다.

# 데이터 캡슐화 및 숨기기
def counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter_instance = counter()
print(counter_instance())  # Output: 1
print(counter_instance())  # Output: 2

2. 데이터 캡슐화 및 숨기기

클로저를 사용하면 함수 범위 내에서 데이터를 캡슐화하여 데이터 숨기기 또는 캡슐화 형식을 제공할 수 있습니다.

3. 데코레이터

향후 정리하겠지만, 데코레이터는 클로저의 일반적인 사용 사례입니다. 코드를 변경하지 않고도 함수의 동작을 수정하거나 확장할 수 있습니다.

파이썬 클로저에서 자유변수 검사

우선 클로저의 활용법을 설명하기에 앞서 클로저에서 자유 변수를 확인하는 법에 대해서 설명합니다.

위의 counter 함수에서 자유 변수명과 자유 변수 값은 아래와 같이 확인 할 수 있습니다.

def counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter_instance = counter()
print(counter_instance())  # Output: 1
print(counter_instance())  # Output: 2
print('자유 변수명 확인:',counter_instance.__code__.co_freevars)  # ('count',)
print('자유 변수값 확인:',counter_instance.__closure__[0].cell_contents)  # 2

위 코드에서 주의해야 할 점은 자유 변수에 mutable 객체를 사용하는 것이 아니라면 inner function에서 자유 변수를 nonlocal로 선언해야 한다는 것입니다.

클로저 활용법

클로저를 클래스와 비교한 것에서 눈치 채셨겠지만, 클로저는 함수형 프로그래밍을 가능하게 합니다.

함수형 프로그램하면 대표적으로 알 수 있는 콜백 함수부터 팩토리 함수까지 아래와 같이 활용할 수 있습니다

팩토리 함수

클로저 기능을 이용해서 다양한 특수 기능의 함수를 생성하게 만들 수 있습니다.

아래는 power_factory 코드로 다양한 거듭제곱을 반환하는 함수를 생성합니다.

def power_factory(power):
    def power_function(x):
        return x ** power

    return power_function

square = power_factory(2)
cube = power_factory(3)

print(square(3))  # Output: 9 
print(cube(3))    # Output: 27 

콜백 함수

클로저 함수를 만들 때 inner function은 outer funcion 내부에서 정의하는 게 아니라 함수를 매개변수로 받게 할 수 있습니다. 그러면 함수를 수정하지 않고도 다양한 작업을 유연하게 변경 할 수 있는 콜백 함수를 만들 수 있습니다.

def perform_operation(x, y, operation):
    return operation(x, y)

def add(x, y):
    return x + y

def multiply(x, y):
    return x * y

result_add = perform_operation(3, 4, add)
result_multiply = perform_operation(3, 4, multiply)

print(result_add)        # Output: 7
print(result_multiply)   # Output: 12

파이썬 클로저 사용 시 유의 점

자유 변수로 immutable 객체를 사용 시

위에서 잠시 언급했지만 자유 변수에 mutable 객체를 사용하는 것이 아니라면 inner function에서 자유 변수를 nonlocal로 선언해야 합니다.

Python에서는 내부 함수에서 불변 객체(예: int)를 참조하면 Python은 기본적으로 이를 지역 변수로 간주합니다.

nonlocal로 선언하지 않고 해당 변수의 값을 수정하려고 하면 Python은 UnboundLocalError를 발생 시킵니다.

이 예에서 목록(x[0] += 1)을 수정하는 데에는 목록이 변경 가능하므로 nonlocal을 사용할 필요가 없습니다. 그러나 nonlocal을 사용하지 않고 정수(y += 1)를 수정하려고 하면 정수의 불변성으로 인해 오류가 발생합니다.

def outer_function():
    x = [10]  # a mutable list
    y = 10    # an immutable integer

    def inner_function_list():
        x[0] += 1  # 수정가능한 list는 nonlocal이 필요하지 않음
        print(x[0])

    def inner_function_int():
        # nonlocal을 입력하지 않는 다음 줄 코드에서 UnboundLocalError이 발생함
        y += 1
        print(y)

    return inner_function_list, inner_function_int

inner_list, inner_int = outer_function()

inner_list()  # Output: 11 
inner_int()   # UnboundLocalError

변수 바인딩 주의

변수 바인딩에 주의 해야 합니다. inner function에서 정의된 변수는 해당 함수의 로컬 범위에서 바인딩 됩니다.

즉 아래 코드에서 5번 라인이 실행 된다면 4번 줄의 x가 자유 변수 x가 아닌 inner function의 로컬 변수인 x로 접근하게 되고 이는 에러를 발생 시키게 됩니다.

def outer_function():
    x = [1, 2, 3]
    def inner_function():
        x[0] = 99      # mutable object 수정
        # x = [4, 5, 6]  # 재할당(지역 변수) 시, 에러 발생
        print(x)
    return inner_function

closure_instance = outer_function()
closure_instance()

메모리 누수

클로저가 포함된 경우 의도하지 않은 메모리 누수가 발생하지 않도록 주의해야 합니다. 클로저는 포함 범위에 대한 참조를 유지하므로 수명이 긴 클로저는 개체가 더 이상 필요하지 않은 경우에도 개체가 가비지에 수집 되지 않을 수 있습니다.

클로저 장단점

클로저의 장단점은 위의 글에서 언급되었지만, 요약하면, 장점은 캡슐화, 재사용성, 함수형 프로그래밍를 가능하게 해줍니다. 반면 단점은 변수 바인딩 주의, 메모리 사용량 증가, 복잡성 증가 및 디버깅의 어려움이 있습니다.

따라서 관련된 잠재적인 위험을 염두에 두어야 하며 이에 따라 클로저를 신중하게 사용해야 합니다.

참고하면 좋은 글

Leave a Comment


목차