파이썬

파이썬(python) - 클로저(Closure)

티베트 모래여우 2020. 10. 9. 20:12
반응형

*글을 들어가기 앞서

클로저를 이해하기 위해선 일급 객체(first-class citizen)에 대한 이해가 필요합니다.

일급 객체에 대해 잘 모르시는 분은 아래 링크를 참조해주세요.

 

파이썬(python) - 일급 객체(first-class citizen)

일급 객체(First-class citizen)란? 일급 객체는 OOP에서 사용되는 개념 중 하나로 아래의 조건을 만족하는 객체를 의미합니다. 1. 변수 혹은 데이터 구조(자료구조) 안에 담을 수 있어야 한다. 2. 매개변

tibetsandfox.tistory.com


클로저(Closure)란?

클로저는 어떤 함수의 내부 함수가 외부 함수의 변수(*프리변수)를 참조할 때, 외부 함수가 종료된 후에도 내부 함수가 외부 함수의 변수를 참조할 수 있도록 어딘가에 저장하는 함수를 의미합니다.

클로저 함수는 아래의 조건을 충족해야합니다.

· 어떤 함수의 내부 함수일 것

· 그 내부 함수가 외부 함수의 변수를 참조할 것

· 외부 함수가 내부 함수를 리턴할 것

좀 복잡해 보이니 코드와 함께 풀어서 설명드리겠습니다.

*프리변수(free variable)는 어떤 함수에서 사용되지만 그 함수 내부에서 선언되지 않은 변수를 의미합니다.

처음 봤을땐 저도 대체 이게 뭔 소린가 했습니다.


클로저 파해치기

def hello(msg):
    message = "Hi, " + msg

    def say():
        print(message)

    return say

여기 hello라는 함수가 하나 있습니다.

매개변수로 msg를 받아 message라는 변수에 문자열로 저장하고

내부함수인 say가 message를 print로 출력해줍니다.

그리고 마지막으로 say함수를(함수 실행값이 아닌 함수 자체를)리턴하고 끝납니다.

정말 단순하기 짝이없는 함수지만, 자세히 보시면 say 함수가 클로저의 조건을 모두 만족한다는 사실을 알 수 있습니다.

1. 어떤 함수의 내부 함수일 것 - say함수는 hello함수의 내부 함수

2. 그 내부 함수가 외부 함수의 변수를 참조할 것 - say함수는 외부 함수의 message를 참조

3. 외부 함수가 내부 함수를 리턴할 것 - hello함수는 say함수를 리턴

즉, say함수는 클로저가 될 수 있습니다.

def hello(msg):
    message = "Hi, " + msg

    def say():
        print(message)

    return say

f = hello("Fox") # 클로저 생성
f() # 실행 결과 : "Hi, Fox" 출력

이제 hello함수를 "Fox"라는 문자열과 함께 실행한 결과를 f라는 변수에 저장하고 f를 실행하면

"Hi, Fox"라는 문자열을 출력합니다.

이 문자열이 출력되기까지의 과정을 살펴보자면

1. hello함수에 "Fox"를 매개변수값으로 넘겨주며 실행

2. message변수에 매개변수를 이용해 "Hi, Fox"라는 문자열을 저장

3. say함수가 message변수를 참조

4. say함수 리턴

5. f변수가 say함수를 참조

6. f변수 실행(say함수 실행)

7. f변수는 message변수를 출력

뭔가 이상하지 않나요?

분명 4번 단계에서 hello함수는 역할을 마치고 종료되었습니다. 그리고 메모리에서도 삭제되었겠죠?

메모리에서 함수가 삭제되었으면 내부함수인 message도 함께 삭제되어야 하는데 어떻게 6,7번 단계에서 message변수를 참조해서 출력할 수 있었을까요?

이것을 가능케 한 것이 바로 클로저입니다.

중첩 함수인 say가 외부 함수인 hello의 변수 message를 참조하기에 message변수와 say의 환경을 저장하는 클로저가 동적으로 생성되었고 f가 실행될때는 해당 클로저를 참조하여 message값을 출력할 수 있는 것입니다.

이 클로저는 f변수에 say함수가 할당될 때 생성됩니다.


그렇다면 클로저는 대체 어디에 존재하는 걸까요?

이는 f변수를 dir함수로 까보면 알 수 있습니다.

print(dir(f))
-실행결과-
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', 
'__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__',
 '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', 
'__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', 
'__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', 
'__str__', '__subclasshook__']

3번째 인덱스를 보면 __closure__라는 녀석이 보입니다.

print(type(f.__closure__))
<class 'tuple'>

이 녀석은 Tuple입니다.

print(f.__closure__)
(<cell at 0x00000282C2A844F8: str object at 0x00000282C3183F30>,)

내용물을 보니 뭔 이상한놈이 하나 들어있습니다.

dir함수로 가차없이 또 까봅시다.

print(dir(f.__closure__[0]))
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
 '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
 '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
 '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']

마지막에 cell_contents라는 녀석이 보이네요. 이 녀석을 print하면?

print(f.__closure__[0].cell_contents)
Hi, Fox

네.. 우리가 찾던 message가 여기 있었습니다.

클로저가 저장되는 경로는 __closure__[0].cell_contents정도로 생각할 수 있겠네요.

참고로 저 __closure__ 튜플은 모든 함수 객체가 가지고 있습니다.

그러나 조건을 만족하지 않아 클로저가 생성되지 않으면, 그 값은 None으로 고정됩니다.

def hi():
    print("hi")

print(dir(hi)) #  3번째 인덱스에서 __closure__확인 가능
print(hi.__closure__) # None 출력

클로저의 장점

클로저는 장점이 대체 뭐길래 사용하는걸까요?

여러가지 이유가 있겠지만 제 생각에 클로저의 가장 큰 장점은 "무분별한 전역변수의 남용 방지"라고 생각합니다.

단순히 생각하면 클로저를 쓰는 대신 전역변수를 선언해 상시 접근 가능하게 만들 수 있지만

이렇게 하면 변수가 섞일수도 있고 변수의 책임 범위를 명확하게 할 수 없는 문제가 생깁니다.

하지만 클로저를 사용하면 각 스코프가 클로저로 생성되므로 변수가 섞일 일도 없고 각 스코프에서 고유한 용도로 이용되므로 책임 범위 또한 명확해지죠.

위의 예시에서는 내부 함수가 1개밖에 없지만 만약 내부 함수가 여러개라면?

그 여러개의 내부 함수에서 접근할 수 있게 각각 전역변수를 만들어 값을 지정한다면 코드가 지저분해지고 변수명도 점점 알아보기 힘들어지겠죠? 이를 방지해주는게 가장 큰 장점이라고 생각합니다.

반응형