본문 바로가기

프로그래밍/파이썬

[파이썬] 클로져(Closure) 이해하기

클로저(Closure) 함수

내부함수

클로저(Closure) 함수를 배우기 전에, 내부 함수에 대해 먼저 알아야 한다.
일급 객체 의 특징중에서 함수 내에 함수를 정의 할 수 있다고 했다. 이를 내부 함수(inner function)이라고 한다.

def outer_fucntion(a, b):
  def inner_function(c, d):
    return c + d
  return inner(ab)
 
print(outer_function(35))
 
>>> 출력결과
> 8

심화하기 : 내부 함수(inner function)은 루프나 코드 중복을 피하기 위해 또 다른 함수 내에 어떤 복잡한 작업을 한 번 이상 수행할 때 유용하게 사용한다.

클로저(closure) 1부 : 개념 익히기

위에서 간단하게 내부 함수가 무엇인지 배웠다. 내부 함수는 클로져(closure)처럼 행동할 수 있다.

클로저는 일반 함수와 다르게, 자신의 영역 밖에서 호출된 함수의 변수값과 레퍼런스를 복사, 저장, 접근을 가능하게 한다.
자세한 내용은 위키를 참고하자

def say_words(msg):
    def say_sentence():
        return "안녕? 이걸 출력해줘! : {}".format(msg)
    return say_sentence
 
a = say_words("출출하다")
print("a는 무엇일까? : "a)
print("a의 타입은 무엇일까? : "type(a))
print("a가 함수라는 걸 알았다. 괄호를 붙여보자. ====> "a())
 
>>> 출력결과
> a는 무엇일까? :  <function say_words.<locals>.say_sentence at 0x7fc2c039c0d0>
> a의 타입은 무엇일까? :  <class 'function'>
> a가 함수라는 걸 알았다. 괄호를 붙여보자. ====>  안녕? 이걸 출력해줘! : 출출하다

위 예제에서 내부 함수는 어떤 것인지 맞춰보자. say_sentece() 라는 함수다.
이 내부 함수는 "안녕? 이걸 출력해줘 ! : <msg값> " 을 리턴값으로 돌려준다.

그런데, say_sentece()에 msg를 전달해준 적이 없는데도 메세지를 출력해준다!
이 말이 되는듯 아닌듯 말이 되는 것 같은 아리송함이 있다.

출력결과를 잘 살펴보자
a라는 변수에 say_words()라는 함수를 할당 해줬는데,
a함수를 호출하니까 <function say_words.<locals>.say_sentence at 0x7fc2c039c0d0>

원래는 그냥 function만 출력되야 한다. 지금 당장은 모르겠지만 아무튼 <locals>가 붙는다.
이제 결론을 말해줄 수 있다.

결론 : 클로저는 다른 함수에 의해 동적으로 생성되고, 바깥 함수로부터 생성된 변수값을 알고 있는 변수이다.

클로저(Closure) 2부 : 변수 추적하기

1부 요약 : 클로저는 바깥 함수로부터 생성된 변수를 어딘가에 저장한다. 그리고 이를 기억하고 있는다.
함수가 종료되도 클로저 함수가 기억하고 있는 변수는 메모리상에서 사라지지 않는다.

2부의 내용은 다른 블로그 를 참조해서 정리했다.

2부에서는 클로저는 바깥 함수의 변수를 어딘가에 저장하는데, 어디다가 저장하는지 추적 할 것이다.
1부 코드를 재활용하자.

def say_words(msg):
    def say_sentence():
        return "안녕? 이걸 출력해줘! : {}".format(msg)
    return say_sentence
 
a = say_words("출출하다")
print("a는 무엇일까? : "a)

추적 1단계 : print(dir(a))

dir(a) : 객체(a)가 자체적으로 가지고 있는 변수나 함수를 보여준다. 가장 큰 단위의 조사라고 보면 된다.

아래와 같은 결과가 나온다. a, b, c 순으로 출력되니까 __closure__를 찾아보자.

['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

추적 2단계 : print(type(a.__clousre__)

dir()이라는 내장함수가 변수나 함수를 보여준다고 했으니까, 변수인지 함수인지 타입을 찍어보자.

<class 'tuple'>

추적 3단계 : print(a.__closure__)

오호, 그러면 타입을 알았으니까 튜플 값을 출력해보자.

cell : cell이 뭔가 찾아보니 여러 범위(scope)에서 참조할 수 있는 값을 저장할 수 있는 객체라고 한다.

(<cell at 0x7fc2c0c2fdf8: str object at 0x7fc2c03abd40>,)

print(len(a.__closure__)로 길이를 살펴보니까 1이 출력된다. 사실 위에서 <cell ~~~~> 이 괄호로 둘러쌓여있고, 쉼표(,) 뒤에 아무것도 없으니까 길이가 1이란 걸 알 수 있다.

추적 4단계 : print(dir(a__closure[0]__))

우리는 cell object의 특성을 잘 모른다. 그러니까 다시 dir()을 찍어보자.

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']

포돌이

잡았다 요놈. 마지막에 cell_contents를 숨기고 있었다.

추적 5단계 : print(a.__closure__[0].cell_contents)

드디어 추적이 끝났다. print(a.__closure__[0].cell_contents)를 입력해보면

출출하다

라는 허무한 메시지가 출력된다.

클로저(Closure) 3부 : 마무리

클로저(Closure)를 왜 쓰냐 다시 물어본다면 기존에 만들어진 함수나 모듈 등을 수정하지 않고 wrapper 함수를 이용해서 자기 입맛에 맞게 조정할 수 있다는 것이다.