PEP 253 – 내장 타입의 서브타이핑
- Author:
- Guido van Rossum <guido at python.org>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 14-May-2001
- Python-Version:
- 2.2
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 C와 Python에서 내장 타입의 서브타입을 만들 수 있도록 타입 객체 API에 추가 사항을 제안합니다.
[편집자 주: 이 PEP에서 설명한 아이디어는 Python에 통합되었습니다. 이 PEP는 더 이상 구현을 정확하게 설명하지 않습니다.]
소개
전통적으로 Python의 타입은 PyTypeObject 타입의 전역 변수를 선언하고 이를 정적 초기화자로 초기화하는 방식으로 정적으로 생성되었습니다. 타입 객체의 슬롯은 Python 인터프리터와 관련된 Python 타입의 모든 측면을 기술합니다. 몇몇 슬롯은 (인스턴스의 기본 할당 크기와 같은) 크기 정보를 담고, 다른 슬롯은 여러 플래그를 담지만, 대부분의 슬롯은 다양한 종류의 동작을 구현하는 함수에 대한 포인터입니다. NULL 포인터는 해당 타입이 그 특정 동작을 구현하지 않는다는 뜻입니다. 이 경우 그 타입의 인스턴스에 대해 해당 동작이 호출되면 시스템이 기본 동작을 제공하거나 예외를 발생시킬 수 있습니다. 보통 함께 정의되는 함수 포인터 묶음 중 일부는 더 많은 함수 포인터를 담은 추가 구조체에 대한 포인터를 통해 간접적으로 얻습니다.
PyTypeObject 구조체를 초기화하는 세부 사항은 따로 문서화되지는 않았지만 소스 코드의 예제에서 쉽게 파악할 수 있으며, 이 글에서는 독자가 C로 새로운 Python 타입을 만드는 전통적인 방식에 충분히 익숙하다고 가정합니다.
이 PEP는 다음 기능을 도입합니다:
- 타입이 자기 인스턴스의 팩토리 함수가 될 수 있습니다
- C에서 타입의 서브타입을 만들 수 있습니다
- Python에서 class 문으로 타입의 서브타입을 만들 수 있습니다
- 타입으로부터의 다중 상속을 지원합니다(실용적인 범위 내에서 – 여전히 리스트와 딕셔너리를 동시에 다중 상속할 수는 없습니다)
- 표준 강제 변환 함수(int, tuple(튜플), str 등)는 해당 타입 객체로 재정의되며, 이 타입 객체가 자기 자신의 팩토리 함수 역할을 합니다
- class 문에
__metaclass__선언을 넣어 새 클래스를 만드는 데 사용할 메타클래스를 지정할 수 있습니다 - class 문에
__slots__선언을 넣어 지원되는 인스턴스 변수의 구체적인 이름을 지정할 수 있습니다
이 PEP는 타입에 표준 인트로스펙션을 추가하는 PEP 252를 기반으로 합니다. 예를 들어 특정 타입 객체가 tp_hash 슬롯을 초기화하면, 그 타입 객체를 인트로스펙션했을 때 __hash__ 메서드가 있습니다. PEP 252는 또한 모든 메서드를 담은 딕셔너리를 타입 객체에 추가합니다. Python 수준에서 이 딕셔너리는 내장 타입에 대해 읽기 전용이며, C 수준에서는 직접 접근할 수 있습니다(단, 초기화 과정의 일부가 아니라면 수정해서는 안 됩니다).
바이너리 호환성을 위해 tp_flags 슬롯의 플래그 비트 하나가 아래에서 소개하는 타입 객체의 여러 새 슬롯이 존재하는지를 나타냅니다. tp_flags 슬롯에 Py_TPFLAGS_HAVE_CLASS 비트가 설정되지 않은 타입은 모든 서브타이핑 슬롯의 값이 NULL이라고 가정합니다. (경고: 현재 구현 프로토타입은 이 플래그 비트를 아직 일관되게 검사하지 않습니다. 이는 최종 릴리스 전에 수정되어야 합니다.)
현재 Python에서는 타입과 클래스를 구분합니다. 이 PEP는 PEP 254와 함께 그 구별을 없앨 것입니다. 하지만 하위 호환성을 위해 이 구별은 아마 앞으로 몇 년간 남아 있을 것이며, PEP 254가 없으면 그 차이는 여전히 큽니다. 타입은 궁극적으로 내장 타입을 베이스 클래스로 가지는 반면, 클래스는 궁극적으로 사용자 정의 클래스에서 파생됩니다. 따라서 이 PEP의 나머지 부분에서는 가능한 한 타입이라는 단어를 사용하겠습니다 – 베이스 타입 또는 슈퍼타입, 파생 타입 또는 서브타입, 그리고 메타타입도 마찬가지입니다. 하지만 때로는 용어가 필연적으로 뒤섞이는데, 예를 들어 객체의 타입은 __class__ 속성으로 주어지고, Python에서 서브타이핑은 class 문으로 표기합니다. 더 구별할 필요가 있다면 사용자 정의 클래스를 “클래식” 클래스라고 부를 수 있습니다.
메타타입에 관하여
논의는 필연적으로 메타타입(또는 메타클래스)으로 이어집니다. 메타타입은 Python에서 전혀 새로운 것이 아닙니다. Python은 언제나 타입의 타입에 대해 이야기할 수 있었습니다:
>>> a = 0
>>> type(a)
<type 'int'>
>>> type(type(a))
<type 'type'>
>>> type(type(type(a)))
<type 'type'>
>>>
이 예제에서 type(a)는 “일반” 타입이고, type(type(a))는 메타타입입니다. 배포된 상태에서는 모든 타입이 같은 메타타입(PyType_Type이며, 이것은 자기 자신의 메타타입이기도 합니다)을 가지지만, 이것이 필수 요건은 아닙니다. 실제로 유용하고 관련성 있는 서드파티 확장(Jim Fulton의 ExtensionClasses)은 추가 메타타입을 생성합니다. types.ClassType으로 알려진 클래식 클래스의 타입 역시 별개의 메타타입으로 간주할 수 있습니다.
메타타입과 밀접하게 연관된 기능으로 “Don Beaudry 훅”이 있습니다. 이는 메타타입이 호출 가능 객체라면 그 인스턴스(일반 타입)를 Python class 문으로 서브클래싱(실제로는 서브타이핑)할 수 있다는 규칙입니다. 저는 이 규칙을 사용하여 내장 타입의 서브타이핑을 지원할 것이며, 실제로 클래스를 생성할 때 항상 메타타입을 호출하기만 하도록 하면 클래스 생성 로직이 크게 단순해집니다. 베이스 클래스가 지정되지 않으면 기본 메타타입이 호출됩니다. 기본 메타타입은 “ClassType” 객체이므로, 일반적인 경우 class 문은 이전과 같이 동작합니다. (이 기본값은 전역 변수 __metaclass__를 설정하여 모듈별로 변경할 수 있습니다.)
Python은 메타타입 또는 메타클래스 개념을 Smalltalk와는 다른 방식으로 사용합니다. Smalltalk-80에는 일반 클래스의 계층 구조를 그대로 반영하는 메타클래스 계층 구조가 있고, 메타클래스는 클래스와 1대1로 대응하며(계층 구조의 루트에서 일어나는 몇 가지 기묘한 부분은 제외합니다), 각 class 문은 일반 클래스와 그 메타클래스를 모두 생성하여 클래스 메서드는 메타클래스에, 인스턴스 메서드는 일반 클래스에 둡니다.
Smalltalk의 맥락에서는 이것이 훌륭할 수도 있지만, Python에서 메타타입을 사용해 온 전통적인 방식과는 호환되지 않으며, 저는 Python의 방식을 이어가는 것을 선호합니다. 이는 Python 메타타입이 일반적으로 C로 작성되며, 여러 일반 타입이 공유할 수 있다는 뜻입니다. (Python에서 메타타입의 서브타입을 만드는 것도 가능해질 것이므로 메타타입을 사용하기 위해 반드시 C를 작성해야 하는 것은 아니지만, Python 메타타입의 능력은 제한될 것입니다. 예를 들어, Python 코드가 원시 메모리를 할당하고 이를 마음대로 초기화하는 것은 결코 허용되지 않을 것입니다.)
메타타입은 타입에 대한 다양한 정책을 결정합니다. 예를 들어 타입이 호출될 때 무슨 일이 일어나는지, 타입이 얼마나 동적인지(타입이 생성된 후에 그 타입의 __dict__를 수정할 수 있는지), 메서드 결정 순서가 무엇인지, 인스턴스 속성을 어떻게 조회하는지 등을 결정합니다.
저는 다중 상속을 최대한 활용하고자 할 때 왼쪽에서 오른쪽으로 진행하는 깊이 우선 방식이 최선의 해법이 아니라고 주장할 것입니다.
저는 다중 상속에서 서브타입의 메타타입이 모든 베이스 타입의 메타타입들의 자손이어야 한다고 주장할 것입니다.
메타타입에 대해서는 나중에 다시 다루겠습니다.
타입을 그 인스턴스의 팩토리로 만들기
전통적으로 각 타입에는 해당 타입의 인스턴스를 생성하는 C 팩토리 함수가 적어도 하나 있습니다(PyTuple_New(), PyInt_FromLong() 등). 이러한 팩토리 함수는 객체를 위한 메모리 할당과 그 메모리의 초기화를 모두 담당합니다. Python 2.0부터는 타입이 가비지 컬렉션에 참여하기로 한 경우 이 팩토리 함수가 가비지 컬렉션 서브시스템과도 인터페이스해야 합니다(가비지 컬렉션 참여는 선택 사항이지만, 이른바 “컨테이너” 타입, 즉 다른 객체에 대한 참조를 담을 수 있어 참조 순환에 관여할 수 있는 타입에는 강력히 권장됩니다).
이 제안에서는 타입 객체가 자기 인스턴스의 팩토리 함수가 될 수 있으므로, 타입은 Python에서 직접 호출할 수 있는 호출 가능 객체가 됩니다. 이는 클래스가 인스턴스화되는 방식을 모방한 것입니다. 다양한 내장 타입의 인스턴스를 생성하기 위한 C API는 계속 유효하며, 경우에 따라서는 더 효율적입니다. 모든 타입이 자기 자신의 팩토리 함수가 되는 것은 아닙니다.
타입 객체에는 새로운 슬롯인 tp_new가 있으며, 이 슬롯은 해당 타입 인스턴스의 팩토리 역할을 할 수 있습니다. 이제 타입은 호출 가능 객체입니다. 메타타입인 PyType_Type에 tp_call 슬롯이 설정되어 있기 때문입니다. 이 함수는 호출되는 타입의 tp_new 슬롯을 찾습니다.
설명: 일반 타입 객체(PyInt_Type이나 PyList_Type 등)의 tp_call 슬롯은 그 타입의 인스턴스가 호출될 때 어떤 일이 일어나는지를 정의합니다. 특히 함수 타입 PyFunction_Type의 tp_call 슬롯은 함수를 호출 가능 객체로 만드는 핵심입니다. 또 다른 예로, 정수는 호출 가능 객체가 아니므로 PyInt_Type.tp_call은 NULL입니다. 새로운 패러다임은 타입 객체를 호출 가능 객체로 만듭니다. 타입 객체는 자신의 메타타입(PyType_Type)의 인스턴스이므로, 메타타입의 tp_call 슬롯(PyType_Type.tp_call)은 어떤 타입 객체든 호출될 때 실행되는 함수를 가리킵니다. 그런데 각 타입은 자신의 인스턴스를 생성하려면 저마다 다른 일을 해야 하므로, PyType_Type.tp_call은 호출되는 타입의 tp_new 슬롯에 곧바로 처리를 넘깁니다. PyType_Type 자체도 호출 가능 객체이며, 그 tp_new 슬롯은 새로운 타입을 생성합니다. class 문이 이것을 사용합니다(위에서 설명한 Don Beaudry 훅을 공식화한 것입니다). 그렇다면 PyType_Type을 호출 가능 객체로 만드는 것은 무엇입니까? 그것의 메타타입의 tp_call 슬롯입니다 – 하지만 그것은 자기 자신의 메타타입이므로, 그 슬롯은 바로 자기 자신의 tp_call 슬롯입니다!
타입의 tp_new 슬롯이 NULL이면 예외가 발생합니다. 그렇지 않으면 tp_new 슬롯이 호출됩니다. tp_new 슬롯의 시그니처는 다음과 같습니다
PyObject *tp_new(PyTypeObject *type,
PyObject *args,
PyObject *kwds)
여기서 ‘type’은 tp_new 슬롯이 호출되는 타입이고, ‘args’와 ‘kwds’는 호출에 전달된 위치 인자와 키워드 인자로, tp_call에서 변경 없이 그대로 전달됩니다. (‘type’ 인자는 상속과 함께 사용됩니다. 아래를 참조하십시오.)
반환되는 객체의 타입에는 제약이 없지만, 관례상 주어진 타입의 인스턴스여야 합니다. 반드시 새 객체를 반환할 필요는 없으며, 기존 객체에 대한 참조를 반환해도 됩니다. 반환값은 항상 호출자가 소유하는 새 참조여야 합니다.
tp_new 슬롯이 객체를 반환하면, 결과 객체 타입의 tp_init() 슬롯이 NULL이 아닌 경우 이를 호출하여 추가 초기화를 시도합니다. 이 슬롯의 시그니처는 다음과 같습니다:
int tp_init(PyObject *self,
PyObject *args,
PyObject *kwds)
이는 클래식 클래스의 __init__() 메서드에 더 가깝게 대응하며, 실제로 슬롯/특수 메서드 대응 규칙에 따라 그 메서드에 매핑됩니다. tp_new() 슬롯과 tp_init() 슬롯의 책임 차이는 각각이 보장하는 불변 조건에 있습니다. tp_new() 슬롯은 가장 본질적인 불변 조건, 즉 그것이 없으면 객체를 구현하는 C 코드가 망가지는 불변 조건만 보장해야 합니다. tp_init() 슬롯은 오버라이드할 수 있는 사용자별 초기화에 사용해야 합니다. 딕셔너리 타입을 예로 들어 봅시다. 그 구현에는 해시 테이블을 가리키는 내부 포인터가 있으며, 이 포인터는 절대 NULL이어서는 안 됩니다. 이 불변 조건은 딕셔너리의 tp_new() 슬롯이 처리합니다. 반면, 딕셔너리의 tp_init() 슬롯은 전달된 인자를 바탕으로 딕셔너리에 초기 키와 값의 집합을 부여하는 데 사용할 수 있습니다.
불변 객체 타입의 경우 tp_init() 슬롯에서 초기화를 할 수 없다는 점에 유의하십시오. 그렇게 하면 Python 사용자에게 초기화를 변경할 수 있는 방법을 제공하게 되기 때문입니다. 따라서 불변 객체는 일반적으로 비어 있는 tp_init() 구현을 가지며, 모든 초기화를 tp_new() 슬롯에서 수행합니다.
tp_new() 슬롯이 왜 tp_init() 슬롯을 직접 호출해서는 안 되는지 궁금할 수 있습니다. 그 이유는 특정 상황(예: 영속 객체 지원)에서는 특정 타입의 객체를 필요 이상으로 초기화하지 않고 생성할 수 있는 것이 중요하기 때문입니다. 이는 tp_init()을 호출하지 않고 tp_new() 슬롯만 호출하여 간편하게 수행할 수 있습니다. 또한 tp_init()이 호출되지 않거나 두 번 이상 호출될 수도 있습니다 – 이러한 비정상적인 경우에도 그 동작은 견고해야 합니다.
일부 객체의 경우 tp_new()가 기존 객체를 반환할 수 있습니다. 예를 들어, 정수의 팩토리 함수는 -1부터 99까지의 정수를 캐시합니다. 이는 tp_new()에 전달된 타입 인자가 tp_new() 함수를 정의한 타입이고(이 예에서는 type == &PyInt_Type인 경우), 이 타입의 tp_init() 슬롯이 아무 일도 하지 않을 때만 허용됩니다. 타입 인자가 다르다면, 그 tp_new() 호출은 파생 타입의 tp_new()가 객체를 생성하고 객체의 베이스 타입 부분을 초기화하기 위해 시작한 것입니다. 이 경우 tp_new()는 항상 새 객체를 반환해야 합니다(또는 예외를 발생시켜야 합니다).
tp_new()와 tp_init()는 모두 정확히 같은 ‘args’ 및 ‘kwds’ 인자를 받아야 하며, 서로 독립적으로 호출될 수 있으므로 둘 다 인자가 허용 가능한지 검사해야 합니다.
객체 생성과 관련된 세 번째 슬롯으로 tp_alloc()이 있습니다. 이 슬롯은 객체에 필요한 메모리를 할당하고, 참조 횟수(ob_refcnt)와 타입 포인터(ob_type)를 초기화하며, 객체의 나머지 부분을 모두 0으로 초기화하는 역할을 맡습니다. 또한 타입이 가비지 컬렉션을 지원한다면 객체를 가비지 컬렉션 서브시스템에 등록해야 합니다. 이 슬롯은 파생 타입이 메모리 할당 정책(예를 들어 어느 힙을 사용할지)을 초기화 코드와 별도로 재정의할 수 있도록 하기 위해 존재합니다. 시그니처는 다음과 같습니다.:
PyObject *tp_alloc(PyTypeObject *type, int nitems)
타입 인자는 새 객체의 타입입니다. nitems 인자는 보통 0이며, 할당 크기가 가변적인 객체(기본적으로 문자열, 튜플, long)만 예외입니다. 할당 크기는 다음 식으로 주어집니다.:
type->tp_basicsize + nitems * type->tp_itemsize
tp_alloc 슬롯은 서브클래스를 만들 수 있는 타입에서만 사용됩니다. 베이스 클래스의 tp_new() 함수는 첫 번째 인자로 전달된 타입의 tp_alloc() 슬롯을 호출해야 합니다. 항목 수를 계산하는 것은 tp_new() 함수의 책임입니다. type->tp_itemsize 멤버가 0이 아니면 tp_alloc() 슬롯이 새 객체의 ob_size 멤버를 설정합니다.
(참고: 일부 디버깅 컴파일 모드에서는 타입 구조체에 할당 및 해제 횟수를 세는 카운터로 tp_alloc이라는 이름의 멤버와 tp_free 슬롯이 이미 있었습니다. 이들은 tp_allocs와 tp_deallocs로 이름이 바뀌었습니다.)
tp_alloc()과 tp_new()의 표준 구현이 제공됩니다. PyType_GenericAlloc()은 표준 힙에서 객체를 할당하고 올바르게 초기화합니다. 이 함수는 위의 공식을 사용해 할당할 메모리 양을 결정하며, GC 등록도 처리합니다. 이 구현을 사용하지 않을 유일한 이유는 다른 힙에서 객체를 할당하려는 경우일 것입니다(int나 튜플처럼 매우 작고 자주 사용되는 일부 객체가 그렇게 합니다). PyType_GenericNew()가 더하는 것은 거의 없습니다. 이 함수는 단지 nitems를 0으로 하여 타입의 tp_alloc() 슬롯을 호출할 뿐입니다. 하지만 모든 초기화를 tp_init() 슬롯에서 수행하는 가변 타입에는 이것이 딱 알맞을 수 있습니다.
서브타이핑을 위한 타입 준비
서브타이핑의 기본 개념은 C++의 단일 상속과 매우 비슷합니다. 베이스 타입은 구조체 선언(C++의 클래스 선언과 비슷함)과 타입 객체(C++의 vtable과 비슷함)로 기술됩니다. 파생 타입은 구조체를 확장할 수 있고(단, 베이스 구조체 멤버의 이름, 순서, 타입은 바꾸지 않고 그대로 두어야 합니다), 타입 객체의 특정 슬롯을 재정의하고 나머지는 그대로 둘 수 있습니다. (C++ vtable과 달리 모든 Python 타입 객체는 동일한 메모리 레이아웃을 가집니다.)
베이스 타입은 다음을 수행해야 합니다.
tp_flags에 플래그 값Py_TPFLAGS_BASETYPE을 추가합니다.tp_new(),tp_alloc()및 선택적인tp_init()슬롯을 선언하고 사용합니다.tp_dealloc()과tp_free()를 선언하고 사용합니다.- 객체 구조체 선언을 외부에 공개합니다.
- 서브타이핑을 인식하는 타입 검사 매크로를 내보냅니다.
tp_new(), tp_alloc(), tp_init()의 요구 사항과 시그니처는 이미 위에서 논의했습니다. tp_alloc()은 메모리를 할당하고 대부분 0으로 초기화해야 합니다. tp_new()는 tp_alloc() 슬롯을 호출한 다음 최소한으로 필요한 초기화를 수행해야 합니다. tp_init()은 가변 객체를 더 폭넓게 초기화하는 데 사용해야 합니다.
객체의 수명이 끝날 때에도 비슷한 규칙이 있다는 것은 그리 놀라운 일이 아닙니다. 관련된 슬롯은 tp_dealloc() (Python 확장 타입을 구현해 본 사람이라면 누구나 익숙한 슬롯)과 새로 등장한 tp_free()입니다. (두 이름은 완전히 대칭을 이루지는 않습니다. tp_free()가 tp_alloc()에 대응하는 것은 괜찮지만, tp_dealloc()은 tp_new()에 대응합니다. tp_dealloc 슬롯의 이름을 바꿔야 하지 않겠습니까?)
tp_free() 슬롯은 메모리를 해제하고 가비지 컬렉션 서브시스템에서 객체의 등록을 해제하는 데 사용해야 하며, 파생 클래스가 오버라이드할 수 있습니다. tp_dealloc()은 객체의 초기화를 해제한 다음(보통 여러 하위 객체에 대해 Py_XDECREF()를 호출합니다) tp_free()를 호출하여 메모리를 해제해야 합니다. tp_dealloc()의 시그니처는 예전과 같습니다:
void tp_dealloc(PyObject *object)
tp_free()의 시그니처도 같습니다:
void tp_free(PyObject *object)
(이 PEP의 이전 버전에서는 tp_clear() 슬롯에도 맡은 역할이 있었습니다. 이것은 좋지 않은 생각으로 판명되었습니다.)
C에서 유용하게 서브타입을 만들 수 있으려면, 타입은 인스턴스의 구조체 선언을 헤더 파일을 통해 내보내야 합니다. 서브타입을 파생하는 데 이 선언이 필요하기 때문입니다. 베이스 타입의 타입 객체도 내보내야 합니다.
베이스 타입에 타입 검사 매크로(PyDict_Check() 등)가 있다면, 이 매크로가 서브타입도 인식하도록 만들어야 합니다. 이는 새로운 PyObject_TypeCheck(object, type) 매크로를 사용하여 할 수 있으며, 이 매크로는 베이스 클래스 링크를 따라가는 함수를 호출합니다.
PyObject_TypeCheck() 매크로에는 약간의 최적화가 들어 있습니다. 먼저 object->ob_type을 type 인자와 직접 비교하고, 일치하면 함수 호출을 건너뜁니다. 이 정도면 대부분의 상황에서 충분히 빠를 것입니다.
타입 검사 매크로가 이렇게 바뀌면, 베이스 타입의 인스턴스를 요구하는 C 함수가 파생 타입의 인스턴스로 호출될 수 있다는 점에 유의하십시오. 특정 타입의 서브타이핑을 허용하기 전에, 그 타입의 코드를 검사하여 이 변경으로 문제가 생기지 않는지 확인해야 합니다. 프로토타입에서는 내장 Python 객체 타입에 대해 타입이 정확히 일치하는지도 검사하는 타입 검사 매크로를 하나 더 추가하는 것이 유용한 것으로 드러났습니다(예를 들어, PyDict_Check(x)는 x가 딕셔너리이거나 딕셔너리 서브클래스의 인스턴스이면 참이지만, PyDict_CheckExact(x)는 x가 딕셔너리일 때만 참입니다).
C에서 내장 타입의 서브타입 만들기
가장 단순한 형태의 서브타이핑은 C에서의 서브타이핑입니다. 이것이 가장 단순한 형태인 이유는 C 코드가 일부 문제를 인지하도록 요구할 수 있고, 규칙을 따르지 않는 C 코드가 코어 덤프를 일으켜도 괜찮기 때문입니다. 더욱 단순하게 하기 위해 단일 상속으로 제한합니다.
tp_itemsize가 0인 가변 베이스 타입에서 파생한다고 가정합시다. 서브타입 코드는 GC를 인식하지 않지만, 베이스 타입으로부터 GC 인식 기능을 상속받을 수는 있습니다(이는 자동으로 이루어집니다). 베이스 타입의 할당은 표준 힙을 사용합니다.
파생 타입은 먼저 베이스 타입의 구조체를 포함하는 타입 구조체를 선언합니다. 예를 들어, 다음은 내장 리스트 타입의 서브타입에 대한 타입 구조체입니다:
typedef struct {
PyListObject list;
int state;
} spamlistobject;
베이스 타입 구조체 멤버(여기서는 PyListObject)는 구조체의 첫 번째 멤버여야 한다는 점에 유의하십시오. 그 뒤에 오는 멤버는 모두 추가된 멤버입니다. 또한 베이스 타입을 포인터로 참조하지 않는다는 점에도 유의하십시오. 베이스 타입 구조체의 실제 내용을 포함해야 합니다! (서브타입 인스턴스 앞부분의 메모리 레이아웃이 베이스 타입 인스턴스의 메모리 레이아웃과 같아지도록 하는 것이 목적입니다.)
다음으로, 파생 타입은 타입 객체를 선언하고 초기화해야 합니다. 타입 객체의 슬롯 대부분은 0으로 초기화해도 됩니다. 0은 베이스 타입의 슬롯을 그 자리에 복사해야 한다는 신호입니다. 올바르게 초기화해야 하는 슬롯은 다음과 같습니다:
- 객체 헤더는 평소처럼 채워야 하며, 타입은
&PyType_Type이어야 합니다. - tp_basicsize 슬롯은 서브타입 인스턴스 구조체의 크기로 설정해야 합니다(위 예에서는
sizeof(spamlistobject)). - tp_base 슬롯은 베이스 타입의 타입 객체 주소로 설정해야 합니다.
- 파생 슬롯이 포인터 멤버를 정의한다면
tp_dealloc슬롯 함수를 특별히 주의해서 다뤄야 하며, 자세한 내용은 아래를 참고하십시오. 포인터 멤버가 없다면 이 슬롯을 0으로 설정하여 베이스 타입의 할당 해제 함수를 상속할 수 있습니다. tp_flags슬롯은 통상적인Py_TPFLAGS_DEFAULT값으로 설정해야 합니다.tp_name슬롯은 반드시 설정해야 하며,tp_doc도 설정할 것을 권장합니다(이 슬롯들은 상속되지 않습니다).
서브타입이 추가 구조체 멤버를 정의하지 않는다면(새로운 데이터 없이 새로운 동작만 정의한다면), tp_basicsize 슬롯과 tp_dealloc 슬롯은 0으로 두어도 됩니다.
서브타입의 tp_dealloc 슬롯은 특별히 주의해서 다뤄야 합니다. 객체의 할당을 해제할 때 DECREF하거나 해제해야 하는 추가 포인터 멤버를 파생 타입이 정의하지 않는다면, 이 슬롯을 0으로 설정해도 됩니다. 그런 멤버가 있다면 서브타입의 tp_dealloc() 함수는 모든 PyObject * 멤버에 대해 Py_XDECREF()를 호출하고, 자신이 소유한 그 밖의 포인터에 대해서는 알맞은 메모리 해제 함수를 호출한 다음, 베이스 클래스의 tp_dealloc() 슬롯을 호출해야 합니다. 이 호출은 베이스 타입의 타입 구조체를 통해 이루어져야 합니다. 예를 들어 표준 리스트 타입에서 파생하는 경우는 다음과 같습니다:
PyList_Type.tp_dealloc(self);
서브타입이 베이스 타입과 다른 할당 힙을 사용하려면, 서브타입은 tp_alloc() 슬롯과 tp_free() 슬롯을 모두 재정의해야 합니다. 이 슬롯들은 각각 베이스 클래스의 tp_new() 슬롯과 tp_dealloc() 슬롯이 호출합니다.
타입의 초기화를 완료하려면 PyType_InitDict()를 호출해야 합니다. 이 함수는 서브타입에서 0으로 초기화된 슬롯을 대응하는 베이스 타입 슬롯의 값으로 대체합니다. (또한 타입의 딕셔너리인 tp_dict를 채우고, 타입 객체에 필요한 그 밖의 여러 초기화도 수행합니다.)
서브타입은 PyType_InitDict()가 호출되기 전까지는 사용할 수 없습니다. 서브타입이 모듈에 속한다면 이 호출은 모듈 초기화 중에 하는 것이 가장 좋습니다. (특정 모듈에 속하지 않는) Python 코어에 추가되는 서브타입의 경우에는 대안으로 생성자 함수에서 서브타입을 초기화할 수 있습니다. PyType_InitDict()를 두 번 이상 호출해도 됩니다. 두 번째 이후의 호출은 아무런 효과가 없습니다. 불필요한 호출을 피하려면 tp_dict==NULL인지 검사할 수 있습니다.
(Python 인터프리터를 초기화하는 동안 일부 타입은 실제로 초기화되기 전에 사용됩니다. 실제로 필요한 슬롯, 특히 tp_dealloc이 초기화되어 있는 한 이 방식은 동작하지만, 불안정하므로 일반적인 관행으로는 권장하지 않습니다.)
서브타입 인스턴스를 생성할 때는 서브타입의 tp_new() 슬롯을 호출합니다. 이 슬롯은 먼저 베이스 타입의 tp_new() 슬롯을 호출한 다음, 서브타입의 추가 데이터 멤버를 초기화해야 합니다. 인스턴스를 더 초기화하려면 일반적으로 tp_init() 슬롯을 호출합니다. tp_new() 슬롯은 tp_init() 슬롯을 호출해서는 안 된다는 점에 유의하십시오. tp_init() 슬롯을 호출할지는 tp_new()의 호출자(일반적으로 팩토리 함수)가 정합니다. tp_init() 슬롯을 호출하지 않는 것이 적절한 상황도 있습니다.
서브타입이 tp_init() 슬롯을 정의한다면, 그 tp_init() 슬롯은 일반적으로 먼저 베이스 타입의 tp_init() 슬롯을 호출해야 합니다.
(XXX 여기에 인자 전달에 관한 문단이 한두 개 있어야 합니다.)
Python에서의 서브타이핑
다음 단계는 Python에서 class 문을 통해 선택된 내장 타입의 서브타이핑을 허용하는 것입니다. 지금은 단일 상속으로 한정하여, 간단한 class 문에서 일어나는 일을 살펴보면 다음과 같습니다:
class C(B):
var1 = 1
def method1(self): pass
# etc.
class 문의 본문은 새로운 환경(기본적으로 지역 네임스페이스로 사용되는 새 딕셔너리)에서 실행되고, 그다음 C가 생성됩니다. 다음은 C가 생성되는 방식을 설명합니다.
B가 타입 객체라고 가정합시다. 타입 객체도 객체이고 모든 객체에는 타입이 있으므로, B에도 타입이 있습니다. B 자체가 타입이므로, B의 타입을 메타타입이라고도 부릅니다. B의 메타타입은 type(B) 또는 B.__class__로 접근할 수 있습니다(후자의 표기법은 타입에 대해서는 새로운 것으로, PEP 252에서 도입되었습니다). 이 메타타입을 M(Metatype의 머리글자)이라고 합시다. class 문은 새로운 타입 C를 생성합니다. C는 B와 마찬가지로 타입 객체가 되므로, C의 생성을 메타타입 M의 인스턴스화로 봅니다. 서브클래스를 생성하기 위해 제공해야 하는 정보는 다음과 같습니다:
- 이름(이 예에서는 문자열 “C”);
- 베이스(B를 담은 원소가 하나인 튜플);
- 클래스 본문을 실행한 결과를 딕셔너리 형태로 나타낸 것(예:
{"var1": 1, "method1": <functionmethod1 at ...>, ...}).
class 문은 결과적으로 다음과 같은 호출을 수행합니다:
C = M("C", (B,), dict)
여기서 dict는 클래스 본문을 실행한 결과로 만들어진 딕셔너리입니다. 다시 말해, 메타타입(M)이 호출됩니다.
이 예에는 베이스가 하나뿐이지만, 여전히 베이스의 (원소가 하나인) 시퀀스를 전달한다는 점에 유의하십시오. 이렇게 하면 다중 상속의 경우와 인터페이스가 일관됩니다.
현재 Python에서는 이것을 고안자의 이름을 따서 “Don Beaudry 훅”이라고 부릅니다. 이것은 베이스 클래스가 일반 클래스가 아닐 때에만 호출되는 예외적인 경우입니다. 일반 베이스 클래스의 경우(또는 베이스 클래스가 지정되지 않은 경우), 현재 Python은 클래스를 위한 C 수준 팩토리 함수인 PyClass_New()를 직접 호출합니다.
새로운 시스템에서는 이것이 변경되어, Python이 항상 메타타입을 결정하고 위에서 설명한 대로 그것을 호출합니다. 하나 이상의 베이스가 주어지면 첫 번째 베이스의 타입이 메타타입으로 사용되고, 베이스가 주어지지 않으면 기본 메타타입이 선택됩니다. 기본 메타타입을 “클래식” 클래스의 메타타입인 PyClass_Type으로 설정함으로써, class 문의 클래식 동작이 유지됩니다. 이 기본값은 전역 변수 __metaclass__를 설정하여 모듈별로 변경할 수 있습니다.
여기에는 두 가지 추가 개선 사항이 있습니다. 첫째, 메타타입을 직접 지정할 수 있으면 유용합니다. 클래스 스위트가 변수 __metaclass__를 정의하면, 그것이 호출할 메타타입입니다. (모듈 수준에서 __metaclass__를 설정하는 것은 베이스 클래스가 없고 명시적인 __metaclass__ 선언도 없는 class 문에만 영향을 준다는 점에 유의하십시오. 반면 클래스 스위트에서 __metaclass__를 설정하면 기본 메타타입을 무조건 재정의합니다.)
둘째, 베이스가 여러 개인 경우 모든 베이스가 같은 메타타입을 가질 필요는 없습니다. 이것을 메타클래스 충돌 [1]이라고 합니다. 일부 메타클래스 충돌은 베이스 집합에서 주어진 다른 모든 메타타입으로부터 파생된 메타타입을 찾아 해결할 수 있습니다. 그러한 메타타입을 찾을 수 없으면 예외가 발생하고 class 문은 실패합니다.
이러한 충돌 해결은 메타타입 생성자에서 구현할 수 있습니다. class 문은 첫 번째 베이스의 메타타입(또는 __metaclass__ 변수로 지정된 메타타입)을 호출할 뿐이며, 이 메타타입의 생성자가 가장 많이 파생된 메타타입을 찾습니다. 그것이 자기 자신이면 그대로 진행하고, 그렇지 않으면 그 메타타입의 생성자를 호출합니다. (궁극의 유연성: 다른 메타타입은 모든 베이스가 같은 메타타입을 가져야 한다거나, 베이스 클래스가 하나뿐이어야 한다거나, 그 밖의 어떤 조건이든 요구하도록 선택할 수 있습니다.)
([1]에서는 주어진 모든 메타클래스의 서브클래스인 새 메타클래스가 자동으로 파생됩니다. 하지만 Python에서 여러 메타클래스의 충돌하는 메서드 정의를 어떻게 병합해야 하는지는 의문의 여지가 있으므로, 저는 이것이 실현 가능하다고 생각하지 않습니다. 필요하다면 사용자가 그러한 메타클래스를 직접 파생시키고 __metaclass__ 변수를 사용해 지정할 수 있습니다. 이 작업을 수행하는 새 메타클래스를 두는 것도 가능합니다.)
M을 호출하려면 M 자체에도 타입, 즉 메타-메타타입이 있어야 한다는 점에 유의하십시오. 그리고 메타-메타타입에도 타입, 즉 메타-메타-메타타입이 있습니다. 이런 식으로 계속됩니다. 이 연쇄는 보통 어느 단계에서 메타타입을 자기 자신의 메타타입으로 만듦으로써 끊어집니다. 실제로 Python에서 일어나는 일이 바로 이것입니다. PyType_Type의 ob_type 참조는 &PyType_Type으로 설정됩니다. 서드파티 메타타입이 없다면 PyType_Type이 Python 인터프리터의 유일한 메타타입입니다.
(이 PEP의 이전 버전에서는 메타 수준이 하나 더 있었고, “turtle”이라는 메타-메타타입이 있었습니다. 이는 불필요한 것으로 판명되었습니다.)
어떤 경우든 C를 생성하는 작업은 M의 tp_new() 슬롯이 수행합니다. 이 슬롯은 “확장된” 타입 구조체를 위한 공간을 할당하며, 이 구조체에는 타입 객체, 보조 구조체(as_sequence 등), 타입 이름을 담은 문자열 객체(타입 객체가 아직 참조하고 있는 동안 이 객체가 할당 해제되지 않도록 보장하기 위함), 그리고 약간의 보조 저장 공간(나중에 설명함)이 들어 있습니다. 몇 가지 중요한 슬롯(예를 들어 tp_name은 타입 이름을 가리키도록 설정됨)을 제외하고 이 저장 공간을 0으로 초기화한 다음, tp_base 슬롯이 B를 가리키도록 설정합니다. 그런 다음 B의 슬롯을 상속하기 위해 PyType_InitDict()가 호출됩니다. 마지막으로 C의 tp_dict 슬롯이 네임스페이스 딕셔너리(M 호출에 전달된 세 번째 인자)의 내용으로 갱신됩니다.
다중 상속
Python class 문은 다중 상속을 지원하며, 우리는 내장 타입이 관련된 다중 상속도 지원할 것입니다.
그러나 몇 가지 제약이 있습니다. C 런타임 아키텍처에서는 몇몇 퇴화된 경우를 제외하면 서로 다른 두 내장 타입의 의미 있는 서브타입을 만드는 것이 현실적으로 불가능합니다. 완전히 일반적인 다중 상속을 지원하도록 C 런타임을 변경하는 것은 코드베이스를 지나치게 크게 뒤엎는 일이 될 것입니다.
서로 다른 내장 타입으로부터의 다중 상속에서 발생하는 주된 문제는 내장 타입의 C 구현이 구조체 멤버에 직접 접근한다는 사실에서 비롯됩니다. C 컴파일러는 객체 포인터에 대한 상대 오프셋을 생성할 뿐이며, 그것으로 끝입니다. 예를 들어 리스트와 딕셔너리 타입 구조체는 각각 서로 다르지만 겹치는 여러 구조체 멤버를 선언합니다. 리스트를 기대하며 객체에 접근하는 C 함수는 딕셔너리를 전달받으면 동작하지 않으며, 그 반대도 마찬가지입니다. 그리고 리스트와 딕셔너리에 접근하는 모든 코드를 다시 작성하지 않고서는 이에 대해 우리가 할 수 있는 일이 별로 없습니다. 이는 너무 많은 작업이 필요하므로 하지 않을 것입니다.
다중 상속의 문제는 충돌하는 구조체 멤버 할당 때문에 발생합니다. Python에서 정의된 클래스는 보통 인스턴스 변수를 구조체 멤버에 저장하지 않습니다. 인스턴스 변수는 인스턴스 딕셔너리에 저장됩니다. 이것이 부분적인 해결책의 열쇠입니다. 다음과 같은 두 클래스가 있다고 가정해 봅시다:
class A(dictionary):
def foo(self): pass
class B(dictionary):
def bar(self): pass
class C(A, B): pass
(여기서 ‘dictionary’는 내장 딕셔너리 객체의 타입으로, type({}) 또는 {}.__class__ 또는 types.DictType이라고도 합니다.) 구조체 레이아웃을 살펴보면, A 인스턴스는 딕셔너리 레이아웃 뒤에 __dict__ 포인터가 오는 레이아웃을 가지며, B 인스턴스도 같은 레이아웃을 가집니다. 구조체 멤버 레이아웃 간에 충돌이 없으므로 이는 문제가 없습니다.
다음은 또 다른 예제입니다:
class X(object):
def foo(self): pass
class Y(dictionary):
def bar(self): pass
class Z(X, Y): pass
(여기서 ‘object’(객체)는 모든 내장 타입의 베이스이며, 그 구조체 레이아웃에는 ob_refcnt와 ob_type 멤버만 있습니다.) 이 예제는 X 인스턴스의 __dict__ 포인터 오프셋이 Y 인스턴스의 오프셋과 다르기 때문에 더 복잡합니다. Z 인스턴스의 __dict__ 포인터는 어디에 있습니까? 답은 __dict__ 포인터의 오프셋이 하드코딩되어 있지 않고 타입 객체에 저장되어 있다는 것입니다.
특정 머신에서 ‘object’ 구조체의 길이가 8바이트이고, ‘딕셔너리’ 구조체가 60바이트이며, 객체 포인터가 4바이트라고 가정해 봅시다. 그러면 X 구조체는 12바이트(객체 구조체 뒤에 __dict__ 포인터가 이어짐)이고, Y 구조체는 64바이트(딕셔너리 구조체 뒤에 __dict__ 포인터가 이어짐)입니다. 이 예제에서 Z 구조체는 Y 구조체와 같은 레이아웃을 가집니다. 각 타입 객체(X, Y, Z)에는 __dict__ 포인터를 찾는 데 사용되는 “__dict__ 오프셋”이 있습니다. 따라서 인스턴스 변수를 조회하는 절차는 다음과 같습니다:
- 인스턴스의 타입을 가져옵니다
- 타입 객체에서
__dict__오프셋을 가져옵니다 - 인스턴스 포인터에
__dict__오프셋을 더합니다 - 그 결과로 얻은 주소를 살펴 딕셔너리 참조를 찾습니다
- 그 딕셔너리에서 인스턴스 변수 이름을 조회합니다
물론 이 절차는 C로만 구현할 수 있으며, 몇 가지 세부 사항은 생략했습니다. 하지만 이를 통해 클래식 클래스에서 사용할 수 있는 것과 비슷한 다중 상속 패턴을 사용할 수 있습니다.
XXX 베이스 클래스 호환성을 판단하는 전체 알고리즘을 여기에 적어야 하지만, 지금은 그럴 여력이 없습니다. 아래에서 언급하는 구현의 typeobject.c에 있는 best_base()를 참고하십시오.
MRO: 메서드 결정 순서(조회 규칙)
다중 상속에는 메서드 결정 순서의 문제가 따릅니다. 메서드 결정 순서란 주어진 이름의 메서드를 찾기 위해 클래스 또는 타입과 그 베이스들을 검색하는 순서입니다.
고전적인 Python에서 이 규칙은 왼쪽에서 오른쪽, 깊이 우선 규칙이라고도 하는 다음 재귀 함수로 주어집니다:
def classic_lookup(cls, name):
if cls.__dict__.has_key(name):
return cls.__dict__[name]
for base in cls.__bases__:
try:
return classic_lookup(base, name)
except AttributeError:
pass
raise AttributeError, name
이 방식의 문제는 “다이아몬드 다이어그램”을 생각해 보면 분명해집니다:
class A:
^ ^ def save(self): ...
/ \
/ \
/ \
/ \
class B class C:
^ ^ def save(self): ...
\ /
\ /
\ /
\ /
class D
화살표는 서브타입에서 그 베이스 type(s)를 가리킵니다. 이 다이어그램은 B와 C가 A에서 파생되고, D가 B와 C에서 (따라서 간접적으로 A에서도) 파생된다는 것을 뜻합니다.
C가 베이스 A에 정의된 메서드 save()를 오버라이드한다고 가정합시다. (C.save()는 아마 A.save()를 호출한 다음 자신의 상태 일부를 저장할 것입니다.) B와 D는 save()를 오버라이드하지 않습니다. D 인스턴스에서 save()를 호출하면 어떤 메서드가 호출됩니까? 클래식 조회 규칙에 따르면 C.save()는 무시되고 A.save()가 호출됩니다!
이는 바람직하지 않습니다. 이렇게 되면 아마 C가 망가지고(C의 상태가 저장되지 않으므로), 애초에 C를 상속한 목적 자체가 무의미해집니다.
고전 Python에서는 왜 이것이 문제가 되지 않았습니까? 고전 Python의 클래스 계층 구조에서는 다이아몬드 다이어그램을 거의 찾아볼 수 없습니다. 대부분의 클래스 계층 구조는 단일 상속을 사용하며, 다중 상속은 보통 믹스인 클래스에 한정됩니다. 사실 여기에서 보인 문제가 아마도 고전 Python에서 다중 상속이 인기가 없는 이유일 것입니다.
새 시스템에서는 왜 이것이 문제가 됩니까? 타입 계층의 최상위에 있는 ‘object’(객체) 타입은 서브타입이 유용하게 확장할 수 있는 여러 메서드를 정의합니다. 예를 들면 __getattr__()이 있습니다.
(여담: 고전 Python에서 __getattr__() 메서드는 실제로는 속성 가져오기 연산의 구현이 아닙니다. 이 메서드는 일반적인 방법으로 속성을 찾을 수 없을 때에만 호출되는 훅입니다. 이 점은 종종 단점으로 지적되어 왔습니다. 어떤 클래스 설계에는 모든 속성 참조에 대해 호출되는 __getattr__() 메서드가 정당하게 필요합니다. 하지만 그렇다면 당연히 이 메서드는 기본 구현을 직접 호출할 수 있어야 합니다. 가장 자연스러운 방법은 기본 구현을 object.__getattr__(self, name) 형태로 사용할 수 있게 하는 것입니다.)
따라서 다음과 같은 고전 클래스 계층 구조는:
class B class C:
^ ^ def __getattr__(self, name): ...
\ /
\ /
\ /
\ /
class D
새 시스템에서는 다이아몬드 다이어그램으로 바뀌며:
object:
^ ^ __getattr__()
/ \
/ \
/ \
/ \
class B class C:
^ ^ def __getattr__(self, name): ...
\ /
\ /
\ /
\ /
class D
원래 다이어그램에서는 C.__getattr__() 메서드가 호출되지만, 고전 조회 규칙을 적용한 새 시스템에서는 object.__getattr__() 메서드가 호출될 것입니다!
다행히도 더 나은 조회 규칙이 있습니다. 설명하기가 조금 어렵지만, 이 규칙은 다이아몬드 다이어그램에서 올바르게 동작하며, 상속 그래프에 다이아몬드가 없을 때(트리일 때)는 고전 조회 규칙과 같습니다.
새 조회 규칙은 상속 다이어그램에 있는 모든 클래스를 탐색될 순서대로 나열한 리스트를 만듭니다. 시간을 절약하기 위해 이 리스트는 클래스 정의 시점에 만들어집니다. 새 조회 규칙을 설명하기 위해, 먼저 고전 조회 규칙에서는 이러한 리스트가 어떤 모습일지 생각해 봅시다. 다이아몬드가 있으면 고전 조회는 일부 클래스를 여러 번 방문한다는 점에 유의하십시오. 예를 들어 위의 ABCD 다이아몬드 다이어그램에서 고전 조회 규칙은 다음 순서로 클래스를 방문합니다:
D, B, A, C, A
리스트에 A가 두 번 나타난다는 점에 주목하십시오. 두 번째로 나타난 A는 불필요합니다. 그곳에서 찾을 수 있는 것은 무엇이든 첫 번째 A를 탐색할 때 이미 발견되었을 것이기 때문입니다.
이 관찰을 이용하여 새 조회 규칙을 설명하겠습니다. 고전 조회 규칙을 사용하여, 중복을 포함해 탐색될 클래스의 리스트를 만듭니다. 이제 리스트에 여러 번 나타나는 각 클래스에 대해, 마지막 것을 제외한 나머지를 모두 제거합니다. 그 결과 리스트에는 각 조상 클래스가 정확히 한 번씩 들어 있습니다(가장 많이 파생된 클래스, 즉 예제의 D도 포함합니다).
이 순서로 메서드를 탐색하면 다이아몬드 다이어그램에서 올바르게 동작합니다. 리스트를 만드는 방식 때문에, 다이아몬드가 없는 상황에서는 탐색 순서가 바뀌지 않습니다.
이것은 하위 호환성을 깨뜨리지 않습니까? 기존 코드가 동작하지 않게 되지 않겠습니까? 모든 클래스의 메서드 결정 순서를 바꾼다면 그렇게 될 것입니다. 그러나 Python 2.2에서 새 조회 규칙은 내장 타입에서 파생된 타입에만 적용되며, 내장 타입에서 파생하는 것 자체가 새로운 기능입니다. 베이스 클래스가 없는 class 문은 “클래식 클래스”를 만들며, 베이스 클래스가 모두 클래식 클래스인 class 문도 마찬가지입니다. 클래식 클래스에는 클래식 조회 규칙이 사용됩니다. (클래식 클래스에서 새 조회 규칙을 시험해 보려면 다른 메타클래스를 명시적으로 지정할 수 있습니다.) 또한 클래스 계층 구조를 분석하여 메서드 결정 순서가 바뀌면 영향을 받을 메서드를 찾아 주는 도구도 제공할 것입니다.
XXX 새 MRO의 동기는 Damian Conway가 제시한 다른 방식으로도 설명할 수 있습니다. 아직 탐색하지 않은 파생 클래스에 메서드가 정의되어 있다면 베이스 클래스에 정의된 메서드는 결코 사용하지 않습니다(예전 탐색 순서에서는 그런 경우가 생깁니다).
XXX 추후 작성 예정
이 PEP에서 논의할 추가 주제:
- 하위 호환성 문제!!!
- 클래스 메서드와 정적 메서드
- 협력 메서드와
super() - 타입 객체 슬롯(tp_foo)과 특수 메서드(
__foo__) 사이의 매핑 (사실 이 내용은 PEP 252에 속할 수도 있습니다) - 내장 타입을 가리키는 내장 이름(object(객체), int, str, list 등)
__dict__와__dictoffset____slots__HEAPTYPE플래그 비트- GC 지원
- 모든 새 함수에 대한 API 문서
__new__사용법- 메타클래스 작성 (
mro()등 사용) - 사용자를 위한 고수준 개요
미해결 문제
__del__이 필요합니까?__dict__,__bases__에 대한 대입- 일관성 없는 명명 (예: tp_dealloc/tp_new/tp_init/tp_alloc/tp_free)
- ‘dictionary’(딕셔너리)에 대한 내장 별칭 ‘dict’를 추가해야 합니까?
- dict/list 등의 서브클래스를 시스템 함수에 전달하면
__getitem__등을 오버라이드한 메서드가 항상 사용되지는 않습니다
구현
이 PEP(및 PEP 252)의 프로토타입 구현은 CVS에서 받을 수 있고, Python 2.2 알파 및 베타 릴리스 시리즈에도 들어 있습니다. 여기에서 설명한 기능의 예는 Lib/test/test_descr.py 파일과 확장 모듈 Modules/xxsubtype.c를 참조하십시오.
참고 문헌
Copyright
This document has been placed in the public domain.