반응형

파이선에서 클로저 함수란 무엇인지, 그 동작에 대해 알아본다. 또한 클로저와 데코레이터의 관계도 파악한다.

 

클로저 (Closure)

  • 클로저란, 함수 안에서 정의된 내부 함수가 외부 함수의 지역 변수를 참조하고, 외부 함수의 실행이 끝난 뒤에도 그 변수의 메모리를 계속 참조하는 함수이다. 외부 함수가 종료된 뒤에도 변수가 소멸하지 않는다.
  • 외부 함수의 변수를 캡처하여 내부 함수에서 계속 사용할 수 있기 때문에, 클로저를 사용하면 특정 메모리 공간에 계속 접근 가능하면서 동작 가능한 함수를 생성할 수 있다.

 

클로저의 구성 요건

  1. 중첩 함수: 함수 안에 또 다른 함수가 정의되어 있어야 한다.
  2. 외부 함수 변수 참조: 내부 함수가 외부 함수의 변수를 참조해야 한다.
  3. 외부 함수의 리턴값: 외부 함수는 내부 함수를 리턴해야 한다.

 

아래 간단한 클로저의 예시를 보자.

def outer_function(x):
    # 외부 함수의 지역 변수
    def inner_function(y):
        print(f"x = {x}")
        return x + y  # 외부 변수 x 참조

    return inner_function  # 내부 함수를 반환


# 외부 함수를 호출하고 결과로 내부 함수를 가져옴
closure1 = outer_function(10)
closure2 = outer_function(20)
print(f"hex(id(closure1)) = {hex(id(closure1))}, hex(id(closure2)) = {hex(id(closure2))}")

# 내부 함수 호출
print(f"closure1(5) = {closure1(5)}")  # 10 + 5 = 15
print(f"closure2(5) = {closure2(5)}")  # 20 + 5 = 25
hex(id(closure1)) = 0x231de68d1c0
hex(id(closure2)) = 0x231de68d260
x = 10
closure1(5) = 15
x = 20
closure2(5) = 25

inner_fuction은 외부 함수인 outer_function의 변수 x를 참조하고 있다.

closure1 = outer_function(10)를 통해 closure1 변수가 클로저 함수를 가리킨다.

closure2 = outer_function(10)를 통해 closure2 변수도 클로저 함수를 가리킨다.

주소값을 통해 확인할 수 있듯, closure1closure2는 각기 다른 클로저 함수를 가리키고 있음을 알 수 있다.

중요한 점은 closure1closure2가 단순히 inner_function이라는 함수를 가리킨다는 것만이 아니라, 그것을 감싸는  outer_function의 실행 컨텍스트(즉 외부 변수 x의 값)을 함께 캡처(capture)하고 있다는 점이다.

 

클로저의 활용(장점)

1. 데이터의 은닉: 외부 변수에 직접 접근하지 못하도록 보호한다. 

위 예시에서 외부 변수 x의 값은 외부로부터 직접 접근할 수 없다(=은닉되고 있다).

xouter_function의 지역 변수이며, outer_function의 호출 이후에는 이 외부 변수는 클로저 함수의 호출을 통해서만 간접적으로 접근 가능하고 외부에서는 직접 접근할 수 없다. 즉 외부에서는 x라는 이름으로 값을 확인하거나 어떤 다른 방법으로 해당 값을 수정할 수 있는 방법이 없다. 

즉 아래와 같은 코드로 외부 변수 x를 수정할 수 없다.

closer1.x = 30

 

2. 상태 유지: 클로저는 함수 호출 사이에 상태를 유지할 수 있는 방법을 제공한다.

외부 변수의 값을 수정하는 클로저의 예시 코드를 보자.

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

counter_instance = counter()
print(counter_instance())  # 1
print(counter_instance())  # 2
print(counter_instance())  # 3

nonlocal 키워드는 클로저 내부에서 외부 함수의 변수(즉 count)를 수정할 때 사용된다.

총 3번의 함수 호출 동안 count의 값은 계속해서 클로저에 의해 접근 및 참조되고 있음을 확인할 수 있다.

클로저 함수인 counter_instance는 계속해서 외부 변수인 count에 대한 reference를 유지하고 있기 때문이다.

클로저의 이런 특성은, OOP(객체지향 프로그래밍)와 유사한 기능을 제공한다. 파이썬 OOP에서는 인스턴스의 변수와 이를 조작하는 메소드를 통해 "상태"를 관리하는데, 클로저를 사용하면 클래스와 인스턴스 없이도 비슷한 방식으로 상태를 관리할 수 있다는 의미이다.

특징 객체지향 프로그래밍(OOP) 클로저
구조 클래스와 객체 사용 함수와 변수 사용
상태 저장 위치 객체의 인스턴스 변수 클로저의 외부 함수 변수
상태 변경 방식 메서드를 통해 변경 클로저의 내부 함수로 변경
사용 사례 복잡한 상태 관리, 재사용성 높은 설계 간단한 상태 관리, 함수형 프로그래밍 스타일

 

 

클로저 사용 시 주의할 점(단점)

1. 클로저 함수를 지나치게 많이 사용할 경우, 메모리에 오래 유지될 수 있는 변수들로 인해 메모리 누수 가능성이 있다.

클로저 내부에서 외부 함수의 지역 변수를 참조하면, 클로저가 삭제되지 않는 한 그 변수도 메모리에서 제거되지 않는다.

따라서 코드 흐름 상 해당 변수가 더 이상 필요하지 않더라도, 클로저가 살아 있는 한, 파이썬 가비지 컬렉터에 의해 해당 객체의 메모리를 해제할 수가 없게 된다.

이 변수들의 크기가 크거나 갯수가 많다면, 불필요한 메모리 점유가 발생할 수 있다.

 

2. 클로저의 사용은 코드의 가독성과 유지보수성을 저해할 수 있다.

클로저는 함수 안에 또 다른 함수를 정의하고 반환하므로, 코드가 중첩되면서 가독성이 떨어지게 된다. 특히 함수의 실행 컨텍스트가 여러 단계로 나뉘어서, 변수의 흐름을 이해하기가 어려워진다.

def outer_function(a):
    def middle_function(b):
        def inner_function(c):
            return a + b + c  # 여러 레벨의 변수 참조
        return inner_function
    return middle_function

result = outer_function(1)(2)(3)
print(result)  # 6

위 코드의 경우, abc 변수가 어떤 변수인지 추적하기 어렵다. 그리고 중첩된 함수가 많으므로 흐름을 읽기가 어렵다.

 

 

클로저의 상세한 작동원리 영상

유튜브에 파이썬의 클로저의 작동 원리를 매우 잘 설명한 영상이 있다: https://youtu.be/tNSOaA1z6Uo

 

 

클로저와 데코레이터의 관계

  • 데코레이터는 다른 함수를 감싸는 함수로, 원래 함수에 기능을 추가하거나 동작을 변경한다.
    • 데코레이터는 함수를 입력으로 받고, 새로운 함수를 반환한다.
    • (데코레이터에 대한 상세 글은 추후 작성할 예정)
  • 데코레이터는 클로저를 기반으로 동작한다.
  • 데코레이터 안에서 내부 함수(wrapper 함수)는 외부 함수(데코레이터)의 변수(데코레이터의 인자 함수)를 참조해야 하기 때문에, 클로저를 자연스럽게 사용하게 된다.

클로저와 데코레이터의 관계를 볼 수 있는 아래 예시 코드를 보자.

def my_decorator(func):
    def wrapper(*args, **kwargs):  # 내부 함수 (클로저)
        print("함수 호출 전")
        result = func(*args, **kwargs)
        print("함수 호출 후")
        return result
    return wrapper

@my_decorator
def add(a, b):
    return a + b

print(add(2, 3))
함수 호출 전
함수 호출 후
5

 

@my_decorator 를 사용한 add(2, 3) 호출은 아래의 코드와 완전히 동일하다.

my_decorator(add)(2, 3)

my_decorator(add)my_decorator의 내부 함수인 wrapper를 리턴받는데, 이것이 바로 클로저 함수이다. 

해당 클로저 함수는 wrapper의 외부 변수인 func에 대한 참조를 유지하고 있다. 이 덕분에 클로저 함수인 wrapper를 호출했을 때, wrapper 내부에서 func를 호출할 수 있는 것이다.

참고로 add의 함수 파라미터 목록과 wrapper의 함수 파라미터 목록은 동일해야 한다. 그래야 wrapper에서 func(즉 add)를 호출할 때 올바른 아규먼트를 전달할 수 있다.

반응형

+ Recent posts