클로저를 이해하기 위해서는 일급 함수, 일급 객체에 대한 이해가 필요하다.
만약 일급함수, 일급객체를 모른다면 아래 포스팅이 도움이 될 수 있다.
클로저란?
클로저는 외부 함수에 접근할 수 있는 내부 함수 혹은 이러한 원리를 칭하는 용어이다.
외부 함수는 외부 함수 자신의 지역변수를 사용하는 내부함수가 소멸될때까지 소멸되지 않는다.
예를 들어 임의의 A함수 내부에 다른 B함수가 있다면 B함수는 A함수의 지역변수를 사용할 수 있지만 그 반대는 불가능하다.
1. 클로저 실행 순서
def add(x, y): #1
return x + y
def create_calc(func, x): #2
offset = 20 #4
def calc(y): #5
y += offset #7
return func(x, y) #8
return calc #6
newadder = create_calc(add, 15) #3
# result = 55
print(newadder(20)) #9
- add 함수가 정의된다.
- create_calc 함수가 정의된다.
- newadder 변수에 create_calc 함수를 할당한다.
- create_calc 함수 내부의 지역변수 offset 변수가 정의된다.
- calc 함수를 정의한다.
- calc 를 리턴한다.
- calc가 받은 매개변수(지역변수가 됨)의 값을 offset만큼 더한다.
- func를 리턴한다.
- newadder(20)의 값을 print 한다.
#3, #7, #8에서 에서 아주 중요한 클로저의 특징이 드러난다.
함수는 실행이 될 때 stack메모리 상에 메모리를 할당 받고, 실행이 종료되면 메모리를 반환하는 구조이다.
따라서 다음과 같은 의문점이 생길 수 있다.
1. #3에서 create_calc함수가 할당됨과 동시에 create_calc함수를 사용했기 때문에, create_calc함수는 소멸되어야 하지 않는가?
2. create_calc함수가 소멸된다면 newadder를 실행할 때 calc 함수가 실행 될 것이고, calc함수에서 사용하는 offset과 func는 사용할 수 없을 것이다. 그렇다면 newadder는 어떻게 실행되는가?
파이썬은 일급 함수를 지원하는 언어이기 때문에 create_calc 함수를 '클로저'로 인식하고 특별한 동작을 할 수 있게 도와준다.
따라서 아래 클로저 메모리 구조를 살펴보고 어떻게 create_calc 함수 내부의 지역변수들이 살아있을 수 있는지 알아보자
2. 클로저 메모리 구조 파헤치기
def add(x, y):
return x + y
def mul(x, y):
return x * y
def create_calc(func, x):
offset = 20
def calc(y):
y += offset
return func(x, y)
return calc
newadder = create_calc(add, 15)
newmuler = create_calc(mul, 10)
print("location of add() :", add)
print("location of mul() :", mul)
print("location of a create_calc :", create_calc)
print("location of newadder :", newadder)
print("location of newmuler :", newmuler)
print()
print(dir(create_calc))
print()
print(newadder.__closure__)
print()
print(dir(newadder.__closure__[0]))
print()
print(newadder.__closure__[0].cell_contents)
print()
print(newmuler.__closure__)
print()
print(newmuler.__closure__[0].cell_contents)
print()
result:
location of add() : <function add at 0x7fd4e80cef70>
location of mul() : <function mul at 0x7fd4d8208040>
location of a create_calc : <function create_calc at 0x7fd4d82080d0>
location of newadder : <function create_calc.<locals>.calc at 0x7fd4d8208160>
location of newmuler : <function create_calc.<locals>.calc at 0x7fd4d82081f0>
['__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__']
newadder closure : (<cell at 0x7fd4d8206fd0: function object at 0x7fd4e80cef70>, <cell at 0x7fd4d8206fa0: int object at 0x7fd4e802eb90>, <cell at 0x7fd4d8206f70: int object at 0x7fd4e802eaf0>)
['__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']
<function add at 0x7fd4e80cef70>
newmuler closure : (<cell at 0x7fd4d8206e50: function object at 0x7fd4d8208040>, <cell at 0x7fd4d8206df0: int object at 0x7fd4e802eb90>, <cell at 0x7fd4d8206dc0: int object at 0x7fd4e802ea50>)
<function mul at 0x7fd4d8208040>
newadder closure는 tuple 형식으로 되어있음을 확인할 수 있다.
newadder closure를 살펴보자
create_calc에는 지역변수라고 할 수 있는게 3가지가 있다.
- offset
- func
- y
따라서 closure tuple에는 3개의 변수가 담겨있는것을 newadder closure에서 확인할 수 있다.
[0] : <cell at 0x7fd4d8206fd0: function object at 0x7fd4e80cef70>
- 메모리의 0x7fd4d8206fd0 부분에 위치한 클로저 cell은 0x7fd4e80cef70에 위치하는 function object를 참조한다.
즉 add 함수를 참조한다는 뜻이다. 이는 add 함수의 location과 비교해 보면 알 수 있다.
[1] : <cell at 0x7fd4d8206fa0: int object at 0x7fd4e802eb90>
- offset 변수가 저장된 위치를 참조하는 cell이 지정되었다
[2] : <cell at 0x7fd4d8206f70: int object at 0x7fd4e802eaf0>
- y 변수가 저장된 위치를 참조하는 cell이 지정되었다.
그리고 closure의 dir()을 보면 'cell_contents'라는 명령어가 있다. 이 명령어는 클로저가 리턴하는 값을 리턴한다.
newadder의 0번째 cell의 cell_contents를 살펴보면 <function add at 0x7fd4e80cef70> 라고 적혀있다.
이는 add 함수를 의미하는 것으로, calc 함수 내부의 리턴값과 동일함을 확인할 수 있다.
클로저 cell이라는 개념을 통해서 외부 함수가 어떻게 내부함수가 종료될 때까지 자신의 지역변수값을 유지할 수 있는지 이해할 수 있었다.
'Lang > Python' 카테고리의 다른 글
[Python] Decorator (데코레이터) (0) | 2021.12.03 |
---|---|
[Python] First-Class Function (일급 함수) (2) | 2021.12.02 |
Tkinter 로 계산기 만들기 (0) | 2020.06.18 |