파이썬 – 클로저 (Closure)

5 분 소요

이번 글에서는 파이썬의 클로저(closure)를 정리합니다. 파이썬 – 퍼스트클래스 함수 (First Class Function) 글을 먼저 읽으면 이해에 도움이 됩니다.

클로저란 무엇일까요? 한국어 위키백과에는 관련 항목이 없어 영어 Wikipedia의 정의를 참고합니다.

In programming languages, closures (also lexical closures or function closures) are techniques for implementing lexically scoped name binding in languages with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment:[1] a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.[b] A closure—unlike a plain function—allows the function to access those captured variables through the closure’s copies of their values or references, even when the function is invoked outside their scope.

번역하면 다음과 같습니다.

프로그래밍 언어에서의 클로저란 퍼스트클래스 함수를 지원하는 언어의 네임 바인딩 기술이다. 클로저는 어떤 함수를 함수 자신이 가지고 있는 환경과 함께 저장한 레코드이다. 또한 함수가 가진 프리변수(free variable)를 클로저가 만들어지는 당시의 값과 레퍼런스에 맵핑하여 주는 역할을 한다. 클로저는 일반 함수와는 다르게, 자신의 영역 밖에서 호출된 함수의 변수값과 레퍼런스를 복사하고 저장한 뒤, 이 캡처한 값들에 액세스할 수 있게 도와준다.

여기서 프리변수(free variable)란 코드 블록 안에서 사용되었지만, 그 코드 블록 안에서 정의되지 않은 변수를 뜻합니다. 프리변수에 대한 자세한 설명은 별도의 글에서 다룹니다.

정의만 봐서는 이해가 쉽지 않으므로 예제로 살펴봅니다. 다음 코드를 closure.py 파일로 저장합니다.

closure.py
def outer_func(): # 1
message = 'Hi' # 3

    def inner_func(): # 4
        print message # 6 

    return inner_func() # 5

outer_func() # 2

저장 후 터미널에서 해당 디렉터리로 이동해 다음 명령어로 프로그램을 실행합니다.

$ python closure.py
Hi

실행하면 “Hi"가 출력됩니다. 간단한 구문이지만 클로저를 설명하기 위해 “Hi"가 출력되기까지의 과정을 단계별로 확인합니다.

#1에서 정의된 함수 outer_func를 #2에서 호출합니다. outer_func는 인수를 받지 않습니다. 함수가 실행되면 #3에서 message 변수에 ‘Hi’ 문자열이 할당됩니다. 이어서 #4에서 inner_func를 정의하고, #5에서 inner_func를 호출하며 동시에 그 결과를 리턴합니다. #6에서는 message 변수를 참조해 출력합니다. 여기서 messageinner_func 안에서 정의되지는 않았지만 사용되고 있으므로 프리변수에 해당합니다.

여기까지가 “Hi"가 출력되기까지의 흐름입니다. 다음 단계로 넘어갑니다.

#5의 코드를 다음과 같이 수정해 실행합니다.

closure.py
def outer_func(): # 1
message = 'Hi' # 3

    def inner_func(): # 4
        print(message) # 6 

    return inner_func # 5 <-- ()를 지웠습니다.

outer_func() # 2
$ python closure.py

아무 것도 출력되지 않습니다. #5에서 outer_funcinner_func를 호출하지 않고 함수 오브젝트 자체를 리턴하기 때문입니다.

이번에는 outer_func가 리턴하는 inner_func 오브젝트를 변수에 할당해 봅니다. #2의 코드를 수정해 실행합니다.

closure.py
def outer_func(): # 1
message = 'Hi' # 3

    def inner_func(): # 4
        print(message) # 6 

    return inner_func # 5

my_func = outer_func() # 2 <-- 리턴값인 inner_func를 변수에 할당합니다.
$ python closure.py

이번에도 출력되는 값은 없습니다.

이번에는 my_func 변수에 실제로 inner_func 함수가 할당되어 있는지 확인합니다. #7행을 추가해 실행합니다.

closure.py
def outer_func(): # 1
message = 'Hi' # 3

    def inner_func(): # 4
        print(message)  # 6

    return inner_func # 5

my_func = outer_func() # 2

print(my_func)  # 7 <-- 추가
$ python closure.py
<function outer_func.<locals>.inner_func at 0x000001DCADB2AA60>

inner_func 함수가 정상적으로 할당되어 있습니다. 이제 my_func 변수로 inner_func 함수를 호출합니다.

closure.py
def outer_func(): # 1
message = 'Hi' # 3

    def inner_func(): # 4
        print(message)  # 6

    return inner_func # 5

my_func = outer_func() # 2

my_func() # 7
my_func() # 8
my_func() # 9
$ python closure.py
Hi
Hi
Hi

my_func()를 3번 실행해 “Hi"가 3번 출력되었습니다. 그런데 한 가지 짚고 넘어갈 점이 있습니다. outer_func는 #2에서 호출된 직후 종료됐습니다. 그런데 #7, #8, #9에서 호출된 my_func(my_funcinner_func와 같습니다)가 outer_func의 로컬 변수인 message를 참조하고 있습니다. 어떻게 가능했을까요? 그 해답이 바로 클로저입니다. 클로저는 함수의 프리변수 값을 어딘가에 저장한다고 했습니다. 도대체 어디에 저장하는지 그 동작을 파헤쳐 봅니다.

다음과 같이 코드를 수정해 실행합니다.

closure.py
def outer_func():  # 1
message = 'Hi'  # 3

    def inner_func():  # 4
        print(message)  # 6

    return inner_func  # 5

my_func = outer_func()  # 2

print(my_func)  # 7
print()
print(dir(my_func))  # 8
print()
print(type(my_func.__closure__))  # 9
print()
print(my_func.__closure__)  # 10
print()
print(my_func.__closure__[0])  # 11
print()
print(dir(my_func.__closure__[0]))  # 12
print()
print(my_func.__closure__[0].cell_contents)  # 13
$ python closure.py
\<function outer_func.\<locals\>.inner_func at 0x000002913914AA60\>

['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__ha
sh__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subcl
asshook__']

<class 'tuple'>

(<cell at 0x000002913910B730: str object at 0x0000029139147B70>,)

<cell at 0x000002913910B730: str object at 0x0000029139147B70>

['__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']

Hi

출력된 결과를 하나씩 확인합니다.

#7 결과: my_func 변수에 inner_func 함수 오브젝트가 할당되어 있다는 점은 앞에서 확인했습니다.

#8 결과: 클로저가 어디에 데이터를 저장하는지 알아보기 위해 dir()my_func의 네임스페이스를 확인했습니다. __closure__라는 속성이 보입니다.

#9 결과: __closure__의 타입을 확인합니다. 튜플입니다.

#10 결과: 튜플 안의 내용을 확인합니다. 아이템이 하나 들어 있습니다.

#11 결과: 튜플의 첫 번째 아이템을 확인합니다. cell 오브젝트입니다.

#12 결과: cell 오브젝트가 가진 속성을 확인합니다. cell_contents 속성이 보입니다.

#13 결과: cell_contents에 들어 있는 값을 확인합니다. ‘Hi’가 들어 있습니다.

이로써 클로저가 어디에 데이터를 저장하는지 확인했습니다.

여기서부터 함수의 파라미터를 사용하면 한층 응용된 패턴을 만들 수 있습니다. 다음 코드를 저장해 실행합니다.

closure.py
def outer_func(tag):  # 1
text = 'Some text'  # 5
tag = tag  #6

    def inner_func():  # 7
        print('<{0}>{1}<{0}>'.format(tag, text))  # 9

    return inner_func  # 8

h1_func = outer_func('h1')  # 2
p_func = outer_func('p')  # 3

h1_func()  # 4
p_func()  # 10
$ python closure.py
<h1>Some text<h1>
<p>Some text<p>

outer_func 함수 하나로 h1 태그와 p 태그로 문자열을 감싸는 함수를 만들었습니다. 이번에는 태그 안의 문자열도 함께 제어할 수 있도록 함수를 확장해 봅니다.

closure.py
def outer_func(tag):  # 1
tag = tag  # 5

    def inner_func(txt):  # 6
        text = txt  # 8
        print('<{0}>{1}<{0}>'.format(tag, text))  # 9

    return inner_func  # 7

h1_func = outer_func('h1')  # 2
p_func = outer_func('p')  # 3

h1_func('h1 태그의 안입니다.')  # 4
p_func('p 태그의 안입니다.')  # 10
$ python closure.py
<h1>h1 태그의 안입니다.<h1>
<p>p 태그의 안입니다.<p>

클로저는 이렇게 하나의 함수로 여러 가지 함수를 손쉽게 만들 수 있을 뿐 아니라, 기존 함수나 모듈을 수정하지 않고도 wrapper 함수를 이용해 동작을 확장할 수 있는 유용한 패턴입니다.

다음 글에서는 클로저를 활용한 대표적 패턴인 데코레이터를 다룹니다.

X