weakref — 약한 참조
weakref — 약한 참조 (Weak references)
weakref 모듈은 Python 프로그래머가 객체에 대한 약한 참조를 만들 수 있게 해 줘요. 아래에서 referent(피참조 객체)란 약한 참조가 가리키는 객체를 의미합니다.
출처: Python 표준 라이브러리
본문
weakref 모듈은 Python 프로그래머가 객체에 대한 약한 참조(weak reference)를 만들 수 있게 해 줍니다. 참조 대상 객체를 referent라고 부릅니다.
객체에 대한 약한 참조는 그 객체를 살아 있게 유지하기에 충분하지 않아요. referent에 대한 남은 참조가 약한 참조뿐이라면, 가비지 컬렉션은 referent를 파괴하고 그 메모리를 다른 용도로 재사용할 수 있습니다. 그러나 객체가 실제로 파괴되기 전까지는 강한 참조가 없어도 약한 참조가 객체를 반환할 수 있어요.
약한 참조의 주요 용도는 큰 객체를 담는 캐시나 매핑을 구현하는 것입니다. 큰 객체가 단지 캐시나 매핑에 나타난다는 이유만으로 살아 있게 유지하고 싶지 않을 때 유용하죠.
예를 들어 큰 바이너리 이미지 객체가 여러 개 있다면 각각에 이름을 연결하고 싶을 수 있어요. 이름을 이미지로(또는 이미지를 이름으로) 매핑하는데 Python 딕셔너리를 쓰면, 이미지 객체가 딕셔너리의 값이나 키로 나타난다는 이유만으로 계속 살아 있게 됩니다. weakref 모듈이 제공하는 WeakKeyDictionary와 WeakValueDictionary 클래스는 그 대안으로, 약한 참조를 사용해 객체가 매핑 객체에 나타난다는 이유만으로 살아 있지 않게 하는 매핑을 만듭니다. 예를 들어 이미지 객체가 WeakValueDictionary의 값이면, 그 이미지 객체에 대한 마지막 남은 참조가 약한 매핑이 쥔 약한 참조뿐일 때 가비지 컬렉션이 객체를 회수할 수 있고, 약한 매핑의 해당 항목은 그냥 삭제돼요.
WeakKeyDictionary와 WeakValueDictionary는 구현에서 약한 참조를 사용하며, 키나 값이 가비지 컬렉션에 회수될 때 약한 딕셔너리에 알리는 콜백 함수를 약한 참조에 설정합니다. WeakSet은 set 인터페이스를 구현하지만 WeakKeyDictionary처럼 요소에 대한 약한 참조를 유지합니다.
finalize는 객체가 가비지 컬렉션될 때 호출할 정리 함수를 등록하는 간단한 방법을 제공해요. 모듈이 파이널라이저가 객체가 수집될 때까지 살아 있도록 자동으로 보장하므로, 원시 약한 참조에 콜백 함수를 설정하는 것보다 쓰기 쉬운데요.
대부분의 프로그램은 이 약한 컨테이너 타입이나 finalize 중 하나를 쓰는 것으로 충분하다는 걸 알게 될 거예요. 보통 직접 약한 참조를 만들 필요는 없어요. 저수준 메커니즘은 고급 사용을 위해 weakref 모듈로 노출되어 있습니다.
모든 객체가 약한 참조될 수 있는 것은 아닙니다. 약한 참조를 지원하는 객체는 클래스 인스턴스, Python으로 작성된 함수(C는 아님), 인스턴스 메서드, set, frozenset, 일부 파일 객체, 제너레이터, 타입 객체, 소켓, 배열, deque, 정규식 패턴 객체, 코드 객체 등이에요.
버전 3.2에서 변경:
thread.lock,threading.Lock, 코드 객체 지원 추가.
list, dict 같은 여러 내장 타입은 직접 약한 참조를 지원하지 않지만 하위 클래스를 통해 지원을 추가할 수 있어요.
class Dict(dict):
pass
obj = Dict(red=1, green=2, blue=3) # this object is weak referenceable
CPython 구현 세부 사항 —
tuple과int같은 다른 내장 타입은 하위 클래스를 만들어도 약한 참조를 지원하지 않습니다.
확장 타입은 쉽게 약한 참조를 지원하게 만들 수 있어요. 주어진 타입에 __slots__이 정의되면 __slots__ 선언의 문자열 시퀀스에 '__weakref__' 문자열도 있지 않은 한 약한 참조 지원이 비활성화됩니다.
class weakref.ref(object[, callback])
object에 대한 약한 참조를 반환합니다. referent가 아직 살아 있으면 참조 객체를 호출해 원래 객체를 얻을 수 있고, referent가 더 이상 살아 있지 않으면 참조 객체를 호출하면 None이 반환돼요. callback이 제공되고 None이 아니며 반환된 weakref 객체가 아직 살아 있으면, 객체가 파이널라이즈되려 할 때 콜백이 호출됩니다. 약한 참조 객체가 콜백에 유일한 매개변수로 전달되고, referent는 더 이상 사용할 수 없어요.
같은 객체에 여러 약한 참조를 만드는 것은 허용됩니다. 각 약한 참조에 등록된 콜백은 가장 최근에 등록된 콜백부터 가장 오래된 순으로 호출됩니다.
콜백이 일으킨 예외는 표준 오류 출력에 기록되지만 전파될 수는 없습니다. 이는 객체의 __del__() 메서드에서 일어난 예외와 정확히 같은 방식으로 처리돼요.
약한 참조는 객체가 해시 가능하면 해시 가능합니다. 객체가 삭제된 후에도 해시 값을 유지해요. hash()가 객체 삭제 후에 처음 호출되면 TypeError를 일으킵니다.
약한 참조는 동등성(equality) 테스트는 지원하지만 순서(ordering)는 지원하지 않습니다. referent가 아직 살아 있으면 두 참조는 referent와 같은 동등 관계를 가져요 (콜백 무관). 어느 쪽 referent가 삭제됐으면 참조 객체가 같은 객체일 때만 참조들이 같습니다.
이것은 팩토리 함수가 아니라 하위 클래스화 가능한 타입입니다. 약한 참조는 참조하는 객체의 타입에 대해 제네릭입니다.
callback
이 읽기 전용 속성은 현재 weakref에 연결된 콜백을 반환합니다. 콜백이 없거나 weakref의 referent가 더 이상 살아 있지 않으면 이 속성의 값은 None이에요. (버전 3.4에서 변경: __callback__ 속성 추가)
weakref.proxy(object[, callback])
약한 참조를 사용하는 object의 프록시를 반환합니다. 이렇게 하면 약한 참조 객체에 필요한 명시적 역참조를 요구하는 대신 대부분의 콘텍스트에서 프록시를 쓸 수 있어요. 반환된 객체의 타입은 object가 호출 가능한지에 따라 ProxyType 또는 CallableProxyType 중 하나입니다. 프록시 객체는 referent와 무관하게 해시 가능하지 않습니다. 이는 본질적으로 변경 가능한 성질과 관련된 여러 문제를 피하고, 딕셔너리 키로 쓰이는 것을 막아요. callback은 ref() 함수의 같은 이름 매개변수와 동일합니다.
referent가 가비지 컬렉션된 후에 프록시 객체의 속성에 접근하면 ReferenceError가 발생합니다.
버전 3.8에서 변경: 프록시 객체의 연산자 지원에 행렬 곱셈 연산자
@와@=가 확장되었습니다.
weakref.getweakrefcount(object)
object를 가리키는 약한 참조와 프록시의 수를 반환합니다.
weakref.getweakrefs(object)
object를 가리키는 모든 약한 참조와 프록시 객체의 목록을 반환합니다.
class weakref.WeakKeyDictionary([dict])
키를 약하게 참조하는 매핑 클래스입니다. 키에 대한 강한 참조가 더 이상 없으면 딕셔너리의 항목이 버려져요. 애플리케이션의 다른 부분이 소유한 객체에, 그 객체에 속성을 추가하지 않고 추가 데이터를 연결하는 데 쓸 수 있습니다. 특히 속성 접근을 오버라이드하는 객체에 유용해요.
기존 키와 같은 값의 키(같은 동일성은 아님)가 딕셔너리에 삽입되면 값을 대체하지만 기존 키는 대체하지 않는다는 점을 알아 두세요. 이 때문에 원래 키에 대한 참조가 삭제되면 딕셔너리의 항목도 함께 삭제됩니다.
>>> class T(str): pass
...
>>> k1, k2 = T(), T()
>>> d = weakref.WeakKeyDictionary()
>>> d[k1] = 1 # d = {k1: 1}
>>> d[k2] = 2 # d = {k1: 2}
>>> del k1 # d = {}
해결 방법은 재할당 전에 키를 제거하는 것입니다.
>>> class T(str): pass
...
>>> k1, k2 = T(), T()
>>> d = weakref.WeakKeyDictionary()
>>> d[k1] = 1 # d = {k1: 1}
>>> del d[k1]
>>> d[k2] = 2 # d = {k2: 2}
>>> del k1 # d = {k2: 2}
버전 3.9에서 변경: PEP 584에 명시된
|와|=연산자 지원 추가.
WeakKeyDictionary 객체에는 내부 참조를 직접 노출하는 추가 메서드가 있어요. 참조는 사용 시점에 "살아" 있음이 보장되지 않으므로, 참조를 호출한 결과를 사용 전에 확인해야 합니다. 이는 가비지 컬렉터가 키를 필요보다 오래 붙들게 하는 참조를 만드는 것을 피하는 데 쓸 수 있어요.
WeakKeyDictionary.keyrefs()
키에 대한 약한 참조의 반복 가능한 객체를 반환합니다.
class weakref.WeakValueDictionary([dict])
값을 약하게 참조하는 매핑 클래스입니다. 값에 대한 강한 참조가 더 이상 없으면 딕셔너리의 항목이 버려져요.
버전 3.9에서 변경: PEP 584에 명시된
|와|=연산자 지원 추가.
WeakValueDictionary 객체에는 WeakKeyDictionary.keyrefs() 메서드와 같은 문제가 있는 추가 메서드가 있어요.
WeakValueDictionary.valuerefs()
값에 대한 약한 참조의 반복 가능한 객체를 반환합니다.
class weakref.WeakSet([elements])
요소에 대한 약한 참조를 유지하는 Set 클래스입니다. 요소에 대한 강한 참조가 더 이상 없으면 그 요소가 버려져요.
class weakref.WeakMethod(method[, callback])
바인딩된 메서드(클래스에 정의되고 인스턴스에서 조회된 메서드)에 대한 약한 참조를 시뮬레이션하는 커스텀 ref 하위 클래스입니다. 바인딩된 메서드는 일시적(ephemeral)이라 표준 약한 참조가 그것을 붙들 수 없어요. WeakMethod는 객체나 원래 함수가 죽을 때까지 바인딩된 메서드를 재생성하는 특별한 코드를 가집니다.
>>> class C:
... def method(self):
... print("method called!")
...
>>> c = C()
>>> r = weakref.ref(c.method)
>>> r()
>>> r = weakref.WeakMethod(c.method)
>>> r()
<bound method C.method of <__main__.C object at 0x7fc859830220>>
>>> r()()
method called!
>>> del c
>>> gc.collect()
0
>>> r()
>>>
callback은 ref() 함수의 같은 이름 매개변수와 동일합니다. (버전 3.4에서 추가)
class weakref.finalize(obj, func, /, *args, **kwargs)
obj가 가비지 컬렉션될 때 호출될 호출 가능한 파이널라이저 객체를 반환합니다. 일반 약한 참조와 달리 파이널라이저는 참조 객체가 수집될 때까지 항상 살아 있어서, 수명 주기 관리를 크게 단순화해요.
파이널라이저는 (명시적으로든 가비지 컬렉션에서든) 호출될 때까지 살아 있는 것으로 간주되고, 그 후에는 죽은 것으로 간주됩니다. 살아 있는 파이널라이저를 호출하면 func(*arg, **kwargs)를 평가한 결과를 반환하고, 죽은 파이널라이저를 호출하면 None을 반환해요.
가비지 컬렉션 중 파이널라이저 콜백이 일으킨 예외는 표준 오류 출력에 표시되지만 전파될 수는 없습니다. 객체의 __del__() 메서드나 약한 참조의 콜백에서 일어난 예외와 같은 방식으로 처리돼요.
프로그램이 종료될 때(더 일반적으로 인터프리터 종료 시) 각 남아 있는 살아 있는 파이널라이저는 atexit 속성이 거짓으로 설정되지 않았다면 호출됩니다. 생성 역순으로 호출돼요. 파이널라이저는 모듈 전역이 None으로 대체됐을 가능성이 있는 인터프리터 종료의 후반부에는 콜백을 절대 호출하지 않습니다.
call()
self가 살아 있으면 죽은 것으로 표시하고 func(*args, **kwargs)를 호출한 결과를 반환합니다. self가 죽어 있으면 None을 반환해요.
detach()
self가 살아 있으면 죽은 것으로 표시하고 (obj, func, args, kwargs) 튜플을 반환합니다. self가 죽어 있으면 None을 반환합니다.
peek()
self가 살아 있으면 (obj, func, args, kwargs) 튜플을 반환합니다. self가 죽어 있으면 None을 반환해요.
alive
파이널라이저가 살아 있으면 참, 아니면 거짓인 프로퍼티입니다.
atexit
기본적으로 참인 쓰기 가능한 불리언 프로퍼티입니다. 인터프리터 종료 시 atexit이 참인 모든 남은 살아 있는 파이널라이저가 생성 역순으로 호출됩니다.
참고 —
func,args,kwargs가 직접이든 간접이든obj에 대한 참조를 소유하지 않도록 하는 것이 중요합니다. 그렇지 않으면obj가 절대 가비지 컬렉션되지 않으니까요. 특히func는obj의 바인딩된 메서드가 아니어야 합니다.
버전 3.4에서 추가.
class weakref.ReferenceType
약한 참조 객체의 타입 객체입니다.
class weakref.ProxyType
호출 가능하지 않은 객체의 프록시 타입 객체입니다.
class weakref.CallableProxyType
호출 가능한 객체의 프록시 타입 객체입니다.
weakref.ProxyTypes
프록시에 대한 모든 타입 객체를 담은 시퀀스입니다. 두 프록시 타입 이름을 모두 명시하지 않고 객체가 프록시인지 테스트하기 쉽게 해 줍니다.
관련 자료 — PEP 205 (Weak References)는 이 기능의 제안·근거이며, 초기 구현 링크와 다른 언어의 유사 기능 정보를 포함합니다.
약한 참조 객체 (Weak Reference Objects)
약한 참조 객체는 ref.__callback__ 외에는 메서드나 속성이 없어요. 약한 참조 객체는 referent가 아직 존재하면 그것을 호출해 얻을 수 있습니다.
>>> import weakref
>>> class Object:
... pass
...
>>> o = Object()
>>> r = weakref.ref(o)
>>> o2 = r()
>>> o is o2
True
referent가 더 이상 존재하지 않으면 참조 객체를 호출해도 None을 반환합니다.
>>> del o, o2
>>> print(r())
None
약한 참조 객체가 아직 살아 있는지 테스트하려면 ref() is not None 표현식을 써야 합니다. 보통 참조 객체를 사용해야 하는 애플리케이션 코드는 이 패턴을 따라야 해요.
# r is a weak reference object
o = r()
if o is None:
# referent has been garbage collected
print("Object has been deallocated; can't frobnicate.")
else:
print("Object is still live!")
o.do_something_useful()
"살아 있음"에 대한 별도 테스트를 쓰면 스레드 애플리케이션에서 경쟁 조건을 만듭니다. 다른 스레드가 약한 참조를 호출하기 전에 약한 참조를 무효화할 수 있기 때문이에요. 위에 보인 관용구는 스레드 애플리케이션과 단일 스레드 애플리케이션 모두에서 안전합니다.
ref 객체의 특수한 버전은 하위 클래스화로 만들 수 있습니다. WeakValueDictionary의 구현에서 매핑의 각 항목에 대한 메모리 오버헤드를 줄이는 데 사용됩니다. 이는 참조에 추가 정보를 연결하는 데 가장 유용하지만, referent를 가져오는 호출에 추가 처리를 삽입하는 데도 쓸 수 있어요.
이 예는 ref의 하위 클래스가 객체에 대한 추가 정보를 저장하고 referent에 접근할 때 반환 값을 바꾸는 방법을 보여 줍니다.
import weakref
class ExtendedRef(weakref.ref):
def __init__(self, ob, callback=None, /, **annotations):
super().__init__(ob, callback)
self.__counter = 0
for k, v in annotations.items():
setattr(self, k, v)
def __call__(self):
"""Return a pair containing the referent and the number of
times the reference has been called.
"""
ob = super().__call__()
if ob is not None:
self.__counter += 1
ob = (ob, self.__counter)
return ob
예제 (Example)
이 간단한 예는 애플리케이션이 객체 ID로 이전에 본 객체를 가져올 수 있는 방법을 보여 줍니다. 객체의 ID를 다른 데이터 구조에서 사용할 수 있으면서도 객체를 강제로 살아 있게 하지 않고, 객체가 있으면 ID로 여전히 가져올 수 있어요.
import weakref
_id2obj_dict = weakref.WeakValueDictionary()
def remember(obj):
oid = id(obj)
_id2obj_dict[oid] = obj
return oid
def id2obj(oid):
return _id2obj_dict[oid]
파이널라이저 객체 (Finalizer Objects)
finalize를 쓰는 주요 이점은 반환된 파이널라이저 객체를 보존할 필요 없이 콜백을 등록하기 쉽다는 것입니다. 예를 들어
>>> import weakref
>>> class Object:
... pass
...
>>> kenny = Object()
>>> weakref.finalize(kenny, print, "You killed Kenny!")
<finalize object at ...; for 'Object' at ...>
>>> del kenny
You killed Kenny!
파이널라이저는 직접 호출할 수도 있습니다. 그러나 파이널라이저는 콜백을 기껏해야 한 번 호출해요.
>>> def callback(x, y, z):
... print("CALLBACK")
... return x + y + z
...
>>> obj = Object()
>>> f = weakref.finalize(obj, callback, 1, 2, z=3)
>>> assert f.alive
>>> assert f() == 6
CALLBACK
>>> assert not f.alive
>>> f() # callback not called because finalizer dead
>>> del obj # callback not called because finalizer dead
파이널라이저는 detach() 메서드로 등록을 해제할 수 있어요. 이는 파이널라이저를 죽이고 생성자에 넘긴 인자를 반환합니다.
>>> obj = Object()
>>> f = weakref.finalize(obj, callback, 1, 2, z=3)
>>> f.detach()
(<...Object object ...>, <function callback ...>, (1, 2), {'z': 3})
>>> newobj, func, args, kwargs = _
>>> assert not f.alive
>>> assert newobj is obj
>>> assert func(*args, **kwargs) == 6
CALLBACK
atexit 속성을 False로 설정하지 않으면, 파이널라이저가 아직 살아 있을 때 프로그램 종료 시 호출됩니다. 예를 들어
>>> obj = Object()
>>> weakref.finalize(obj, print, "obj dead or exiting")
<finalize object at ...; for 'Object' at ...>
>>> exit()
obj dead or exiting
파이널라이저를 del() 메서드와 비교하기 (Comparing finalizers with del())
인스턴스가 임시 디렉터리를 나타내는 클래스를 만들고 싶다고 해 볼게요. 디렉터리는 다음 사건 중 첫 번째가 발생할 때 내용과 함께 삭제되어야 합니다.
- 객체가 가비지 컬렉션됨,
- 객체의
remove()메서드가 호출됨, - 프로그램이 종료됨.
__del__() 메서드로 이 클래스를 이렇게 구현하려 시도할 수 있어요.
class TempDir:
def __init__(self):
self.name = tempfile.mkdtemp()
def remove(self):
if self.name is not None:
shutil.rmtree(self.name)
self.name = None
@property
def removed(self):
return self.name is None
def __del__(self):
self.remove()
Python 3.4부터 __del__() 메서드는 더 이상 참조 순환이 가비지 컬렉션되는 것을 막지 않고, 모듈 전역도 인터프리터 종료 중에 더 이상 None으로 강제되지 않습니다. 그래서 이 코드는 CPython에서 아무 문제 없이 동작해야 해요.
그러나 __del__() 메서드의 처리는 인터프리터의 가비지 컬렉터 구현 내부 세부 사항에 의존하므로 악명 높게 구현 특정적(implementation specific)입니다.
더 견고한 대안은 객체의 전체 상태에 접근하는 대신 필요한 특정 함수·객체만 참조하는 파이널라이저를 정의하는 것입니다.
class TempDir:
def __init__(self):
self.name = tempfile.mkdtemp()
self._finalizer = weakref.finalize(self, shutil.rmtree, self.name)
def remove(self):
self._finalizer()
@property
def removed(self):
return not self._finalizer.alive
이렇게 정의하면 우리 파이널라이저는 디렉터리를 적절히 정리하는 데 필요한 세부 사항에만 참조를 받아요. 객체가 절대 가비지 컬렉션되지 않아도 파이널라이저는 종료 시 여전히 호출됩니다.
weakref 기반 파이널라이저의 다른 장점은 정의가 제3자에 의해 제어되는 클래스에 대한 파이널라이저를 등록하는 데 쓸 수 있다는 것입니다. 예를 들어 모듈이 언로드될 때 코드를 실행하는 경우가 있죠.
import weakref, sys
def unloading_module():
# implicit reference to the module globals from the function body
weakref.finalize(sys.modules[__name__], unloading_module)
참고 — 프로그램이 종료되는 바로 그때 데몬 스레드에서 파이널라이저 객체를 만들면 파이널라이저가 종료 시 호출되지 않을 가능성이 있습니다. 그러나 데몬 스레드에서는
atexit.register(),try: ... finally: ...,with: ...도 정리가 일어난다고 보장하지 않습니다.