Closure 파헤치기

2017-03-31 00:00:00
closure python javascript

Closurescope를 교묘하게 이용하는 기술이며 Morris Johns에서 두가지 요소로 요약하고 있습니다.

우선 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 stack frame

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 키워드 대신에 자기 자신을 호출(...)하여 무한으로 스택이 쌓이는 일은 없을 것 같습니다.

Reference