파이썬은 함수를 일급 객체 (first-class citizen) 로 취급한다.
즉, 함수를 변수에 할당하거나 자료구조안에 저장할 수도 있고, 인자(argument)로 다른 함수에 입력하거나 함수의 결과로 출력할 수도 있다.
[Python] 일급 객체 (first-class citizen)
클로저 (Closure)란?
프로그래밍 언어에서의 클로저란 퍼스트클래스 함수를 지원하는 언어의 네임 바인딩 기술이다. 클로저는 어떤 함수를 함수 자신이 가지고 있는 환경과 함께 저장한 레코드이다. 또한 함수가 가진 프리변수(free variable)를 클로저가 만들어지는 당시의 값과 레퍼런스에 맵핑하여 주는 역할을 한다. 클로저는 일반 함수와는 다르게, 자신의 영역 밖에서 호출된 함수의 변수값과 레퍼런스를 복사하고 저장한 뒤, 이 캡처한 값들에 액세스할 수 있게 도와준다.
출처: school of web.net
- 어떤 함수 내부에서 정의된 함수는 클로저가 될 수 있으며, 클로저는 바깥 함수로부터 생성된 변수값을 변경 또는 저장할 수 있는 함수이다.
클로저 이해하기
- 클로저를 이해하기 위해 아래와 같은 코드를 작성해보자.
def outer_func(): #1
message = 'Hello' #3
def inner_func(): #4
print(message) #6
return inner_func() #5
outer_func() #2
Hello
-
함수를 실행하니
'Hello'
라는 문자열을 출력하였다. 어떤 과정으로 함수가 작동되는지 살펴보자. - #1 :
outer_func
라는 함수가 정의되었다. 인자를 받지않는 함수이다. - #2 :
outer_func
함수가 호출되었다. 함수가 호출되면 함수 내부의 코드들이 실행되기 시작한다. - #3 :
message
라는 변수에'Hello'
라는 문자열이 할당되었다. - #4 :
inner_func
라는 함수가 정의되었다. 이 함수는outer_func
의 내부에서 정의된 함수이며 인자를 받지않는 함수이다. - #5 :
outer_func
의 출력값으로inner_func
를 호출한 결과를 리턴한다. - #6 :
inner_func
가 호출되었으므로inner_func
내부의 명령인print(massage)
가 실행된다. message
변수에는'Hello'
가 할당되어 있으므로'Hello'
라는 문자열을 출력하게 된다.
※ 이 때 inner_func
함수에게 있어서 message
변수는 프리변수 (free variable) 가 된다.
프리변수(free variable)
파이썬에서 프리변수는 코드블럭안에서 사용은 되었지만, 그 코드블럭안에서 정의되지 않은 변수를 뜻한다.
- 위의 함수를 아래와 같이 수정한 후 실행해보자.
def outer_func():
message = 'Hello'
def inner_func():
print(message)
return inner_func # ()를 삭제하여 inner_func를 호출하지 않고 리턴함.
print(outer_func())
<function outer_func.<locals>.inner_func at 0x7f2db28d1c80>
- 함수 객체가 리턴되었다.
inner_func
함수를 호출하지 않고 함수 자체를 리턴하였기 때문이다. outer_func
에 의해 리턴되는inner_func
함수를 변수에 할당해보자.
my_func = outer_func()
outer_func
를 호출한outer_func()
명령은inner_func
라는 함수 객체를 리턴하기 때문에my_func
는outer_func
함수 내부에서 정의된 함수인inner_func
와 같아졌다고 할 수 있다.- 즉,
outer_func
안에서만 존재하는inner_func
를 밖으로 끄집어내었다고 볼 수 있다. - 실제로 그런지 확인해보자.
my_func
는inner_func
와 같다고 했으니my_func()
의 결과도inner_func()
의 결과와 같을 것이라고 기대하고 실행해본 결과 진짜로 같았다.
my_func() # my_func를 호출
Hello
my_func = outer_func()
이니까 아래와 같이 실행해도 작동하지 않을까해서 해봤더니 진짜 된다.
outer_func()() # outer_func()의 출력은 inner_func이므로 이 명령은 inner_func()와 같다.
Hello
my_func
함수 자체를print
하여 객체 정보를 확인해보니outer_func.<locals>.inner_func
, 즉,outer_func
의locals
스코프에 있는inner_func
가my_func
에 할당되어 있는 것을 실제로 볼 수 있었다.
print(my_func) # my_func 함수 객체의 정보 확인
<function outer_func.<locals>.inner_func at 0x7fcee3202840>
- 여기서 한 가지 흥미로운 사실을 발견할 수 있다.
my_func
함수를 세 번 호출해보았다.
def outer_func(): #1
message = 'Hello' #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
Hello
Hello
Hello
outer_func
는 #2에서 호출된 뒤 #5에서 출력을 리턴하고 종료되었다.- 그런데
inner_func
와 같은 함수인my_func
는 호출되었을 때outer_func
함수의 로컬 영역에 있는message
라는 변수를 어떻게 참조하여print
한 것일까? - 이는
inner_func
가 클로저이기 때문에 가능한 것이며, 외부 함수가 가지고 있는 값을 저장하여 기억한다는 점이 바로 클로저의 특징이다.
- 또다른 예제를 통해 좀 더 자세히 이해해보자.
- 하나의 문자열을 받아서 html 태그의 형태로 바꿔주는 함수를 정의한 후 아래와 같이 호출해보았다.
def html_tag(tag): #1
tag_name = tag #3 #7
def make_tag(): #4 #8
print(f'<{tag_name}></{tag_name}>') #11 #13
return make_tag #5 #9
h1_tag = html_tag('h1') #2
span_tag = html_tag('span') #6
h1_tag() #10
span_tag() #12
<h1></h1>
<span></span>
- 실행 과정을 하나씩 살펴보면 아래와 같다.
- #1 :
html_tag
함수가 정의된다. - #2 :
html_tag
함수에'h1'
이라는 인자를 전달하여 호출하고 출력값을h1_tag
변수에 할당하기로 한다.html_tag
함수가 호출되었으므로 함수 내부의 명령이 실행된다. - #3 :
tag
매개변수에'h1'
인자가 할당되고 이는 다시html_tag
함수의 로컬변수인tag_name
변수에 할당된다. - #4 :
make_tag
함수가 정의된다. - #5 :
make_tag
함수 자체를html_tag
의 출력값으로 리턴한다. 아직까지make_tag
의 내부 명령은 실행되지 않았다. - #6 :
html_tag
함수에'span'
이라는 인자를 전달하여 호출하고 출력값을span_tag
변수에 할당하기로 한다. - #7~#9 : 이전과 같은 과정을 거쳐서
span_tag
변수에는'span'
을 인자로 받아 실행된html_tag
함수의 출력값인make_tag
함수 자체가 할당된다. - #10 : 드디어
h1_tag
함수가 호출된다.h1_tag
에는'h1'
을 인자로 받아 실행된 결과로 출력된make_tag
함수가 할당되어 있으므로,make_tag
함수의 내부 명령이 실행된다. - #11 :
tag_name
에'h1'
이 할당되어 있으므로<h1></h1>
이라는 값을print
하게 된다. - #12 :
span_tag
함수가 호출된다.span_tag
에는'span'
을 인자로 받아 실행된 결과로 출력된make_tag
함수가 할당되어 있으므로,make_tag
함수의 내부 명령이 실행된다. - #13 :
tag_name
에'span'
이 할당되어 있으므로<span></span>
이라는 값을print
하게 된다.
- 종합하면,
html_tag
의 내부 함수인make_tag
는 외부 함수html_tag
에 전달된 인자'h1'
과'span'
을 기억한 채로 각각h1_tag
와span_tag
변수에 호출되지 않은 함수 자체의 형태로 할당되었고,h1_tag()
와span_tag()
명령을 통해 호출되었을 때 전달받았던'h1'
과'span'
을 각각의 호출 결과에 적용해 출력하였다. - 이 때,
make_tag
함수는 클로저가 되며 외부에서 직접적으로 호출할 수 없다.
make_tag()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'make_tag' is not defined
클로저는 외부 함수로부터 받아온 값을 어디에 저장할까?
- 아래와 같은 함수에서 클로저인
inner_func
는outer_func
가 가진message
라는 프리변수를 어디에 저장해서 불러오는 것일까?
def outer_func():
message = 'Hello'
def inner_func():
print(message)
return inner_func
my_func = outer_func()
- 네임스페이스를 확인할 수 있는
dir()
명령을 통해my_func
를 해부 해보자.
dir(my_func)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
'__closure__'
라는 속성이 보인다. 수상하니까 열어보자.
my_func.__closure__
(<cell at 0x7f0308f0e318: str object at 0x7f0308f53c00>,)
(요소,)
의 형태로 나타난 것을 보니 튜플 자료형이고cell
이라는 문자열 객체를 가지고 있는 것 같다.- 여기서
cell
객체는 무엇일까? 검색해보았다.
Cell Object
“Cell” objects are used to implement variables referenced by multiple scopes. For each such variable, a cell object is created to store the value; the local variables of each stack frame that references the value contains a reference to the cells from outer scopes which also use that variable.
출처: docs.python.org
“셀”은 다수의 스코프에서 참조하는 변수를 구현하는데 사용되는 객체이다. 각 변수에 대해 그 값을 저장하는 셀 객체가 만들어지고 이 값들을 참조하는 각 단계의 로컬 변수들에는 같은 변수들을 사용하는 상위 단계 스코프의 셀에 대한 참조가 포함된다.
- 발 번역을 해봤는데… 뭔소리지 이게… 대충 여러 스코프에서 참조하는 변수의 값을 저장하는 공간이라는 것 같다. 모르겠으니까 해부 해보자.
dir(my_func.__closure__[0])
['__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']
cell_contents
라는__
로 감싸지지 않은 유일한 요소 하나가 보인다. 매우 수상하니까 호출해보자.
my_func.__closure__[0].cell_contents
Hello
outer_function
의 로컬 스코프에 있는 변수인message
에 할당된 값인'Hello'
가 출력되었다.- 자세히 이해는 되지 않지만 클로저 함수는 상위 함수가 가지는 로컬 변수들을 셀 객체를 만들어 저장하고 불러오는 것을 확인할 수 있었다.
- 더 자세한 이해를 했을 때 다시 작성해야겠다.
Reference
- 이한영 강사님 github: https://github.com/Fastcampus-WPS-6th/Python/blob/master/09.%20%ED%95%A8%EC%88%98.md
- School of Web: http://schoolofweb.net/blog/posts/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%ED%81%B4%EB%A1%9C%EC%A0%80-closure/
- Introducing Python, 빌 루바노빅, 한빛미디어, pp. 139~140