closure
에 대한 문법을 공부하면서 이해를 위해 python tutor
에 해당 코드를 입력하고 메모리 구조를 살펴봤는데 한가지 궁금한점이 생겼다.
def outer():
name = "hello"
def inner():
nonlocal name
name += "world"
return inner
outer()
원래 함수는 호출을 하게되면 해당 함수를 위한 namespace
가 생기고(Local Scope
) 호출이 종료되면 해당 namespace
는 메모리에서 garbage collector
에의해 삭제된다.
해당 namespace
를 참조하는 대상이 없기 때문이다.
근데 해당 코드를 python tutor
에 입력하면 inner
함수와name
함수에 대한 부분이 메모리에서 삭제되지 않는것 처럼
보이고 코드가 종료되어도 희미하게 남아있다.
closure
는자유변수
의 상태
를 저장한다고 하니, 위의 그림처럼 메모리 구조가 구성되어야 하는 것은 맞는 것 같다.
하지만 정말 그럴까? 라는 의문에서 메모리에서 해당 부분을 정말 삭제하지 않는지를 확인해보고 싶어졌다.
아래의 코드는 결론적으로 outer
함수스택에 존재하는 inner
라는 함수가 존재하는 namespace
를 통째로 삭제하고, inner
에 대한 정보역시 남아있지 않을것 같은데, A
가 inner
의 주소를 여전히 기억해주고 있는것으로 보아 메모리에서 내려가지 않는것으로 확인이된다..
해당 코드를 실행하면 전역스택
에 outer
이라는 함수의 주소와 , A
라는 변수가 push
될 것이다.
### local namespace - fuction
def outer():
def inner():
pass
print("enclosed scope에서 inner 함수에 할당 된 주소:",inner)
return inner
A = outer()
print("호출된 outer로 리턴된 inner의 주소:",A)
1) outer
변수를 호출했으므로 outer
를 위한 스택을 enclosed scope
에 만들고 해당 스택에 inner
의 주소를 push
할 것이다.
2) print
문이 실행되어 inner
의 주소가 출력된다.
3) 리턴 값이 inner
이므로 inner
가 호출된다. inner
를 위한 스택을 local scope
에 만들고 pass
이므로 바로 호출을 종료하며 inner
의 주소가 return
된다.
(함수는 일급 객체 이기 때문에 return 값으로 이용할 수 있다)
4) outer
가 종료되고 A
에는 inner
의 주소가 저장된다.
inner
의 주소 정보가 A
라는 객체에 저장되어 있다.outer
가 호출될때 해당 스코프에 저장한 inner 함수 객체의 주소
를 그대로 알고 있는것이다.... (놀라웠다.)local에 inner가 push될때 주소: <function outer.<locals>.inner at 0x00000289A489CF40>
호출된 outer로 리턴된 inner의 주소: <function outer.<locals>.inner at 0x00000289A489CF40>
또 다른 방법으로 gc.get_objects
가 있는데 이 객체에는 메모리에 올라가있는 모든 주소를 가지고 있다고 한다.
# 모든 객체를 순회하면서 주소와 매칭
for obj in gc.get_objects():
if id(obj) == id(A):
print("Found object at address:", obj)
break
else:
print("Object not found.")
어떻게보면 함수호출이 끝나도 계속 메모리를 차지하고 있는 단점이 존재하기는 하지만,
이러한 중첩함수의 위력 closure
을 이용할때 더욱 발휘된다.
closure
는 변수의 상태를 기억해준다.
inner함수가 enclosed scope
에 있는 변수에 어떤 값을 할당하는 코드이다.
### closure
def outer():
name="BOKJUNSOO"
def inner():
nonlocal name
name = name + "is me"
return inner
위의 조건은 closure
를 생성하기에 필요한 조건을 갖추었다.
local scope
에서 enclosed scope
의 변수를 참조할때outer
함수가 호출시 inner
함수를 리턴할때closure
에 outer
의 리턴값을 할당하고
해당 객체를 확인해보면 __closure__
이라는 속성을 확인했을때 리턴값이 존재한다.
>> closure = outer()
>> closure().__closure__
(<cell at 0x00000199C634A1D0: str object at 0x00000199C5FBAD30>,)
closure
를 계속호출하면서 확인해볼 수 있는데, 해당 type
을 보면 튜플로 확인이 된다.
>> closure()
>> closure().__closure__
(<cell at 0x00000199C634A1D0: str object at 0x00000199C6A36970>,)
해당 closure
는 튜플이므로 인덱스를 통해 type
을 확인해보면 cell
이라는 class
로 나타나있다.
>> print(type(closure().__closure__[0]))
<class 'cell'>
이를 이용하여 cell
의 속성을 확인해보면 cell_contents
라는 속성이 있는데,
해당 cell
과 mapping
된 str
을 확인해볼 수 있게 된다! (아주 꼭꼭 숨겨놨다)
>> closure().__closure__[0].cell_contents
"
closure
가 함수 호출이 종료되어도 해당 함수의namespace
에 존재하는 상태정보를 기억한다 "
라는 명제를 증명해보도록 하자. 이것이closure
의 위력이니까
정확한 확인을 위해 closure
을 다시 생성하고, 호출해보고 각각의 객체 주소
와 저장된 값
을 확인해보도록 하자
이를 확인해보기 위해서 outer
의 스택에 저장되는 name
변수의 주소
를 출력해보도록 함수를 조금 수정했다.
### closure
def outer():
name="BOKJUNSOO"
print(hex(id(name)))
def inner():
nonlocal name
name = name + "is me"
return inner
그리고 아래의 코드처럼 작성하여 확인이 조금 편하게 했다.
closure = outer()
print(f"closure 객체 생성시 : {closure.__closure__[0].cell_contents}가 저장되어 있는 주소 {closure.__closure__[0]}")
closure()
print(f"closure 객체로 호출시 : {closure.__closure__[0].cell_contents}가 저장되어 있는 주소 {closure.__closure__[0]}")
위 코드의 결과는 다음과 같고 증명하고자 했던 부분이 증명됐다.....(여기서 묵은 체증이 내려가는 느낌이었다)
0x199c697e6b0
closure 객체 생성시 : BOKJUNSOO가 저장되어 있는 주소 <cell at 0x00000199C64EF1F0: str object at 0x00000199C697E6B0>
closure 객체로 호출시 : BOKJUNSOO is me가 저장되어 있는 주소 <cell at 0x00000199C64EF1F0: str object at 0x00000199C69C5B70>
하나씩 살펴보면 다음과 같다.
outer
함수를 호출했을때 생성된 name
변수에 대한 주소
는 0x199c697e6b0
이다. 이는 cell
에 저장된 str
의 주소와 같다.즉, 호출이 종료되어도 해당 함수스택에 있던 변수에 대한 상태를 cell
이라는 객체에 저장하고 있다는 것이다
closure
를 계속 호출해도 cell
의 주소가 동일하다는 것이다.<cell at 0x00000199C64EF1F0: str object at 0x00000199C697E6B0>
<cell at 0x00000199C64EF1F0: str object at 0x00000199C69C5B70>
cell
의 주소가 0x00000199C64EF1F0
으로 동일하다.
하지만 closure
의 호출로 인해 outer
의 내용이 전부 실행되지 않지만 , inner
의 내용이 실행되어 원래 name
에 다른 값과 주소가 할당된 것을 확인할 수 있다.
즉 중첩함수
에서 확인한 것 처럼 closure
의 객체가 inner
함수에 접근하여 해당 부분만 실행시킬 수 있다는 것이다.
이는 cell
이라는 객체가 str
을 관리하는 것으로 보이고
이를통해 처음 outer
함수를 호출했을때 생성된 name
과 inner
에 대한 스택이 outer
호출이 종료되고 난 이후에도 메모리상 어디가에 존재한다는 것을 알 수 있었다.
조금 더 실험을 하기 위해 이번에는 outer
함수의 namespace
의 변수를 여러개 작성해본다.
def outer():
name="BOKJUNSOO"
name2="BOKHYUNSOO"
print("name의 주소 :",hex(id(name)).upper())
print("name2의주소 : ",hex(id(name2)).upper())
def inner():
nonlocal name, name2
name = name + " is me"
name2 = name2 + " my brother"
return inner
closure = outer()
print(f"closure 객체 생성시 : {closure.__closure__[0].cell_contents}이 저장되어 있는 주소 {closure.__closure__[0]}")
print(f"closure 객체 생성시 : {closure.__closure__[1].cell_contents}가 저장되어 있는 주소 {closure.__closure__[1]}")
closure()
print(f"closure 객체로 호출시 : {closure.__closure__[0].cell_contents}가 저장되어 있는 주소 {closure.__closure__[0]}")
print(f"closure 객체로 호출시 : {closure.__closure__[1].cell_contents}가 저장되어 있는 주소 {closure.__closure__[1]}")
name의 주소 : 0X199C697E6B0
name2의주소 : 0X199C69FB2B0
closure 객체 생성시 : BOKJUNSOO가 저장되어 있는 주소 <cell at 0x00000199C64AB760: str object at 0x00000199C697E6B0>
closure 객체 생성시 : BOKHYUNSOO가 저장되어 있는 주소 <cell at 0x00000199C64AB310: str object at 0x00000199C69FB2B0>
closure 객체로 호출시 : BOKJUNSOO is me가 저장되어 있는 주소 <cell at 0x00000199C64AB760: str object at 0x00000199C69DA0B0>
closure 객체로 호출시 : BOKHYUNSOO my brother가 저장되어 있는 주소 <cell at 0x00000199C64AB310: str object at 0x00000199C69D8A70>
예상한대로 name
과 name2
의 정보를 호출종료 이후에도 저장하고 있음을 확인할 수 있었다.
해당 결과를 통해 각각의 cell
을 이용해서 각각의 변수의 상태를 저장하고 있는 것을 확인할 수 있다.