파이썬 약한참조

Weak Reference

weakref 모듈은 파이썬 객체에 대해 약한 참조를 만들 수 있게 해준다.

파이썬에서 모든 값은 객체이고, 변수와 같이 그 객체를 지칭하는 이름은 모두 참조이다. 기본적으로 파이썬에서의 참조는 모두 강한참조로 만들어진다. 파이썬에서의 객체는 내부적으로 참조수(reference count)를 유지하고 있으며, 참조수가 0이되면 자동으로 가비지컬렉터에 의해 제거된다. 한번 확인해 볼까?

class Foo(object):
    def __init__(self):
        self.value = 1

    def __del__(self):
        print("object({}) is destroyed.".format(Foo.__class__))

>>> a = Foo()
>>> a = 1
object(<class 'type'>) is destroyed.

위 예에서 삭제될 때 메시지를 출력하는 클래스를 하나 작성하고 그 인스턴스를 만들어 a라는 이름으로 참조하게 했다. (주의할 것은 파이썬의 모든 변수는 참조이다. 값을 담든 변수는 없다고 봐야 한다.) 이제 이 a를 다른 객체를 가리키도록 변경했다. (파이썬의 1은 정수객체이다!) 그러자 처음에 생성되었던 객체는 아무도 자신을 참조해주지 않기에 자동으로 제거된다. 마지막 메시지는 한 때 a였던 객체의 마지막 유언인 셈이다.

탱글링

이런 강한 참조는 다음과 같은 문제를 낳게 된다. 두 객체가 서로를 강한 참조로 갖는 것이다. (이는 거의 모든 객체지향 프로그래밍 언어에서 범하기 쉬운 실수이다.)

>>> a = Foo()
>>> a.value =1
>>> b = Foo()
>>> b.value = 2
>>> a.ref = b
>>> b.ref = a
>>> del a
>>> del b 

두 객체를 삭제했지만 어떠한 ‘유언’도 출력되지 않았다. 처음에 만들어진 두 객체는 모두 다른 객체의 참조로서 참조가 살아있는 것이다. 이를 위해서는 1) 클래스 자체에서 외부 참조를 None으로 변경하거나, 객체를 삭제하기 전에 그 객체가 가지고 있는 다른 객체의 참조를 제거했어야 했다. 보다 바람직하게는 다음과 같은 디자인이 필요했다.

이런 함정을 피하기 위해 약한 참조가 필요하다.

참조를 지우면서 삭제하기

약한 참조를 만들기 이전에 다음과 같은 케이스는 어떻게 될까? Foo 객체가 삭제되는 시점에 다른 객체를 참조하고 있던 속성을 None으로 바꾼다면?

class Bar():
    def __init__(self, value=1):
        self.value = value
        self.ref = None
    def __del__(self):
        self.ref = None
        print("object({}) is destroyed.".format(self))

>>> c = Bar()
>>> d = Bar(2)
>>> c.ref = d
>>> d.ref = c
>>> del c
>>> del d

애석하게도 우리가 원하는대로 동작하지 않는다. 더군다나 이상한 것은

>>> c = Bar()
>>> d = Bar()
>>> c.ref = d
>>> d.ref = c
>>> del c.ref
>>> del c
>>> del d.ref
object(<__main__.Bar object at 0x02A37050>) is destroyed.> # c가 제거됨
>>> del d # d가 제거되지 않음 

과 같이 GC가 즉각 반응하지 못한다는 것이다. (d는 추후의 어느 시점–Bar가 새로운 인스턴스를 만들 때– 제거되기는 한다.)

약한참조

약한참조를 이용하면 이 문제를 피해나갈 수 있다.

import weakref
class Foo():
    def __init__(self, value):
        self.value = value
        self.__ref = None
    def __del__(self):
        print("object({}) is destroyed.".format(self))
    @property
    def ref(self):
        if self.__ref is None:
            return None
        else:
            return self.__ref()
    @ref.setter
    def ref(self, obj):
        self.__ref = weakref.ref(obj)

weakref.ref() 함수는 주어진 객체에 대한 약한 참조를 만들어낸다. 약한 참조를 통한 원객체를 얻을 때는 약한참조 자체를 함수처럼 호출한다.

>>> a = Foo()
>>> b = weakref.ref(a)
>>> b().value
1
>>> del a
object(<__main__.Foo object at 0x02A370B0>) is destroyed. 
>>> b()         # None

즉 약한참조는 원 객체에 대한 약한 참조 상태를 가지고 있는 객체이며, 실제 타깃 객체를 참조하는 순간에 일시적으로 강한참조로 변경된 다음, 해당 참조를 없앤다고 보면 된다. 따라서 위의 Foo에 대해서 탱글링되는 객체를 만들어보면

>>> a = Foo()
>>> b = Foo(2)
>>> a.ref = b
>>> b.ref = a
>>> a.ref.value
2
>>> b.ref.value
1
>>> del a
object(<__main__.Bar object at 0x02A370B0>) is destroyed.
>>> b.ref           # None
>>> del b
object(<__main__.Bar object at 0x02554090>) is destroyed.

이상과 같이 깔끔하게 동작하는 것을 알 수 있다.

언제 약한 참조를 쓰나

약한 참조는 위에서 살펴본바와 같이 두 개의 객체가 서로를 각각 참조하는 경우 탱글링을 방지하기 위해서 사용할수도 있지만, 그보다는 캐싱에 사용하면 효과적이다. 만약 많은 양의 이미지 바이너리데이터를 여러개 가지고 있고, 이를 다른 이름으로 사용한다고 할 때 사전에 이름을 키로 해서 데이터를 모아둘 수 있다.이미지가 많으면 그만큼 메모리에 부담을 주게 되고, 메모리 정리를 위해서는 개별적으로 사전의 원소를 제거해야 한다. 하지만 있어도 그만 없어도 그만인 캐시용도라면 굳이 사전이 끝까지 이 데이터들을 붙잡고 있을 필요가 없다. 이를 위해 weakref 모듈은 weakValueDictionary라는 타입을 제공해준다. 이는 사전과 동일하게 동작하지만 값으로 들어있는 요소들은 약한 참조만을 가지고 있어서 외부에서 모든 강한참조가 제거되면 가비지 컬렉터는 그냥 해당 메모리를 회수하고, 이 약한 참조는 None을 가리키도록 변경된다.

사전과 리스트의 약한참조

사전이나 리스트 그 자체에는 약한 참조를 줄 수 없다.

>>> a = [1, 2, 3]
>>> b = weakref.ref(a)
Traceback (most recent call last):
  File "<pyshell#79>", line 1, in <module>
    b = weakref.ref(a)
TypeError: cannot create weak reference to 'list' object

이를 우회하려면 해당 타입의 클래스를 상속하는 서브 클래스를 만들면 된다.

>>> class List(list):
        pass
>>> a = List([1, 2, 3])
>>> b = weakref.ref(a)
>>> len(b())
3