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)이 될 수 있게 합니다.

이 제안은 다음 절로 나뉩니다:

  • 제안하는 해결책
  • 구현 전략
  • 가능한 응용
  • Python에서의 기존 약한 참조 작업
  • Java의 약한 참조

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

해결 공간의 측면

약한 참조 문제에는 서로 다른 두 가지 측면이 있습니다:

  • 약한 참조의 무효화
  • Python 코드에 약한 참조를 제공하는 방식

무효화

약한 참조 무효화에 대한 과거의 접근 방식은 흔히 강한 참조를 저장하고, 약한 참조 객체의 모든 인스턴스를 검사할 수 있으며, 참조 대상의 참조 횟수가 1이 되면(약한 참조가 저장한 참조가 마지막으로 남은 참조임을 나타냄) 그것들을 무효화하는 데 의존해 왔습니다. 이 방식에는 Python의 메모리 관리 기구를 바꿀 필요가 없고, 어떤 타입이든 약하게 참조할 수 있다는 장점이 있습니다.

이러한 무효화 접근 방식의 단점은, 약하게 참조된 객체가 적절히 짧은 시간 안에 감지될 만큼 약한 참조 관리가 충분히 자주 호출된다고 가정한다는 점입니다. 이는 참조를 무효화하기 위해 어떤 자료 구조를 훑어야 한다는 뜻이고, 이 연산은 약하게 참조된 객체 수에 대해 O(N)이므로, 약하게 참조된 개별 객체 하나에 대해서는 효과적으로 분할 상환되지 않습니다. 또한 이 방식은 애플리케이션이 약하게 참조된 객체를 다루는 코드를 어느 정도 자주 호출한다고 가정하므로, 라이브러리 코드에서는 약한 참조의 매력이 떨어집니다.

무효화에 대한 또 다른 접근 방식은 할당 해제 코드가 약한 참조의 존재 가능성을 인지하고, 객체가 할당 해제될 때마다 약한 참조 관리 코드를 명시적으로 호출하여 모든 무효화를 수행하게 하는 것입니다. 이를 위해서는 약한 참조가 가능한 객체의 tp_dealloc 핸들러를 변경해야 합니다. 약한 참조를 지원하는 객체에서는 핸들러의 “맨 앞”에 호출을 하나 더 추가해야 하며, 객체에서 그 객체에 대한 약한 참조 체인으로 대응시키는 효율적인 방법도 필요합니다.

표현 방식

지금까지 약한 참조를 Python 계층에 제공하는 방식은 두 가지였습니다. 하나는 명시적인 참조 객체로, 내부 객체에 대한 사용 가능한 참조를 얻으려면 어떤 연산을 수행해야 합니다. 다른 하나는 원래 객체를 최대한 흉내 내는 프록시 객체입니다.

Python에서 객체 관리 계층을 추가로 구현할 때는 참조 객체를 다루기가 쉽습니다. 참조 대상에 연산을 수행한 뒤 무효한 약한 참조를 사용할 때 발생하는 특수한 예외를 잡을 필요 없이, 참조가 살아 있는지를 명시적으로 확인할 수 있기 때문입니다.

그러나 약한 참조가 원래 객체와 매우 비슷해 보인다는 이유만으로 프록시 방식을 선호하는 사용자도 많습니다.

제안하는 해결책

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

새 모듈인 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)에 해당하는 것이 없습니다. “약한 딕셔너리”를 구현하는 것은 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의 “약한(weak)” 참조는 Dianne Hackborn의 예전 vref 제안과 가장 비슷합니다. 참조 객체는 하나의 Python 객체를 가리키지만, 그 객체에 대한 참조를 소유하지는 않습니다. 그 객체가 할당 해제되면 참조 객체는 무효화됩니다. 참조 객체의 사용자는 참조가 무효화되었는지를 쉽게 확인할 수 있으며, 참조 대상 객체를 사용하려고 시도할 때 NullObjectDereferenceError가 발생할 수도 있습니다.

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

“팬텀(phantom)” 참조는 조금 다릅니다. 약한 참조나 소프트 참조와 달리, 참조가 큐에 추가될 때 참조 대상이 지워지지 않습니다. 어떤 객체에 대한 모든 팬텀 참조가 큐에서 제거되면 그 객체가 지워집니다. 이는 객체의 .finalize() 메서드가 호출되기 전에 수행되어야 하는 추가 정리 작업이 끝날 때까지 객체를 살려 두는 데 사용할 수 있습니다.

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

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

[들여쓰기를 하고 문단을 다시 정렬했지만, 내용은 변경하지 않았습니다. –Fred]

제안: 가상 참조

참조 카운팅 대 가비지 컬렉션에 관해 반복되는 논의를 부분적으로나마 해결하고자, “잘 구조화된” 순환 그래프를 만드는 데 도움이 될 Python 확장을 제안하고자 합니다. 특히, 이 확장을 사용하면 최소한 부모 역방향 포인터가 있는 트리와 이중 연결 리스트를 순환 걱정 없이 만들 수 있어야 합니다.

제가 제안하려는 기본 메커니즘은 “가상 참조(virtual reference)”, 이하 “vref”라고 부르는 것입니다. vref는 본질적으로 객체의 참조 횟수를 증가시키지 않는, 객체에 대한 핸들입니다. 즉, 객체에 대한 vref를 가지고 있어도 그 객체가 파괴되는 것을 막지 못합니다. 예를 들어, 이를 통해 Python 프로그래머는 앞서 언급한 트리 구조를 만들 수 있으며, 이 트리는 더 이상 사용되지 않을 때 자동으로 파괴됩니다. 부모에 대한 역방향 참조를 모두 vref로 만들면, 트리의 파괴를 막는 참조 순환이 더 이상 생기지 않기 때문입니다.

이 메커니즘을 구현하려면 Python 코어는 더 이상 존재하지 않는 객체를 가리키는 -실제- 포인터가 절대 남지 않도록 보장해야 합니다. 제가 제안하려는 구현은 현재 Python 시스템에 두 가지 기본 사항을 추가하는 것입니다:

  1. 새로운 “vref” 타입입니다. Python 프로그래머는 이를 통해 가상 참조를 생성하고 조작합니다. 내부적으로 이는 기본적으로 자신이 참조하는 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으로 지워집니다.

프록시 의미론

이 모델에서 Python 프로그래머는 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가 만들어지기 때문입니다.

첫 번째인 포인터 의미는 구현하기가 매우 쉽다는 장점이 있습니다. vref 타입은 극히 단순하여, 최소한 단일 속성인 객체(object)와 참조를 생성하는 함수 하나만 있으면 됩니다.

그러나 저는 프록시 의미론이 정말 마음에 듭니다. Python 프로그래머의 부담을 덜어 줄 뿐만 아니라, 실제 객체를 사용할 수 있는 곳이라면 어디서든 vref를 사용하는 것과 같은 멋진 일을 할 수 있게 해 줍니다. 안타깝게도, 현재 Python 구현에서 이를 구현하는 것은 사실상 불가능하지는 않더라도 아마 극도로 고통스러운 일일 것입니다. 그래도 흥미롭게 여겨진다면, 이를 어떻게 구현할지에 대해 몇 가지 생각이 있습니다. 한 가지 방법은 vref를 처리하는 새로운 타입 검사 함수를 도입하는 것입니다. 이렇게 하면 vref를 예상하지 못하는 오래된 C 모듈은 수정될 때까지 단순히 타입 오류를 반환하게 될 것으로 기대합니다.

마지막으로, 이 시스템이 제공할 수 있는 다른 추가 기능들도 있습니다. 제게 특히 흥미로워 보이는 기능 중 하나는 Python 프로그래머가 vref에 “소멸자” 함수를 추가할 수 있게 하는 것입니다 – 이 Python 함수는 참조 대상 객체가 해제되기 직전에 호출되므로, Python 프로그램이 다른 객체에 보이지 않게 자신을 붙여 그 객체가 사라지는지 지켜볼 수 있습니다. 멋져 보이기는 하지만, 아직 실제로 쓸 만한 용도는 떠올리지 못했습니다… :)

– Dianne