중첩 함수(Nested function)는 함수 안에 다른 함수가 정의된 함수를 의미합니다. 함수 안에 정의된 함수를 내부 함수(Inner function), 내부 함수를 감싸고있는 함수를 외부 함수(Outer function)라고 표현합니다.
중첩 함수는 하나의 함수 안에 하나 이상의 다른 함수를 정의하거나 여러 단계로 중첩된 형태로도 표현될 수 있습니다. 이때 각각의 내부 함수들은 scope chain에 의해 자신을 감싸고 있는 외부 함수의 메모리에 접근이 가능하게 됩니다. 조금더 정확히 표현하면 내부 함수는 외부 함수의 메모리를 복사해서 지니게 됩니다. 따라서 외부 함수의 변수나 매개변수에 접근해서 사용이 가능합니다. 이와 같이 자신의 local 네임스페이스가 아닌 외부 함수의 네임스페이스에 접근하는 내용이 LEGB 규칙 중 E인 enclosed에 해당하게 됩니다. (LEGB란?)
내부 함수에서는 외부 함수의 자원을 이용할 수 있다는 장점이 있습니다. 따라서 같은 매개 변수를 사용하고 연관된 기능을 하는 함수들을 하나의 외부 함수에 묶어서 사용하기도 합니다. 단, 반대인 외부 함수는 내부 함수에 접근할 수 없습니다.
def calculator(x):
def add(y):
return x+y
return add
f = calculator(10) # 매개변수 x로 10이 전달되어 x가 10인 add함수 반환
print(f(5)) # 15
print(f(10)) # 20
calculator라는 외부 함수는 매개변수 x를 입력받아 add 함수를 반환합니다. 따라서 f에는 x가 10인 add 함수가 할당되게 됩니다. 그 다음 f에 매개변수를 전달하여 실행하게 되면 y값이 전달되어 매개변수와 10을 더한 정수가 반환되는 것입니다.
클로저(Closure)는 중첩 함수의 일종입니다. 간단하게 정의한다면 '함수의 반환값으로 내부 함수를 사용하는 함수'라고 정의할 수 있겠습니다. 함수의 반환값으로 내부 함수를 사용하게 되면 first-class function 속성에 따라 내부 함수를 변수에 저장하고 사용할 수 있게 됩니다. (First-Class Function)
원래 클로저의 사전적 정의는 first-class function을 지원하는 언어에서 유효 범위의 이름을 바인딩하는 기술입니다. 쉽게 이야기하면 '함수와 함수가 사용하는 환경(non-local 변수들)을 저장하는 것'입니다. 이를 확인할 수 있는 예제는 반환된 내부함수에서 외부함수의 변수를 사용하는 예제입니다.
def outer():
outer_var = 10
def inner(x):
return outer_var**x
return inner
f = outer()
print(f(2)) # 100
print(f(3)) # 1000
inner 함수에서는 외부 함수의 지역 변수인 outer_var를 사용한 연산값을 반환합니다. 하지만 outer 함수는 f 함수에 inner 함수를 할당하고 종료되었습니다. 그렇다면 inner 함수는 outer 함수의 지역 변수인 outer_var를 사용하기 위해서는 변수를 보관해야 합니다.
클로저의 정의에서 함수와 함수가 사용하는 환경을 저장한다고 설명하였습니다. 클로저는 내부 함수를 반환하지만 내부 함수와 관련된 환경을 따로 저장하고 있습니다. 따라서 외부 함수에서 사용된 자원들이 모두 저장된 상태로 따로 보관되어 내부 함수에서 이 자원들을 사용할 수 있는 것입니다.
f = dir(outer())
print(f)
['__annotations__', '__call__', '__class__', '__closure__', ...]
클로저의 속성을 출력하면 __closure__라는 속성이 있습니다. 이 속성이 클로저에서 필요한 환경 변수들을 저장하는 속성입니다. 이곳에 내부 함수가 사용하는 nonlocal 변수들이 저장되게 됩니다.
지금까지 클로저가 무엇이고, 외부 함수의 자원을 사용하기 위해 환경 자원을 내부적으로 어떻게 저장하고 사용하는지 보았습니다. 이러한 클로저가 사용되는 경우는 크게 3가지로 나뉠 수 있습니다.
global변수를 사용하지 않고, nonlocal 변수를 사용하여 유사한 동작을 구현할 수 있습니다. 또한 nonlocal 변수를 사용하게 되면 데이터를 숨길 수 있기 때문에 일부 정보를 은닉할 수 있게 됩니다.
경우에 따라 모든 로직에 대해 클래스화 하여 멤버 함수를 만들어서 사용하는 것이 비효율적일 수 있습니다. 다루는 변수와 함수가 많지 않은 경우, 클로저로 구현하는 것이 효율적일 수 있습니다.