[Final] PEP 205 - Weak References
원문 링크: PEP 205 - Weak References
상태: Final 유형: Standards Track 작성일: 14-Jul-2000
PEP 205 – Weak References
- 작성자: Fred L. Drake, Jr.
- 상태: Final (최종)
- 유형: Standards Track
- 생성일: 2000년 7월 14일
- Python 버전: 2.1
개요
PEP 205는 Python에 “약한 참조(Weak References)”를 도입하는 것을 제안합니다. 약한 참조는 객체의 참조 카운트를 증가시키지 않고 객체를 참조하는 메커니즘으로, 주로 객체 캐시 관리와 순환 참조(circular references) 문제 해결에 사용됩니다. 이 PEP는 약한 참조의 필요성, 가능한 구현 방법, 그리고 weakref
모듈을 통한 weakref.ref()
, weakref.mapping()
, weakref.proxy()
등의 구체적인 API를 제시합니다.
동기 (Motivation)
Python 프로그래머들이 약한 참조를 필요로 하는 두 가지 주요 응용 분야는 객체 캐시와 순환 참조로 인한 어려움 감소입니다.
캐시 (Weak Dictionaries)
외부 상태를 나타내는 객체를 유지해야 할 때, 단일 인스턴스를 외부 현실에 매핑하고, 여러 인스턴스가 동일한 외부 리소스에 매핑되는 것을 방지하여 동기화 유지가 불필요하게 어려워지는 것을 막아야 합니다. 이러한 경우, 인스턴스 캐시를 지원하는 것이 일반적인 관용구이며, 팩토리 함수를 사용하여 새 인스턴스 또는 기존 인스턴스를 반환합니다.
이러한 접근 방식의 어려움은 두 가지 중 하나를 감수해야 한다는 점입니다.
- 캐시가 무한히 커짐: 장기 실행 프로세스나 상당한 메모리 요구 사항이 있는 비교적 짧은 프로세스에는 적합하지 않습니다.
- 캐시의 명시적 관리: 애플리케이션의 다른 곳에서 캐시를 명시적으로 관리해야 하며, 이는 지루하고 불필요한 코드를 추가하게 됩니다.
약한 참조는 다음과 같은 경우에 유용합니다:
- 여러 내부 사용자가 있더라도 단일 인스턴스로 표현되어야 하는 외부 객체 (예: 디스크에 통째로 다시 기록되어야 하는 파일).
- 생성 비용이 비싸지만 여러 내부 소비자가 필요할 수 있는 객체.
순환 참조 (Circular References)
DOM (Document Object Model)은 부모 및 문서 노드에 대한 막대한 양의 순환 참조를 요구하지만, 각 노드에서 해당 부모로 매핑되는 약한 딕셔너리를 사용하여 이러한 참조를 제거할 수 있습니다. 이는 xml.dom.pulldom
과 같은 컨텍스트에서 특히 유용하며, .unlink()
작업이 No-Op(아무것도 하지 않는 작업)이 되도록 할 수 있습니다.
해결 공간의 측면 (Aspects of the Solution Space)
약한 참조 문제에는 두 가지 뚜렷한 측면이 있습니다.
- 약한 참조의 무효화 (Invalidation): 참조 대상 객체가 더 이상 존재하지 않을 때 약한 참조를 어떻게 무효화할 것인가?
- 과거 접근 방식: 강한 참조를 저장하고 모든 약한 참조 객체를 검사하여, 참조 카운트가 1이 될 때(약한 참조가 마지막 남은 참조임을 나타냄) 무효화합니다. 이 방식은 Python의 메모리 관리 메커니즘을 변경할 필요가 없다는 장점이 있지만, 약한 참조 객체 수에 O(N) 시간이 걸리는 스캔 작업이 필요하며, 라이브러리 코드에는 덜 매력적입니다.
- 대체 접근 방식: 객체 할당 해제 코드(de-allocation code)가 약한 참조의 가능성을 인지하고, 객체가 할당 해제될 때마다 약한 참조 관리 코드를 호출하여 무효화를 처리합니다. 이는
tp_dealloc
핸들러의 변경과 객체에서 해당 약한 참조 체인으로의 효율적인 매핑이 필요합니다.
- Python 코드에 약한 참조를 제시하는 방법 (Presentation): Python 계층에 약한 참조를 어떻게 노출할 것인가?
- 명시적 참조 객체 (Explicit Reference Objects): 내부 객체에 대한 유효한 참조를 검색하기 위해 특정 작업이 필요한 명시적 참조 객체입니다. 객체 관리 계층을 추가할 때 작업하기 쉽습니다.
- 프록시 객체 (Proxy Objects): 가능한 한 원래 객체처럼 가장하는 프록시 객체입니다. 많은 사용자가 약한 참조가 원본 객체와 매우 유사하게 보이기 때문에 프록시 접근 방식을 선호합니다.
제안된 해결책 (Proposed Solution)
약한 참조는 상당한 메모리 크기를 가지거나(직접 또는 간접적으로) 외부 리소스(데이터베이스 연결, 열린 파일 등)에 대한 참조를 보유하는 모든 Python 객체를 가리킬 수 있어야 합니다.
새로운 모듈인 weakref
는 약한 참조를 생성하는 데 사용되는 새 함수를 포함할 것입니다.
weakref.ref()
: “약한 참조 객체(weak reference object)”를 생성하며, 선택적으로 객체가 최종화(finalized)되기 직전에 호출될 콜백을 첨부할 수 있습니다.- 약한 참조 객체는 참조된 객체가 회수되지 않았다면 접근을 허용하고, 객체가 여전히 메모리에 존재하는지 확인할 수 있습니다.
- 참조된 객체를 검색하려면 참조 객체를 호출합니다. 참조된 객체가 더 이상 존재하지 않으면
None
을 반환합니다.
weakref.WeakKeyDictionary()
(원문에서는weakref.mapping()
으로 언급되었으나, 최종적으로WeakKeyDictionary
와WeakValueDictionary
로 구현): “약한 딕셔너리(weak dictionary)”를 생성합니다. 이는 임의의 키를 값에 매핑하지만, 값에 대한 강한 참조를 소유하지 않습니다. 값이 최종화되면 해당 (키, 값) 쌍이 모든 매핑에서 제거됩니다.weakref.proxy()
: 원본 객체처럼 최대한 동작하는 프록시 객체를 생성합니다.- 프록시 객체는 해시 불가능(not hashable)합니다. 참조 대상 객체가 사라진 후에는 비교가 불가능하므로 딕셔너리 키로 사용할 수 없습니다.
- 참조 대상 객체가 사라진 후 프록시 객체에 대한 연산은 대부분
weakref.ReferenceError
를 발생시킵니다. is
비교,type()
,id()
는 계속 작동하지만, 항상 프록시 자체를 참조하며 참조 대상 객체를 참조하지 않습니다.
약한 참조에 등록된 콜백은 단일 매개변수를 받아야 하며, 이는 약한 참조 또는 프록시 객체 자체입니다. 콜백 내에서는 객체에 접근하거나 부활시킬 수 없습니다.
구현 전략 (Implementation Strategy)
약한 참조의 구현에는 각 약한 참조 가능 객체(weakly-referencable object)에 대해 지워져야 하는 참조 컨테이너 목록이 포함됩니다. 참조가 약한 딕셔너리에서 온 경우, 딕셔너리 항목이 먼저 지워집니다. 그런 다음, 관련 콜백이 객체를 매개변수로 전달받아 호출됩니다. 모든 콜백이 호출되면 객체는 최종화되고 할당 해제됩니다.
많은 내장 타입이 약한 참조 관리에 참여하며, 모든 확장 타입도 참여할 수 있습니다. 타입 구조는 인스턴스 구조 내에 약한 참조 구조 목록을 포함하는 오프셋을 제공하는 추가 필드를 포함할 것입니다. 이 필드의 값이 0보다 작거나 같으면 객체는 참여하지 않습니다. 이 경우 weakref.ref()
, <weakdict>.__setitem__()
및 .setdefault()
, 그리고 항목 할당은 TypeError
를 발생시킵니다. 필드의 값이 0보다 크면 새 약한 참조가 생성되어 목록에 추가될 수 있습니다.
이 접근 방식은 숫자 또는 기타 작은 타입에 메모리 부담을 주지 않고 임의의 확장 타입이 참여할 수 있도록 합니다.
약한 참조를 지원하는 표준 타입에는 인스턴스, 함수, 바운드(bound) 및 언바운드(unbound) 메서드가 포함됩니다. Python 2.2에서 클래스 타입(“new-style classes”)이 추가되면서, 타입도 약한 참조를 지원하게 되었습니다. 클래스 타입의 인스턴스는 약한 참조 가능 기본 타입이 있거나, 클래스가 __slots__
를 지정하지 않거나, __weakref__
라는 슬롯이 지정된 경우 약한 참조가 가능합니다. 제너레이터(Generators)도 약한 참조를 지원합니다.
가능한 응용 분야 (Possible Applications)
- PyGTK+ 바인딩
- Tkinter: 위젯에서 부모로의 약한 참조를 사용하여 순환 참조를 피할 수 있습니다. 일반적인 경우 객체가 더 빨리 폐기되지는 않지만, 프로그래머가 참조를 해제하기 전에
.destroy()
를 호출하는 것에 대한 의존성이 줄어듭니다. 이는 주로 장기 실행 애플리케이션에 도움이 될 것입니다. - DOM 트리
Python의 이전 약한 참조 작업 (Previous Weak Reference Work in Python)
- Dianne Hackborn의 “가상 참조(virtual references)”: Java의
java.lang.ref.WeakReference
와 매우 유사하지만, 무효화 큐(invalidation queues)에 해당하는 기능은 없었습니다. 이 정보는 웹에서 사라졌지만, 이 PEP의 부록으로 포함되어 있습니다. - Marc-André Lemburg의
mx.Proxy
패키지: - Dieter Maurer의
weakdict
모듈: C와 Python으로 구현되었으며, Python 1.5.2a 이후 업데이트되지 않았습니다. - Alex Shindich의
PyWeakReference
: - Eric Tiedemann의 약한 딕셔너리 구현:
Java의 약한 참조 (Weak References in Java)
Java는 세 가지 형태의 약한 참조(“weak”, “soft”, “phantom”)와 한 가지 흥미로운 헬퍼 클래스를 제공합니다. 관련 클래스는 java.lang.ref
패키지에 정의되어 있습니다.
- Weak References (약한 참조): Dianne Hackborn의
vref
제안과 가장 유사합니다. 참조 객체가 단일 Python 객체를 참조하지만, 그 객체에 대한 강한 참조를 소유하지 않습니다. 객체가 할당 해제되면 참조 객체는 무효화됩니다. - Soft References (소프트 참조): 유사하지만, 참조 대상 객체에 대한 다른 모든 참조가 해제되자마자 무효화되지는 않습니다. 소프트 참조는 참조를 소유하지만, 메모리 할당자가 메모리가 다른 곳에 필요하면 참조 대상을 해제하도록 허용합니다.
- Phantom References (팬텀 참조): 객체가 큐에 추가될 때 참조 대상이 지워지지 않는다는 점에서 다릅니다. 객체에 대한 모든 팬텀 참조가 큐에서 제거되면 객체가 지워집니다. 이는 객체의
.finalize()
메서드가 호출되기 전에 발생해야 하는 추가 정리 작업이 수행될 때까지 객체를 살아있게 유지하는 데 사용될 수 있습니다.
각 참조 타입에는 메모리 할당자에 의해 무효화될 때 참조를 큐에 추가하는 옵션이 있습니다.
부록 – Dianne Hackborn의 vref 제안 (1995) (Appendix – Dianne Hackborn’s vref proposal (1995))
이 부록은 Dianne Hackborn이 1995년에 제안한 “가상 참조(Virtual References)”에 대한 원본 제안을 포함합니다. 이 제안은 참조 카운팅 대 가비지 컬렉션에 대한 반복적인 논의를 부분적으로 해결하려는 시도였습니다.
핵심 메커니즘은 “가상 참조(virtual reference)” 또는 “vref”입니다. vref는 객체의 참조 카운트를 증가시키지 않는 객체에 대한 핸들입니다. 즉, vref를 가지고 있어도 객체가 파괴되는 것을 막지 않습니다. 이를 통해 순환 참조를 생성하지 않고 트리 구조와 같은 것을 생성할 수 있습니다.
구현을 위해 두 가지 기본 추가 사항이 제안되었습니다.
- 새로운 “vref” 타입: Python 프로그래머가 가상 참조를 생성하고 조작하는 데 사용됩니다. 내부적으로는 참조하는 Python 객체에 대한 포인터를 가진 C 레벨 Python 객체입니다. 객체 할당 해제 시, 해당 객체에 대한 vref 목록을 순회하여 모든 vref가
Nothing
과 같은 안전한 값을 가리키도록 합니다. - 기본 Python 객체에 새 필드 추가: 이 필드는
NULL
이거나, 자신을 참조하는 모든 vref 객체의 목록 헤드를 가리킵니다.
vref 객체에 대해 두 가지 가능한 시맨틱(semantics)이 고려되었습니다.
포인터 시맨틱 (Pointer Semantics)
이 모델에서 vref는 Python 레벨 포인터처럼 동작합니다. Python 프로그램은 실제 객체를 조작하기 위해 vref를 명시적으로 역참조해야 합니다. 예를 들어, MyVref = vref.new(MyObject)
를 사용하고 MyVref.object
로 실제 객체에 접근합니다. &
(vref 생성) 및 *
(역참조)와 같은 C-스타일 구문도 제안되었습니다.
프록시 시맨틱 (Proxy Semantics)
이 모델에서 Python 프로그래머는 vref 객체를 마치 참조하는 객체를 직접 조작하는 것처럼 다룹니다. 이는 vref에 대한 모든 연산이 참조된 객체로 리디렉션되도록 구현함으로써 달성됩니다. 이 경우 역참조 연산자(*
)는 의미가 없으며, &
연산자만 존재합니다. &MyObject
는 MyObject
와 동일하게 동작하며, (&MyObject).attr
는 MyObject.attr
와 같습니다. &&MyObject
는 MyObject
자체를 참조합니다.
Hackborn은 프록시 시맨틱을 선호했지만, 당시 Python 구현에서는 구현하기 매우 어렵거나 사실상 불가능하다고 언급했습니다.
이 문서는 퍼블릭 도메인에 있습니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments