이번 시간에는 저번 시간에 이어서 본격적인 클로저에 대한 정의와 개념, 조건,
특징 등에 대해서 알아보려고 합니다.
이 글을 통해서 조금이나마 도움이 되시는 분이 계시다면 만족합니다 :)
자! 그러면 한번 클로저에 대해서 알아보도록 할까요?
먼저 다양한 클로저의 정의에 대해서 한번 알아보도록 하겠습니다.
사실 클로저의 정의만 보고 이해하기란 정말 쉽지 않습니다.
그래도 일단 정의에 대해서 보고 가보도록 하죠
다음은 MDN에서 정의한 Closure입니다.
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function.
이를 직역하면,
클로저는 주변 상태(lexical environment)에 대한 참조와 함께 번들로 묶인(포함 된) 함수의 조합입니다.
즉, 클로저를 사용하면 내부 함수에서 외부 함수의 scope에 액세스 할 수 있습니다.
로 해석이 가능하겠네요.
이 문장만 봐서는 전혀 알수가 없습니다. 이해가 어려운 단어와 문장으로 가득합니다.
그러면 천천히 개념에 대해서 좀 더 이해하고 다음의 문장을 다시 이해해 보도록 목표를 삼아 보죠!
다른 클로저의 정의를 좀 더 알아볼까요?
다음과 같은 정의를 블로그에 찾아 볼 수 있었습니다.
자신을 둘러싼 스코프(네임스페이스)의 상태값을 기억하는 함수
즉, 클로저는 일단 '함수' 인 것 같습니다.
두 번째 정의에서는 함수 중에서 자신을 둘러싼 scope의 상태 값을 기억하는 함수라고 하네요.
자신을 둘러싼 scope의 상태값이라.. 다음과 같은 코드를 의미하는 것일까요?
msg = "안녕하세요!"
def main_func():
global msg
return msg
main_func()
위의 코드는 global scope에 있는 msg
변수를
main_func
의 local scope에 가져다가 사용해서 해당 값을 그대로 반환하고 있습니다.
하지만 완벽히 위의 정의를 충족시키고 있진 못하고 있는 듯 합니다.
무엇보다도 위의 코드는 굉장히 위험합니다.
왜일까요?
첫 번째로, global scope에 있는 msg
변수가 변경되거나 삭제하면
이에 main_func
이 에러가 발생하거나 값이 변할 수 있습니다.
msg = "안녕하세요!"
def main_func():
global msg
return msg
del msg
main_func()
# 결과
NameError: name 'msg' is not defined
두 번째로, scope의 상태 값을 기억한다고 보기 어렵습니다.
위의 코드는 기억하기 보다는 오히려 그냥 global scope의 msg
변수를 그대로 참조해서 반환하고 있는 것이 정확하겠네요.
그렇다면 저번 시간에 알아본 중첩 함수(nested function)와 nonlocal를 조금 활용해 보면 어떨까요?
다음의 코드를 한번 보시겠습니다.
def outer_func():
msg = "안녕하세요 !"
def inner_func():
nonlocal msg
return msg
return inner_func
f = outer_func()
f()
위의 코드는 outer_func
이라는 외부 함수 내에 inner_func
이라는 중첩 함수를 구현하고,
inner_func
은 nonlocal scope에 위치하는 msg를 읽어 반환하고 있습니다.
최종적으로 outer_func
는 중첩 함수 자체(inner_func
)를 반환하고 있습니다.
전 시간에 알아보았지만, 상위 scope의 변수를 읽는 행위는 따로 nonlocal로 선언해 주지 않아도 됩니다.
그렇다면 f
함수는 자신을 둘러싼 scope(msg
)의 값을 기억하고 있을까요?
def outer_func():
msg = "안녕하세요!"
def inner_func():
return msg
return inner_func
f = outer_func()
del outer_func
f()
# 결과
'안녕하세요!'
오! 기억하고 있네요!
제가 del
명령어를 통해 outer_func
이라는 함수 객체를 지웠는데도 불구하고, msg의 값을 그대로 기억하고 있습니다.
점점 더 클로저에 가까워져 가고 있는 것 같습니다.
그렇다면 nonlocal이 아닌, global scope에 위치하는 변수는 어떨까요?
msg = "안녕하세요!" #- global scope에 msg 선언
def outer_func():
def inner_func():
return msg
return inner_func
f = outer_func()
del msg
f()
# 결과
NameError: name 'msg' is not defined
에러가 발생하네요. 즉, 자신의 둘러싼 scope는 nonlocal scope 일 때만 기억할 수 있는 듯 합니다.
그렇다면, 정확히 클로저란 무엇일까요?
클로저를 좀 더 정확히 이해하려면, 클로저의 생성 조건을 파악해야 합니다.
클로저의 생성 조건은 다음과 같습니다.
위에서 추론해 보았던 예제 코드를 통해 클로저 생성 조건을 대입해 볼 수도 있지만,
특정 블로그에 굉장히 좋은 예시가 하나 있어 인용해 적용해 보려고 합니다.
우리는 processor(cpu)가 cache에 들러 원하는 데이터가 있는지를 확인하는 cache hit 검사 모듈을 구현한다고 가정하겠습니다.
구현된 코드는 다음과 같습니다.
def in_cache(func):
cache = {}
def wrapper(n):
if n in cache:
return cache[n]
else:
cache[n] = func(n)
return cache[n]
return wrapper
즉, cache dict 객체 안에 n이 있으면 그대로 반환하며(cache hit), 없으면(cache miss) 값을 생성해서 cache에 저장 후, 해당 값을 반환하는 코드입니다.
아마 눈치 채셨겠지만, 위의 코드에서 wrapper
는 클로저입니다.
cache
라는 객체를 참조하고 있으며in_cache
라는 함수에서는 자신을 반환합니다. 그렇다면 왜 이런 클로저를 생성해서 사용하는 것일까요?
다음의 특징을 보면 이해할 수 있습니다.
숫자가 입력되면 1부터 해당 숫자까지의 합을 계산하는 함수를 하나 가져와 in_cache와 같이 사용해 보겠습니다.
이 때 wrapper 중첩 함수 안에서 print
문을 통해 cache를 확인해 보도록 하죠.
코드는 다음과 같습니다.
def in_cache(func):
cache = {}
def wrapper(n):
print(cache)
if n in cache:
return cache[n]
else:
cache[n] = func(n)
return cache[n]
return wrapper
def list_sum(n):
return sum(list(range(n)))
list_sum = in_cache(list_sum)
해당 코드는 wrapper의 nonlocal scope(enclosing)에 있는 cache라는 변수를 추적합니다.
다음 코드를 통해 실행하면 결과가 어떻게 될까요?
list_sum(5) #- 결과?
list_sum(10) #- 결과?
list_sum(8) #- 결과?
결과는 다음과 같습니다.
{}
Out[56]: 10
{5: 10}
Out[57]: 45
{5: 10, 10: 45}
Out[58]: 28
이상함을 눈치 채셨나요?
nonlocal scope에 위치하는 cache 변수는 전역 변수처럼 행동하고 있습니다!
즉, 우리가 실행한 list_sum = in_cache(list_sum)
구문에서, 원래 정의한 list_sum
함수가 아닌, in_cache(list_sum)
를 반환한 함수를 사용함으로서, 우린 클로저를 사용한 것입니다.
즉, 다시 정리하면 클로저란, 외부함수 in_cache
에 둘러쌓인 nonlocal scope의 객체의 값을 기억하는 함수인 것입니다!
list_sum = in_cache(list_sum)
구문에서 우리는 in_cache
함수의 중첩 함수인
wrapper
함수를 반환 받아 사용했는데요,
해당 함수 밖에 존재하던 cache
변수를 마치 전역 변수처럼 사용할 수 있게 된 것입니다.
외부 함수는 이미 종료됐는데도 말이죠.
다시 말하면, in_cache
함수를 삭제해도 cache
변수가 살아있게 됩니다.
del in_cache
list_sum(5)
#- 결과
{5: 10, 10: 45, 8: 28}
Out[67]: 10
위의 예제를 계속적으로 탐구해 보도록 하겠습니다.
in_cache
함수를 지웠는데도 cache
변수는 전역변수 처럼 그대로 살아있음을 확인했습니다.
그렇다면 해당 변수는 어디에 있는 것일까요?
__closure__
변수에 가지고 있습니다.
파이썬 3을 기준으로 클로저 함수는 자동으로 해당 변수를 가지고 있습니다. 해당 변수에는 클로저가 참조하는 eclosing에 위치하는 변수들의 값을 저장하고 있습니다.
다음과 같이 코드를 실행시켜 보면, 저장된 cache
변수의 값을 확인할 수 있습니다.
list_sum.__closure__[0].cell_contents
# 결과
Out[68]: {5: 10, 10: 45, 8: 28}
이러한 클로저의 장점은 전역변수의 단점을 해소시켜 줄 수 있습니다.
만약 전역 변수를 사용했다면, 다른 영향도를 고려해야 하므로, 위의 행동들이 쉽지 않을 것입니다.
또한 스코프 별로 동일한 변수명(ex) cache
) 를 사용함으로써 좀 더 깨끗한 코드를 작성할 수도 있겠죠.
다시 가장 위로 올라가 클로저의 정의를 살펴볼까요?
클로저는 주변 상태(lexical environment)에 대한 참조와 함께 번들로 묶인(포함 된) 함수의 조합입니다.
즉, 클로저를 사용하면 내부 함수에서 외부 함수의 scope에 액세스 할 수 있습니다.
자신을 둘러싼 스코프(네임스페이스)의 상태값을 기억하는 함수
여기서 말하는 lexical environment는 내부 함수가 선언됐을 때의 scope를 말합니다.
즉, 위의 예제로 대비해서 다시 작성해보면, 클로저는 cache와 같은 객체에 대한 참조와 함께 클로저의 고유 스코프로 묶인 함수의 조합! 으로 다시 이해할 수 있습니다.
이때 cache
는 외부 함수가 사라져도 계속 기억되고 있으므로, 상태값을 기억하고 있죠.
자! 드디어 클로저의 정의까지 완벽히 이해해 보았습니다!
꼭 클로저에 대한 개념을 실제 프로그램 코드에 활용해 보시길 바랍니다 ^^
감사합니다!
이 글을 작성하기에 참조한 블로그는 다음과 같습니다.
Python의 Closure에 대해 알아보자
1급 객체(first-class object)란?