Closure

복준수·2025년 1월 2일
0

Python

목록 보기
4/5
post-thumbnail

closure에 대한 탐구

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에 대한 정보역시 남아있지 않을것 같은데, Ainner의 주소를 여전히 기억해주고 있는것으로 보아 메모리에서 내려가지 않는것으로 확인이된다..


해당 코드를 실행하면 전역스택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는 변수의 상태를 기억해준다.

Closure

closure 속성

inner함수가 enclosed scope에 있는 변수에 어떤 값을 할당하는 코드이다.

### closure
def outer():
    name="BOKJUNSOO"
    def inner():
        nonlocal name
        name = name + "is me"
    return inner

위의 조건은 closure를 생성하기에 필요한 조건을 갖추었다.

  1. 중첩함수로 작성되어 있을때
  2. local scope에서 enclosed scope의 변수를 참조할때
  3. outer 함수가 호출시 inner함수를 리턴할때

closureouter의 리턴값을 할당하고
해당 객체를 확인해보면 __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라는 속성이 있는데,
해당 cellmappingstr을 확인해볼 수 있게 된다! (아주 꼭꼭 숨겨놨다)

>> closure().__closure__[0].cell_contents

closure의 위력!

"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>

하나씩 살펴보면 다음과 같다.

  1. outer 함수를 호출했을때 생성된 name변수에 대한 주소0x199c697e6b0이다. 이는 cell에 저장된 str의 주소와 같다.

    즉, 호출이 종료되어도 해당 함수스택에 있던 변수에 대한 상태를 cell이라는 객체에 저장하고 있다는 것이다

  2. 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함수를 호출했을때 생성된 nameinner에 대한 스택이 outer 호출이 종료되고 난 이후에도 메모리상 어디가에 존재한다는 것을 알 수 있었다.

cell 하나당 변수하나

조금 더 실험을 하기 위해 이번에는 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>

예상한대로 namename2의 정보를 호출종료 이후에도 저장하고 있음을 확인할 수 있었다.
해당 결과를 통해 각각의 cell을 이용해서 각각의 변수의 상태를 저장하고 있는 것을 확인할 수 있다.

0개의 댓글