Closure 파헤치기
2017-03-31 00:00:00Closure란 scope를 교묘하게 이용하는 기술이며 Morris Johns에서 두가지 요소로 요약하고 있습니다.
- first-class 함수를 지원합니다.
즉 다른 변수(variable)를 참조하거나 담을 수 있으며 인수(parameter)로 전달할 수 있고 반환값(return value)으로 전달할 수 있으며(first-citizen) 추가로 함수 타입을 변수로 취급할 수 있고 익명(anonymous)함수를 지원하는 것을 말합니다. - 함수가 실행을 시작할 때 할당되는 stack frame이며 반환 후에 해제되지 않습니다(stack frame이 stack이 아니라 heap에 할당되는 것 처럼).
우선 Javascript 코드를 보면서 천천히 짚어보도록 합니다.
function closure() { var num = 0; var anony = function() { console.log(num); num += 1; } return anony } var func = closure(); func(); // 0 func(); // 1 func(); // 2
먼저 first-class 함수의 성질을 살펴 볼 수 있습니다.
closure
함수 내부에 익명 함수가 보이는데 이 익명함수를 anony
로컬 변수에 할당하였으며 결론적으로 익명 함수를 리턴하게 됩니다.
closure 개념은 first-class 함수의 성질을 만족하는 대부분의 언어에 존재합니다. 도표 참조. 예를 들어 Python 코드에서는 다음과 같이 구현할 수 있습니다.
def closure(): num = 0 def anony(): nonlocal num print(num) num += 1 return anony func = closure() func() # 0 func() # 1 func() # 2
Javascript에서는 scope 규칙이 적용되어 익명 함수 안에서도 외부 변수가 참조 가능하지만 Python에서는 nonlocal
을 명시적으로 지정하여 scope 규칙을 명확히 하는 모습입니다.
하지만 어떻게 closure
의 지역 변수인 num
변수가 둘 다 저장되었을까요?
앞서 stack frame을 언급했지만 이는 다음과 같은 실험으로도 감을 잡으실 수 있습니다.
closure()() # 0 closure()() # 0 closure()() # 0
첫번째로 closure()()
와 같이 호출 할 수 있다는 점과 두번째로 num
이 원래 로컬 변수처럼 0
을 리턴하였습니다.
비밀은 익명 함수를 return
하는 데 있습니다.
return
으로 익명 함수를 반환하게 되지만 closure
함수는 종료되지 않고 func
변수에 할당됩니다.
func = closure()
문으로 함수를 한번 호출한 뒤로는 계속 익명 함수만 호출하게 되어 이전 stack frame에 저장되었던 변수가 남는 것입니다.
일반적으로 함수의 호출 정보를 stack frame이라고 하며 함수로 전달되는 인수와 실행이 종료된 후 복귀 주소와 지역 변수의 정보가 들어갑니다. 일반적으로 이 정보는 heap에 할당되며 stack에서 EBP 레지스터를 통해 참조합니다. 예를 들어 다음과 같이 함수 내부에 함수가 호출되면
def a(): b() def b(): c() def c(): pass a()
a()
를 통해 호출하면 b()
호출 시 stack frame이 생성되며 마찬가지로 c()
호출 후 먼저 c()
가 종료되어 stack frame을 참조하여 b()
로 되돌아가서 b()
가 종료되고 마찬가지로 a()
가 가장 마지막에 종료됩니다.
비슷하게 일반적으로 익명 함수가 동적으로 할당되는 경우 그림으로 나타내면 아래와 같습니다.
closure도 이와 마찬가지입니다. 다시말해 closure
함수가 return
으로 새로운 함수를 호출하게 되지만 종료되지 않고 func
변수에 저장하여 내부 상태가 그대로 남게 됩니다.
closure()()
와 같이 함수를 호출하게 되면 closure
함수가 완전히 종료되므로 내부 상태가 저장되지 않는 것입니다.
Python에서는 sys._getframe(0).f_locals
로 최상단의 stack frame을 확인할 수 있으며 해당 객체의 로컬 namespace를 확인할 수 있습니다.
f_locals
과 같은 여러 속성은 일반적으로 숨겨져 있지만 inspect
라이브러리를 통해 확인할 수 있습니다.
자세한 내용은 아래 Reference를 참조하시기 바랍니다.
실제로 코드 사이사이에 이를 집어넣어 확인해 보면
import sys def closure(): num = 0 print(sys._getframe(0).f_locals) def anony(): num += 1 print(sys._getframe(0).f_locals) return anony func = closure() # {'num': 0} func() # {'num': 1} func() # {'num': 2} func() # {'num': 3}
그렇다면 이 성질을 이용하여 closure()()()()...
형태도 가능함을 알 수 있습니다.
실제로 다음과 같은 3중으로 중첩된 closure도 만들 수 있습니다.
def closure(): v = 0 w = 0 print(sys._getframe(0).f_locals) def anony(): nonlocal v v += 1 print(sys._getframe(0).f_locals) def anony2(): nonlocal v nonlocal w w -= 1 print(sys._getframe(0).f_locals) return anony2 return anony func = closure() # {'w': 0, 'v': 0} func() # {'w': 0, 'v': 1} func() # {'w': 0, 'v': 2} func() # {'w': 0, 'v': 3} func2 = closure()() # {'w': 0, 'v': 0} {'w': 0, 'v': 1} func2() # {'w': -1, 'v': 1} func2() # {'w': -2, 'v': 1} func2() # {'w': -3, 'v': 1}
Python에서는 nonlocal
을 사용하지 않음으로써 scope가 조절이 가능해 해당 변수를 제외시킬 수도 있습니다.
이렇게 closure 개념을 나름대로 깊게 알아보았습니다.
stack frame 구조를 떠올리면 클래스와 비슷한 형태라 하여 this
키워드 대신에 자기 자신을 호출(...)하여 무한으로 스택이 쌓이는 일은 없을 것 같습니다.