Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 205 – 약한 참조

Author:
Fred L. Drake, Jr. <fred at fdrake.net>
Status:
Final
Type:
Standards Track
Created:
14-Jul-2000
Python-Version:
2.1
Post-History:
11-Jan-2001

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

동기

Python 프로그래머들이 언급해온 약한 참조의 기본적인 응용은 두 가지, 즉 객체 캐시와 순환 참조로 인한 고통의 경감입니다.

캐시(약한 딕셔너리)

외부 상태를 표현하는 객체를 유지할 필요가 있는데, 이는 단일 인스턴스를 외부 실체에 매핑하는 것으로, 여러 인스턴스가 동일한 외부 자원에 매핑되도록 허용하면 인스턴스 간의 동기화를 유지하는 데 불필요한 어려움이 생깁니다. 이러한 경우, 흔히 쓰이는 관용구는 인스턴스의 캐시를 지원하는 것입니다. 즉, 팩토리 함수를 사용하여 새 인스턴스나 기존 인스턴스를 반환합니다.

이 접근 방식의 어려움은 두 가지 중 하나를 감수해야 한다는 점인데, 캐시가 한없이 커지거나, 애플리케이션의 다른 곳에서 캐시를 명시적으로 관리해야 합니다. 후자는 매우 번거로울 수 있고 당면한 문제를 해결하는 데 실제로 필요한 것보다 더 많은 코드를 초래하며, 전자는 장시간 실행되는 프로세스나 심지어 메모리 요구량이 상당한 비교적 짧은 프로세스에서도 용납될 수 없습니다.

  • 내부 사용자가 얼마나 많든 상관없이 단일 인스턴스로 표현되어야 하는 외부 객체. 이는 매번 사용할 때마다 잠그고 수정하기보다는 전체를 디스크에 다시 써야 하는 파일을 표현하는 데 유용할 수 있습니다.
  • 생성 비용이 크지만 여러 내부 소비자가 필요로 할 수 있는 객체. 첫 번째 경우와 비슷하지만, 반드시 외부 자원에 묶여 있지는 않으며, 공유 상태에 대한 문제가 아닐 수도 있습니다. 약한 참조는 이 경우, “소프트” 참조와 비슷한 것이 존재하거나 개별 객체의 사용자들이 수명 면에서 겹칠 가능성이 클 때만 유용합니다.

순환 참조

  • DOM은 (부모 및 문서 노드에 대한) 순환 참조를 대량으로 필요로 하지만, 각 노드에서 부모로의 매핑을 나타내는 약한 딕셔너리를 사용하면 이를 제거할 수 있습니다. 이는 xml.dom.pulldom과 같은 맥락에서 특히 유용할 수 있으며, .unlink() 연산이 아무 동작도 하지 않도록(no-op) 만들 수 있습니다.

이 제안서는 다음 섹션들로 나뉩니다:

  • 제안된 해결책
  • 구현 전략
  • 가능한 응용
  • 파이썬에서의 이전 약한 참조 작업
  • 자바의 약한 참조

초기 제안 중 하나의 전문은 인터넷에서 구할 수 없는 것으로 보이므로 부록으로 포함되어 있습니다.

해결책 공간의 측면들

약한 참조 문제에는 두 가지 뚜렷한 측면이 있습니다:

  • 약한 참조의 무효화
  • 파이썬 코드에 대한 약한 참조의 표현

무효화

약한 참조 무효화에 대한 과거의 접근법들은 흔히 강한 참조를 저장하고 약한 참조 객체의 모든 인스턴스를 검사할 수 있는 것에 의존했으며, 참조 대상의 참조 카운트가 1이 될 때(약한 참조가 저장한 참조가 마지막으로 남은 참조임을 나타낼 때) 이를 무효화했습니다. 이 방식은 파이썬의 메모리 관리 기제를 변경할 필요가 없고, 어떤 타입이든 약하게 참조할 수 있다는 장점이 있습니다.

이 무효화 방식의 단점은 약한 참조의 관리가 충분히 자주 호출되어 약하게 참조되는 객체들이 합리적으로 짧은 시간 내에 감지된다고 가정한다는 것인데, 이는 참조를 무효화하기 위해 어떤 자료 구조를 훑는 것을 의미하며, 이는 약하게 참조되는 객체의 수에 대해 O(N)인 연산이므로, 약하게 참조되는 개별 객체 하나하나에 대해 효과적으로 분할 상환되지 않습니다. 이는 또한 애플리케이션이 약하게 참조되는 객체를 처리하는 코드를 어느 정도 자주 호출한다고 가정하는 것이며, 이 때문에 약한 참조는 라이브러리 코드에는 덜 매력적입니다.

무효화에 대한 대안적 접근 방식은, 역참조 대상 객체가 할당 해제될 때마다 할당 해제 코드가 약한 참조의 가능성을 인식하고 모든 무효화를 위해 약한 참조 관리 코드로 특정 호출을 하도록 하는 것입니다. 이를 위해서는 약한 참조가 가능한 객체에 대한 tp_dealloc 핸들러에 변경이 필요합니다. 약한 참조를 지원하는 객체의 경우 핸들러 “맨 위”에서 추가 호출이 필요하며, 객체로부터 해당 객체에 대한 약한 참조 사슬로 매핑하는 효율적인 방법도 필요합니다.

표현 방식

약한 참조가 파이썬 계층에 표현되는 두 가지 방식은, 하위 객체에 대한 사용 가능한 참조를 가져오기 위해 어떤 연산이 요구되는 명시적 참조 객체와, 가능한 한 원본 객체처럼 위장하는 프록시 객체였습니다.

참조 객체는 객체 관리의 추가적인 계층이 파이썬에 더해질 때 다루기 쉬우며, 참조 대상에 대한 연산을 호출하고 유효하지 않은 약한 참조가 사용될 때 발생하는 특별한 예외를 잡을 필요 없이 참조의 생존 여부를 명시적으로 확인할 수 있습니다.

그러나 상당수의 사용자는 약한 참조가 원본 객체와 매우 흡사해 보인다는 단순한 이유로 프록시 방식을 선호합니다.

제안된 해법

약한 참조는 (직접적으로든 간접적으로든) 상당한 메모리 크기를 가지거나, 외부 자원(데이터베이스 연결, 열린 파일 등)에 대한 참조를 보유하는 모든 파이썬 객체를 가리킬 수 있어야 합니다.

새로운 모듈인 weakref는 약한 참조를 만드는 데 사용되는 새 함수들을 포함합니다. weakref.ref()는 “약한 참조 객체”를 생성하며, 선택적으로 객체가 막 종료 처리될 때 호출될 콜백을 첨부할 수 있습니다. weakref.mapping()은 “약한 딕셔너리”를 생성합니다. 세 번째 함수인 weakref.proxy()는 원본 객체와 다소 비슷하게 동작하는 프록시 객체를 만듭니다.

약한 참조 객체는, 참조 대상 객체가 아직 수거되지 않았다면 그 객체에 대한 접근을 허용하며, 해당 객체가 여전히 메모리에 존재하는지 판단할 수 있게 해줍니다. 참조 대상을 가져오는 것은 참조 객체를 호출함으로써 이루어집니다. 참조 대상이 더 이상 존재하지 않으면, 대신 None을 반환합니다.

약한 딕셔너리는 임의의 키를 값에 매핑하지만, 값에 대한 참조를 소유하지는 않습니다. 값이 종료될 때, 그 값을 값으로 가지는 (키, 값) 쌍은 그러한 쌍을 포함하는 모든 매핑에서 제거됩니다. 딕셔너리와 마찬가지로, 약한 딕셔너리는 해시 가능하지 않습니다.

프록시 객체는 자신이 대신하는 객체처럼 가능한 한 동작하려 시도하는 약한 참조입니다. 기반 타입과 관계없이, 프록시는 해시 가능하지 않은데, 약한 참조로서 동작하는 능력이 딕셔너리 키로 사용될 때 실패를 유발하는 근본적인 가변성에 의존하기 때문입니다 – 참조 대상이 소멸하기 전에 올바른 해시 값이 계산되더라도, 결과로 생성된 프록시는 참조 대상이 만료된 후에는 비교할 수 없기 때문에 딕셔너리 키로 사용할 수 없으며, 비교 가능성은 딕셔너리 키에 필수적입니다. 참조 대상이 소멸된 후 프록시 객체에 대한 연산은 대부분의 경우 weakref.ReferenceError를 발생시킵니다. “is” 비교, type(), id()는 계속 동작하지만, 항상 참조 대상이 아니라 프록시를 가리킵니다.

약한 참조에 등록된 콜백은 단일 매개변수를 받아야 하며, 이 매개변수는 약한 참조 또는 프록시 객체 자체입니다. 콜백 내에서는 해당 객체에 접근하거나 되살릴 수 없습니다.

구현 전략

약한 참조의 구현에는 약하게 참조 가능한 각 객체에 대해 지워져야 하는 참조 컨테이너 목록이 포함됩니다. 참조가 약한 딕셔너리로부터 온 것이라면, 딕셔너리 항목이 먼저 지워집니다. 그런 다음, 연관된 콜백이 있다면 해당 객체를 매개변수로 전달하여 호출됩니다. 모든 콜백이 호출되고 나면, 객체가 종료되고 할당 해제됩니다.

많은 내장 타입이 약한 참조 관리에 참여하며, 어떤 확장 타입이든 이를 선택할 수 있습니다. 타입 구조체는 인스턴스 구조체 내로의 오프셋을 제공하는 추가 필드를 포함하며, 이 오프셋은 약한 참조 구조체 목록을 가리킵니다. 필드의 값이 0 이하이면, 해당 객체는 참여하지 않습니다. 이 경우, weakref.ref(), <weakdict>.__setitem__().setdefault(), 그리고 항목 대입은 TypeError를 발생시킵니다. 필드의 값이 0보다 크면, 새로운 약한 참조를 생성하여 목록에 추가할 수 있습니다.

이 접근 방식은 숫자나 다른 작은 타입에 대해 메모리 부담을 지우지 않으면서도 임의의 확장 타입이 참여할 수 있도록 하기 위해 채택되었습니다.

약한 참조를 지원하는 표준 타입에는 인스턴스, 함수, 바운드 및 언바운드 메서드가 있습니다. Python 2.2에서 클래스 타입(“새 형식 클래스”)이 추가되면서 타입도 약한 참조를 지원하게 되었습니다. 클래스 타입의 인스턴스는 약한 참조가 가능한 베이스 타입을 가지고 있거나, 클래스가 __slots__를 지정하지 않았거나, 슬롯 중 하나가 __weakref__로 명명된 경우 약한 참조가 가능합니다. 제너레이터도 약한 참조를 지원합니다.

가능한 응용

PyGTK+ 바인딩?

Tkinter – 위젯에서 부모로 향하는 참조를 약한 참조로 사용하면 순환 참조를 피할 수 있습니다. 일반적인 경우 객체가 더 일찍 폐기되지는 않지만, 참조를 해제하기 전에 프로그래머가 .destroy()를 호출하는 데 그렇게까지 의존하지 않아도 됩니다. 이는 주로 장기 실행 애플리케이션에 이득이 될 것입니다.

DOM 트리.

Python의 이전 약한 참조 작업

Dianne Hackborn은 “가상 참조(virtual references)”라고 부르는 것을 제안했습니다. ‘vref’ 객체는 java.lang.ref.WeakReference 객체와 매우 유사하지만, 무효화 큐(invalidation queue)에 해당하는 것은 없습니다. “약한 딕셔너리(weak dictionary)”를 구현하는 것은 Java에서 (무효화 큐 없이) 약한 참조만 사용하는 것만큼이나 어려울 것입니다. 이에 관한 정보는 웹에서 사라졌지만, 아래에 부록으로 포함되어 있습니다.

Marc-André Lemburg의 mx.Proxy 패키지:

Dieter Maurer가 만든 weakdict 모듈은 C와 Python으로 구현되어 있습니다. 웹 페이지가 Python 1.5.2a 이후로 업데이트되지 않은 것으로 보이므로, 이 구현이 Python 2.0과 호환되는지는 아직 확실하지 않습니다.

Alex Shindich가 만든 PyWeakReference:

Eric Tiedemann은 약한 딕셔너리 구현을 가지고 있습니다:

Java의 약한 참조

http://java.sun.com/j2se/1.3/docs/api/java/lang/ref/package-summary.html

Java는 세 가지 형태의 약한 참조와 한 가지 흥미로운 도우미 클래스를 제공합니다. 이 세 가지 형태는 “weak”, “soft”, “phantom” 참조라고 불립니다. 관련 클래스는 java.lang.ref 패키지에 정의되어 있습니다.

각 참조 타입에 대해, 메모리 할당자에 의해 무효화될 때 그 참조를 큐에 추가하는 옵션이 있습니다. 이 기능의 주된 목적은 상당한 추가적인 잠금 요구 사항을 부과하지 않고도 약한 참조 시맨틱을 포함하는 더 큰 구조를 구성할 수 있게 하는 것으로 보입니다. 예를 들어, 이 기능을 사용하여 참조가 다른 곳에서 더 이상 사용되지 않을 때 키와 참조 대상을 제거하는 “약한” 해시 테이블을 만드는 것은 어렵지 않을 것입니다. 무효화에 대한 일종의 알림 큐 없이 객체에 약한 참조를 사용하면 해시 테이블에 필요한 다양한 연산의 구현이 훨씬 더 번거로워집니다. 저장된 객체의 할당 해제가 드물게 일어난다면 이는 성능 병목이 될 수 있습니다.

Java의 “약한” 참조는 Dianne Hackborn의 예전 vref 제안과 가장 유사합니다: 참조 객체는 단일 Python 객체를 참조하지만, 그 객체에 대한 참조를 소유하지는 않습니다. 그 객체가 할당 해제되면, 참조 객체는 무효화됩니다. 참조 객체의 사용자는 참조가 무효화되었음을 쉽게 확인할 수 있으며, 참조된 객체를 사용하려는 시도가 있을 때 NullObjectDereferenceError가 발생할 수도 있습니다.

“소프트” 참조는 이와 유사하지만, 참조된 객체에 대한 다른 모든 참조가 해제되자마자 무효화되지는 않습니다. “소프트” 참조는 참조를 소유하기는 하지만, 메모리가 다른 곳에서 필요한 경우 메모리 할당자가 참조 대상을 해제할 수 있도록 허용합니다. 이것이 malloc() 구현이 sbrk() 혹은 그에 상응하는 것을 호출하기 전에 소프트 참조가 해제된다는 것인지, 아니면 malloc()NULL을 반환할 때만 소프트 참조가 정리된다는 것인지는 명확하지 않습니다.

“팬텀” 참조는 조금 다릅니다. 약한 참조 및 소프트 참조와 달리, 참조가 큐에 추가될 때 참조 대상이 정리되지 않습니다. 어떤 객체에 대한 모든 팬텀 참조가 큐에서 제거되면, 그 객체는 정리됩니다. 이는 객체의 .finalize() 메서드가 호출되기 전에 이루어져야 하는 추가적인 정리가 수행될 때까지 객체를 살아 있게 유지하는 데 사용될 수 있습니다.

다른 두 참조 타입과 달리, “팬텀” 참조는 반드시 무효화 큐와 연관되어야 합니다.

부록 – Dianne Hackborn의 vref 제안 (1995)

[이 문서는 들여쓰기와 단락 재배열이 이루어졌으나, 내용상의 변경은 없습니다. –Fred]

제안: 가상 참조(Virtual References)

참조 카운팅과 가비지 컬렉션에 관한 반복되는 논의를 부분적으로나마 해결하고자, “잘 구조화된” 순환 그래프의 생성에 도움이 될 Python의 확장 기능을 제안하고자 합니다. 특히, 이는 최소한 부모 역참조를 갖는 트리와 이중 연결 리스트를 순환에 대한 걱정 없이 생성할 수 있도록 해줄 것입니다.

제가 제안하고자 하는 기본 메커니즘은 “가상 참조(virtual reference)”, 이하 “vref”라고 부를 것에 관한 것입니다. vref는 본질적으로 객체의 참조 카운트를 증가시키지 않는, 객체에 대한 핸들입니다. 이는 곧 어떤 객체에 대해 vref를 보유하고 있더라도 그 객체가 소멸되는 것을 막지 않는다는 것을 의미합니다. 이를 통해 예를 들어 Python 프로그래머는 앞서 언급한 트리 구조를, 더 이상 사용되지 않을 때 자동으로 소멸되도록 생성할 수 있을 것입니다 – 모든 부모 역참조를 vref로 만듦으로써, 그것들이 더 이상 트리의 소멸을 막는 참조 순환을 만들지 않게 되기 때문입니다.

이 메커니즘을 구현하기 위해서는, Python 코어가 더 이상 존재하지 않는 객체를 참조하는 -실제- 포인터가 결코 남아있지 않도록 보장해야 합니다. 제가 제안하고자 하는 구현은 현재의 Python 시스템에 두 가지 기본적인 추가 사항을 포함합니다:

  1. Python 프로그래머가 가상 참조를 생성하고 조작할 수 있도록 하는 새로운 “vref” 타입. 내부적으로 이는 기본적으로 자신이 참조하는 Python 객체에 대한 포인터를 가진 C 레벨의 Python 객체입니다. 그러나 다른 모든 Python 코드와는 달리, 이는 해당 객체의 참조 카운트를 변경하지 않습니다. 또한, 아래에서 사용될 이중 연결 리스트를 구현하기 위한 두 개의 포인터를 포함합니다.
  2. 기본 Python 객체[object.h의 PyObject_Head]에 새 필드를 추가하는데, 이 필드는 NULL이거나 자신을 참조하는 모든 vref 객체 리스트의 헤드를 가리킵니다. vref 객체가 다른 객체에 자신을 붙일 때, 이 연결 리스트에 자신을 추가합니다. 그런 다음, vref가 걸려 있는 객체가 할당 해제되면, 이 리스트를 순회하여 그 위에 있는 모든 vref가 안전한 값(예: Nothing)을 가리키도록 보장할 수 있습니다.

이 구현은 바라건대 현재의 Python 코어에 미치는 영향이 최소화되어야 하는데, vref가 존재하지 않을 때는 모든 객체에 포인터 하나만 추가하고, 객체가 할당 해제될 때마다 NULL 포인터 검사만 추가해야 합니다.

Python 언어 수준으로 돌아가서, vref 객체에 대해 가능한 두 가지 시맨틱을 고려해 보았습니다 –

포인터 시맨틱

이 모델에서, vref는 본질적으로 Python 수준의 포인터처럼 동작하며, Python 프로그램은 vref가 참조하는 실제 객체를 다루려면 명시적으로 vref를 역참조해야 합니다.

이 모델을 사용하는 예시 vref 모듈은 “new” 함수를 포함할 수 있습니다. ‘MyVref = vref.new(MyObject)’와 같이 사용하면, MyVref.object == MyObject가 되도록 새 vref 객체를 반환합니다. 이후 MyObject가 할당 해제되면 MyVref.object는 Nothing으로 바뀝니다.

구체적인 예로, 새로운 C 스타일 구문을 도입할 수 있습니다:

  • & – 단항 연산자로, 객체에 대한 vref를 생성하며 vref.new()와 동일합니다.
  • * – 단항 연산자로, vref를 역참조하며 VrefObject.object와 동일합니다.

그러면 다음과 같이 정의할 수 있습니다:

1.     type(&MyObject) == vref.VrefType
2.        *(&MyObject) == MyObject
3. (*(&MyObject)).attr == MyObject.attr
4.          &&MyObject == Nothing
5.           *MyObject -> exception

규칙 #4는 미묘하지만, (실제 참조가 없는 vref)에 대한 vref를 만들었기 때문에 생겨납니다. 따라서 내부 vref가 불가피하게 사라지면 외부 vref는 Nothing으로 지워집니다.

프록시 시맨틱

이 모델에서 파이썬 프로그래머는 vref 객체가 참조하는 객체를 직접 조작하는 것처럼 vref 객체를 조작합니다. 이는 vref에 대한 모든 연산이 참조된 객체로 리디렉션되도록 vref를 구현함으로써 달성됩니다. 이 모델에서는 역참조 연산자(*)가 더 이상 의미가 없으며, 대신 참조 연산자(&)만 있고, 다음과 같이 정의합니다:

1.  type(&MyObject) == type(MyObject)
2.        &MyObject == MyObject
3. (&MyObject).attr == MyObject.attr
4.       &&MyObject == MyObject

다시 말하지만, 규칙 #4가 중요합니다 – 여기서 바깥쪽 vref는 실제로 원본 객체에 대한 참조이며, 안쪽 vref에 대한 참조가 -아닙니다-. 이는 vref에 적용된 모든 연산이 실제로는 그 객체에 적용되기 때문이며, 따라서 vref의 vref를 만드는 것은 실제로 후자의 객체에 대한 vref를 만드는 결과가 됩니다.

첫 번째인 포인터 시맨틱스는 구현하기가 매우 쉽다는 장점이 있습니다. vref 타입은 극도로 단순하여, 최소한 단일 속성인 object와 참조를 생성하는 함수 하나만 있으면 됩니다.

하지만 저는 프록시 시맨틱스가 정말 마음에 듭니다. 파이썬 프로그래머에게 부담을 덜 줄 뿐만 아니라, 실제 객체를 사용할 곳이면 어디든 vref를 사용할 수 있는 등 멋진 일들을 할 수 있게 해줍니다. 안타깝게도, 현재의 파이썬 구현체에서 이를 구현하는 것은 사실상 불가능하지는 않더라도 극도로 고통스러운 일일 것입니다. 하지만 이를 어떻게 할 수 있을지에 대해 몇 가지 생각이 있는데, 흥미로워 보인다면 말씀드리자면, 한 가지 가능성은 vref를 처리하는 새로운 타입 검사 함수를 도입하는 것입니다. 이렇게 하면 vref를 예상하지 못하는 기존 C 모듈들이 수정되기 전까지는 단순히 타입 오류를 반환하도록 만들 수 있을 것으로 기대합니다.

마지막으로, 이 시스템이 제공할 수 있는 몇 가지 추가 기능이 더 있습니다. 제게 특히 흥미로워 보이는 것 하나는, 파이썬 프로그래머가 vref에 “소멸자” 함수를 추가할 수 있도록 허용하는 것입니다 – 이 파이썬 함수는 참조된 객체가 할당 해제되기 직전에 호출되며, 이를 통해 파이썬 프로그램이 다른 객체에 보이지 않게 부착되어 그 객체가 사라지는 것을 지켜볼 수 있게 됩니다. 이는 꽤 근사해 보이지만, 아직 실제로 이를 활용할 실용적인 용도는 떠올리지 못했습니다… :)

– Dianne