[Final] PEP 252 - Making Types Look More Like Classes
원문 링크: PEP 252 - Making Types Look More Like Classes
상태: Final 유형: Standards Track 작성일: 19-Apr-2001
PEP 252 – 타입을 클래스처럼 보이게 만들기
작성자: Guido van Rossum
요약 (Abstract)
이 PEP는 타입(types)의 인트로스펙션(introspection) API를 변경하여, 타입이 클래스(classes)처럼 보이고 타입의 인스턴스가 클래스 인스턴스처럼 보이게 할 것을 제안합니다. 예를 들어, 대부분의 내장 타입(built-in types)의 경우 type(x)
는 x.__class__
와 동일해집니다. C
가 x.__class__
일 때, x.meth(a)
는 일반적으로 C.meth(x, a)
와 동일하며, C.__dict__
는 x
의 메서드와 다른 속성들을 포함하게 됩니다.
이 PEP는 또한 “속성 디스크립터(attribute descriptors)”, 줄여서 “디스크립터(descriptors)”를 사용하여 속성을 지정하는 새로운 접근 방식을 소개합니다. 디스크립터는 속성을 설명하는 데 사용되는 여러 가지 일반적인 메커니즘을 통합하고 일반화합니다. 디스크립터는 메서드(method), 객체 구조의 타입 필드(typed field), 또는 getter 및 setter 함수로 표현되는 일반화된 속성을 설명할 수 있습니다.
일반화된 디스크립터 API를 기반으로, 이 PEP는 또한 클래스 메서드(class methods)와 스태틱 메서드(static methods)를 선언하는 방법을 소개합니다.
[편집자 주: 이 PEP에 설명된 아이디어는 Python에 통합되었습니다. 이 PEP는 더 이상 구현을 정확하게 설명하지 않습니다.]
서론 (Introduction)
Python의 가장 오래된 언어적 결함(language warts) 중 하나는 클래스와 타입 간의 차이입니다. 예를 들어, 딕셔너리(dictionary) 타입을 직접 서브클래싱(subclass)할 수 없으며, 객체가 어떤 메서드와 인스턴스 변수(instance variables)를 가지고 있는지 알아내는 인트로스펙션 인터페이스가 타입과 클래스에 대해 서로 다릅니다.
클래스/타입 분할을 해결하는 것은 Python이 구현되는 방식의 여러 측면에 영향을 미치기 때문에 큰 노력입니다. 이 PEP는 타입의 인트로스펙션 API를 클래스의 인트로스펙션 API와 동일하게 만드는 데 중점을 둡니다. 다른 PEP들은 클래스를 타입처럼 보이게 하고 내장 타입을 서브클래싱하는 것을 제안할 것이며, 이 주제들은 이 PEP의 의제가 아닙니다.
인트로스펙션 API (Introspection APIs)
인트로스펙션은 객체가 어떤 속성을 가지고 있는지 알아내는 것에 관한 것입니다. Python의 매우 일반적인 getattr
/setattr
API는 특정 객체가 지원하는 모든 속성 목록을 얻을 수 있는 방법이 항상 존재한다고 보장하는 것을 불가능하게 만들지만, 실제로는 거의 모든 객체에 대해 함께 작동하는 두 가지 규칙이 나타났습니다. 저는 이것들을 클래스 기반 인트로스펙션 API (class-based introspection API)와 타입 기반 인트로스펙션 API (type-based introspection API)라고 부르겠습니다. 줄여서 클래스 API와 타입 API입니다.
클래스 기반 인트로스펙션 API는 주로 클래스 인스턴스에 사용되며, Jim Fulton의 ExtensionClasses에서도 사용됩니다. 이 API는 객체 x
의 모든 데이터 속성이 딕셔너리 x.__dict__
에 저장되고, 모든 메서드와 클래스 변수는 x
의 클래스인 x.__class__
를 검사하여 찾을 수 있다고 가정합니다. 클래스에는 메서드와 클래스 자체에 의해 정의된 클래스 변수를 포함하는 딕셔너리를 제공하는 __dict__
속성과, 재귀적으로 검사되어야 하는 기본 클래스(base classes)의 튜플인 __bases__
속성이 있습니다. 여기에는 몇 가지 가정이 있습니다:
- 인스턴스 딕셔너리(instance dict)에 정의된 속성은 객체의 클래스에 의해 정의된 속성을 재정의합니다.
- 파생 클래스(derived class)에 정의된 속성은 기본 클래스에 정의된 속성을 재정의합니다.
__bases__
에서 먼저 나타나는 이전 기본 클래스에 정의된 속성은 나중에 나타나는 기본 클래스에 정의된 속성을 재정의합니다.
(마지막 두 규칙은 종종 속성 검색에 대한 왼쪽에서 오른쪽, 깊이 우선(left-to-right, depth-first) 규칙으로 요약됩니다. 이것은 고전적인 Python 속성 검색 규칙입니다. PEP 253은 속성 검색 순서를 변경할 것을 제안할 것이며, 만약 수락된다면 이 PEP도 따를 것입니다.)
타입 기반 인트로스펙션 API는 대부분의 내장 객체에서 어떤 형태로든 지원됩니다. 이 API는 두 개의 특수 속성인 __members__
와 __methods__
를 사용합니다. __methods__
속성은 존재할 경우 객체가 지원하는 메서드 이름 목록입니다. __members__
속성은 존재할 경우 객체가 지원하는 데이터 속성 이름 목록입니다.
타입 API는 때때로 인스턴스와 동일하게 작동하는 __dict__
와 결합됩니다 (예: Python 2.1의 함수 객체의 경우, f.__dict__
는 f
의 동적 속성을 포함하고, f.__members__
는 f
의 정적으로 정의된 속성 이름을 나열합니다).
일부 주의가 필요합니다: 어떤 객체는 __dict__
나 __doc__
와 같은 “내재적(intrinsic)” 속성을 __members__
에 나열하지 않는 반면, 다른 객체는 나열합니다. 때때로 속성 이름이 __members__
또는 __methods__
와 __dict__
의 키(key) 모두에 나타나는 경우도 있는데, 이 경우 __dict__
에서 찾은 값이 사용되는지 여부는 알 수 없습니다.
타입 API는 한 번도 신중하게 명시된 적이 없습니다. 이는 Python의 통설(folklore)의 일부이며, 대부분의 서드파티 확장(third party extensions)은 이를 지원하는 예시를 따르기 때문에 지원합니다. 또한, tp_getattr
핸들러에서 Py_FindMethod()
및/또는 PyMember_Get()
을 사용하는 모든 타입은 각각 __methods__
및 __members__
속성 이름을 특별히 처리하기 때문에 이를 지원합니다.
Jim Fulton의 ExtensionClasses는 타입 API를 무시하고, 더 강력한 클래스 API를 에뮬레이트합니다. 이 PEP에서는 모든 타입에 클래스 API를 지원하기 위해 타입 API를 단계적으로 폐지할 것을 제안합니다.
클래스 API를 선호하는 한 가지 주장은 타입이 지원하는 속성을 알아내기 위해 인스턴스를 생성할 필요가 없다는 것입니다. 이는 문서 프로세서(documentation processors)에 유용합니다. 예를 들어, socket
모듈은 SocketType
객체를 내보내지만, 현재 socket
객체에 어떤 메서드가 정의되어 있는지 알려주지 않습니다. 클래스 API를 사용하면 SocketType
이 socket
객체의 메서드를 정확히 보여주며, 소켓을 생성하지 않고도 해당 독스트링(docstrings)을 추출할 수 있습니다. (이것은 C 확장 모듈이므로, 이 경우 소스 스캐닝 방식의 독스트링 추출은 실현 가능하지 않습니다.)
클래스 기반 인트로스펙션 API의 명세 (Specification of the class-based introspection API)
객체는 정적(static) 속성과 동적(dynamic) 속성의 두 가지 종류의 속성을 가질 수 있습니다. 정적 속성의 이름과 때로는 다른 속성들은 obj.__class__
또는 type(obj)
를 통해 접근할 수 있는 객체의 타입 또는 클래스(메타-객체)를 검사하여 알 수 있습니다. (저는 여기에서 type
과 class
를 상호 교환적으로 사용합니다. 둘 다에 해당하는 서투르지만 설명적인 용어는 “메타-객체”입니다.)
(XXX “static” 및 “dynamic”은 여기서 사용하기 좋은 용어가 아닙니다. 왜냐하면 “static” 속성도 실제로는 매우 동적으로 동작할 수 있고, C++ 또는 Java의 static 클래스 멤버와는 아무 관련이 없기 때문입니다. Barry는 대신 “immutable” 및 “mutable”을 사용할 것을 제안했지만, 이 단어들은 이미 약간 다른 맥락에서 정확하고 다른 의미를 가지고 있으므로 여전히 혼란스러울 것이라고 생각합니다.)
동적 속성의 예시로는 클래스 인스턴스의 인스턴스 변수, 모듈 속성 등이 있습니다. 정적 속성의 예시로는 리스트(lists) 및 딕셔너리(dictionaries)와 같은 내장 객체의 메서드, 그리고 프레임(frame) 및 코드(code) 객체의 속성 (f.f_code
, c.co_filename
등)이 있습니다. 동적 속성을 가진 객체가 __dict__
속성을 통해 이들을 노출할 때, __dict__
는 정적 속성입니다.
동적 속성의 이름과 값은 일반적으로 딕셔너리에 저장되며, 이 딕셔너리는 일반적으로 obj.__dict__
로 접근할 수 있습니다. 이 명세의 나머지 부분은 동적 속성보다는 정적 속성의 이름과 속성을 찾는 데 더 중점을 둡니다. 동적 속성은 obj.__dict__
를 검사하여 쉽게 찾을 수 있습니다.
아래 논의에서 저는 두 가지 종류의 객체를 구분합니다: 일반 객체(regular objects) (예: 리스트, 정수, 함수)와 메타-객체(meta-objects). 타입과 클래스는 메타-객체입니다. 메타-객체도 일반 객체이지만, 주로 일반 객체의 __class__
속성(또는 다른 메타-객체의 __bases__
속성)에 의해 참조되기 때문에 중요하게 다룹니다.
클래스 인트로스펙션 API는 다음 요소들로 구성됩니다:
- 일반 객체의
__class__
및__dict__
속성 - 메타-객체의
__bases__
및__dict__
속성 - 우선순위 규칙 (precedence rules)
- 속성 디스크립터 (attribute descriptors)
이들은 함께 메타-객체에 의해 정의된 모든 속성에 대해 알려줄 뿐만 아니라, 주어진 객체의 특정 속성 값을 계산하는 데도 도움을 줍니다.
일반 객체의 __dict__
속성 (The __dict__
attribute on regular objects)
일반 객체는 __dict__
속성을 가질 수 있습니다. 만약 그렇다면, 이는 최소한 __getitem__()
, keys()
, has_key()
를 지원하는 매핑(mapping)이어야 합니다 (반드시 딕셔너리일 필요는 없습니다). 이것은 객체의 동적 속성을 제공합니다. 매핑의 키는 속성 이름을 제공하고, 해당 값은 그 값을 제공합니다.
일반적으로, 주어진 이름의 속성 값은 __dict__
에서 해당 이름을 키로 하는 값과 동일한 객체입니다. 즉, obj.__dict__['spam']
은 obj.spam
입니다. (그러나 아래의 우선순위 규칙을 참조하십시오. 동일한 이름을 가진 정적 속성이 딕셔너리 항목을 재정의할 수 있습니다.)
일반 객체의 __class__
속성 (The __class__
attribute on regular objects)
일반 객체는 일반적으로 __class__
속성을 가집니다. 만약 그렇다면, 이것은 메타-객체를 참조합니다. 메타-객체는 __class__
가 참조하는 일반 객체에 대한 정적 속성을 정의할 수 있습니다. 이것은 일반적으로 다음 메커니즘을 통해 이루어집니다:
메타-객체의 __dict__
속성 (The __dict__
attribute on meta-objects)
메타-객체는 일반 객체의 __dict__
속성과 동일한 형태의 __dict__
속성을 가질 수 있습니다 (매핑이지만 반드시 딕셔너리일 필요는 없습니다). 만약 그렇다면, 메타-객체의 __dict__
의 키는 해당 일반 객체에 대한 정적 속성의 이름입니다. 값은 속성 디스크립터(attribute descriptors)입니다. 이것은 나중에 설명할 것입니다. 언바운드 메서드(unbound method)는 속성 디스크립터의 특수 케이스입니다.
메타-객체도 일반 객체이기 때문에, 메타-객체의 __dict__
에 있는 항목은 메타-객체의 속성과 일치합니다. 그러나 일부 변환이 적용될 수 있으며, 베이스(bases, 아래 참조)가 추가적인 동적 속성을 정의할 수 있습니다. 즉, mobj.spam
이 항상 mobj.__dict__['spam']
인 것은 아닙니다. (이 규칙에는 클래스의 경우 C.__dict__['spam']
이 함수인 경우 C.spam
은 언바운드 메서드 객체라는 허점이 있습니다.)
메타-객체의 __bases__
속성 (The __bases__
attribute on meta-objects)
메타-객체는 __bases__
속성을 가질 수 있습니다. 만약 그렇다면, 이것은 다른 메타-객체들(베이스)의 시퀀스(sequence)여야 합니다 (반드시 튜플일 필요는 없습니다). __bases__
가 없는 것은 빈 베이스 시퀀스와 동일합니다. __bases__
속성에 의해 정의된 메타-객체들 사이의 관계에는 순환(cycle)이 없어야 합니다. 즉, __bases__
속성은 파생된 메타-객체에서 그들의 기본 메타-객체로 향하는 아크(arcs)를 가진 방향성 비순환 그래프(directed acyclic graph)를 정의합니다. (여러 클래스가 동일한 기본 클래스를 가질 수 있으므로 반드시 트리(tree)일 필요는 없습니다.) 상속 그래프(inheritance graph) 내의 메타-객체의 __dict__
속성은 __class__
속성이 상속 트리의 루트(root)를 가리키는 일반 객체에 대한 속성 디스크립터를 제공합니다 (이는 상속 계층 구조의 루트와 동일하지 않습니다. 오히려 상속 트리가 일반적으로 그려지는 방식에 따라 맨 아래에 가깝습니다). 디스크립터는 먼저 루트 메타-객체의 딕셔너리에서 검색된 다음, 우선순위 규칙(아래 다음 단락 참조)에 따라 해당 베이스에서 검색됩니다.
우선순위 규칙 (Precedence rules)
주어진 일반 객체에 대한 상속 그래프 내의 두 메타-객체가 동일한 이름의 속성 디스크립터를 모두 정의할 때, 검색 순서는 메타-객체에 달려 있습니다. 이를 통해 다른 메타-객체가 다른 검색 순서를 정의할 수 있습니다. 특히, 고전적인 클래스는 오래된 왼쪽에서 오른쪽, 깊이 우선 규칙을 사용하는 반면, 새 스타일 클래스(new-style classes)는 더 고급 규칙을 사용합니다 (PEP 253의 메서드 결정 순서(method resolution order) 섹션 참조).
동적 속성(일반 객체의 __dict__
에 정의된 속성)이 정적 속성(일반 객체의 __class__
를 루트로 하는 상속 그래프 내의 메타-객체에 의해 정의된 속성)과 동일한 이름을 가질 때, 정적 속성이 __set__
메서드를 정의하는 디스크립터인 경우 우선순위를 가집니다 (아래 참조). 그렇지 않은 경우 (__set__
메서드가 없는 경우) 동적 속성이 우선순위를 가집니다. 즉, 데이터 속성( __set__
메서드를 가진 속성)의 경우 정적 정의가 동적 정의를 재정의하지만, 다른 속성의 경우 동적 정의가 정적 정의를 재정의합니다.
이론적 근거 (Rationale): “정적이 동적을 재정의한다” 또는 “동적이 정적을 재정의한다”와 같은 간단한 규칙을 가질 수는 없습니다. 왜냐하면 일부 정적 속성은 실제로 동적 속성을 재정의하기 때문입니다. 예를 들어, 인스턴스의 __dict__
에 있는 키 '__class__'
는 정적으로 정의된 __class__
포인터에 의해 무시되지만, 반면에 inst.__dict__
의 대부분의 키는 inst.__class__
에 정의된 속성을 재정의합니다. 디스크립터에 __set__
메서드가 존재한다는 것은 이것이 데이터 디스크립터(data descriptor)임을 나타냅니다. (읽기 전용(read-only) 데이터 디스크립터도 __set__
메서드를 가집니다. 항상 예외를 발생시킵니다.) 디스크립터에 __set__
메서드가 없다는 것은 디스크립터가 할당을 가로채는 데 관심이 없음을 나타내며, 이때는 고전적인 규칙이 적용됩니다: 메서드와 동일한 이름을 가진 인스턴스 변수는 해당 특정 인스턴스에 대해 메서드를 삭제될 때까지 숨깁니다.
속성 디스크립터 (Attribute descriptors)
여기서부터 흥미롭고 복잡해집니다. 속성 디스크립터(줄여서 디스크립터)는 메타-객체의 __dict__
(또는 그 조상 중 하나의 __dict__
)에 저장되며, 두 가지 용도로 사용됩니다. 디스크립터는 (일반, 비-메타) 객체의 해당 속성 값을 가져오거나 설정하는 데 사용될 수 있으며, 문서화 및 인트로스펙션 목적으로 속성을 설명하는 추가 인터페이스를 가집니다.
Python에서는 디스크립터 인터페이스를 설계하는 데 있어 이전의 선행 작업이 거의 없습니다. 값을 가져오거나 설정하는 것, 또는 다른 방식으로 속성을 설명하는 것에 대한 선행 작업이 없었으며, 몇 가지 사소한 속성( __name__
과 __doc__
이 속성의 이름과 독스트링이어야 한다고 가정하는 것이 합리적)을 제외하면 그렇습니다. 저는 아래에서 그러한 API를 제안할 것입니다.
메타-객체의 __dict__
에서 발견된 객체가 속성 디스크립터가 아닌 경우, 하위 호환성(backward compatibility)은 특정 최소한의 의미론을 요구합니다. 이는 기본적으로 Python 함수 또는 언바운드 메서드(unbound method)인 경우 해당 속성이 메서드이고, 그렇지 않은 경우 동적 데이터 속성의 기본값이라는 의미입니다. 하위 호환성은 또한 (__setattr__
메서드가 없는 경우) 메서드에 해당하는 속성에 할당하는 것이 합법적이며, 이것이 해당 특정 인스턴스에 대해 메서드를 가리는 데이터 속성을 생성한다는 것을 요구합니다. 그러나 이러한 의미론은 일반 클래스와의 하위 호환성을 위해서만 필요합니다.
인트로스펙션 API는 읽기 전용(read-only) API입니다. 우리는 특수 속성(__dict__
, __class__
, __bases__
)에 대한 할당의 효과나 __dict__
항목에 대한 할당의 효과를 정의하지 않습니다. 일반적으로 이러한 할당은 금지된 것으로 간주되어야 합니다. 향후 PEP에서 이러한 할당 중 일부에 대한 의미론을 정의할 수 있습니다. (특히 현재 인스턴스는 __class__
및 __dict__
에 대한 할당을 지원하고, 클래스는 __bases__
및 __dict__
에 대한 할당을 지원하기 때문입니다.)
속성 디스크립터 API의 명세 (Specification of the attribute descriptor API)
속성 디스크립터는 다음 속성들을 가질 수 있습니다. 예시에서 x
는 객체이고, C
는 x.__class__
이며, x.meth()
는 메서드이고, x.ivar
는 데이터 속성 또는 인스턴스 변수입니다. 모든 속성은 선택 사항입니다. 즉, 특정 속성은 주어진 디스크립터에 존재할 수도 있고 아닐 수도 있습니다. 속성이 없다는 것은 해당 정보를 사용할 수 없거나 해당 기능이 구현되지 않았음을 의미합니다.
__name__
: 속성 이름. 별칭(aliasing) 및 이름 변경으로 인해 속성은 다른 이름으로 알려질 수 있지만 (추가적으로 또는 독점적으로), 이것은 속성이 생성된 이름입니다. 예시:C.meth.__name__ == 'meth'
.__doc__
: 속성의 문서화 문자열(docstring).None
일 수 있습니다.__objclass__
: 이 속성을 선언한 클래스. 디스크립터는 이 클래스의 인스턴스인 객체(서브클래스의 인스턴스 포함)에만 적용됩니다. 예시:C.meth.__objclass__
는C
입니다.__get__()
: 객체에서 속성 값을 검색하는 한두 개의 인수로 호출 가능한 함수. 이는 메서드 디스크립터의 경우 “바운드 메서드(bound method)” 객체를 반환할 수 있으므로 “바인딩(binding)” 작업이라고도 합니다. 첫 번째 인수X
는 속성을 검색하거나 바인딩해야 하는 객체입니다.X
가None
인 경우, 선택적 두 번째 인수T
는 메타-객체여야 하며 바인딩 작업은T
의 인스턴스로 제한되는 언바운드 메서드(unbound method)를 반환할 수 있습니다.X
와T
가 모두 지정된 경우X
는T
의 인스턴스여야 합니다. 바인딩 작업이 정확히 무엇을 반환하는지는 디스크립터의 의미론에 따라 달라집니다. 예를 들어, 스태틱 메서드와 클래스 메서드(아래 참조)는 인스턴스를 무시하고 대신 타입에 바인딩합니다.__set__()
: 객체에 속성 값을 설정하는 두 개의 인수를 받는 함수. 속성이 읽기 전용인 경우, 이 메서드는TypeError
또는AttributeError
예외를 발생시킬 수 있습니다 (둘 다 허용됩니다. 둘 다 정의되지 않거나 설정할 수 없는 속성에 대해 역사적으로 발견되었기 때문입니다). 예시:C.ivar.set(x, y)
는x.ivar = y
와 동일합니다.
스태틱 메서드 및 클래스 메서드 (Static methods and class methods)
디스크립터 API를 통해 스태틱 메서드(static methods)와 클래스 메서드(class methods)를 추가할 수 있습니다. 스태틱 메서드는 설명하기 쉽습니다. 이들은 C++ 또는 Java의 static 메서드와 거의 동일하게 작동합니다. 다음은 예시입니다:
class C:
def foo(x, y):
print("staticmethod", x, y)
foo = staticmethod(foo)
C.foo(1, 2)
c = C()
c.foo(1, 2)
C.foo(1, 2)
호출과 c.foo(1, 2)
호출 모두 foo()
를 두 개의 인수로 호출하고 “staticmethod 1 2”를 출력합니다. foo()
의 정의에 “self”는 선언되지 않았으며, 호출에 인스턴스가 필요하지 않습니다.
클래스 문(class statement)의 “foo = staticmethod(foo)” 줄이 결정적인 요소입니다. 이것이 foo()
를 스태틱 메서드로 만듭니다. 내장 staticmethod()
는 함수 인수를 __get__()
메서드가 원래 함수를 변경하지 않고 반환하는 특수한 종류의 디스크립터로 래핑(wraps)합니다. 이것이 없으면, 표준 함수 객체의 __get__()
메서드는 c.foo
에 대한 바운드 메서드 객체와 C.foo
에 대한 언바운드 메서드 객체를 생성했을 것입니다.
(XXX Barry는 “static”이라는 단어가 이미 여러 방식으로 오버로드(overloaded)되어 있기 때문에 “staticmethod” 대신 “sharedmethod”를 사용할 것을 제안했습니다. 하지만 shared가 올바른 의미를 전달하는지 확실하지 않습니다.)
클래스 메서드(Class methods)는 호출되는 클래스인 암묵적인 첫 번째 인수를 받는 메서드를 선언하기 위해 유사한 패턴을 사용합니다. 이것은 C++나 Java에 상응하는 것이 없으며, Smalltalk의 클래스 메서드와 완전히 같지는 않지만 유사한 목적을 수행할 수 있습니다. Armin Rigo에 따르면, 이들은 Borland Pascal 방언 Delphi의 “가상 클래스 메서드(virtual class methods)”와 유사합니다. (Python은 또한 실제 메타클래스(metaclasses)를 가지고 있으며, 아마도 메타클래스에 정의된 메서드가 “클래스 메서드”라는 이름에 더 적합할 것입니다. 하지만 대부분의 프로그래머는 메타클래스를 사용하지 않을 것이라고 예상합니다.) 다음은 예시입니다:
class C:
def foo(cls, y):
print("classmethod", cls, y)
foo = classmethod(foo)
C.foo(1)
c = C()
c.foo(1)
C.foo(1)
호출과 c.foo(1)
호출 모두 foo()
를 두 개의 인수로 호출하고 “classmethod __main__.C
1”을 출력합니다. foo()
의 첫 번째 인수는 암묵적이며, 메서드가 인스턴스를 통해 호출되었더라도 클래스입니다. 이제 예시를 계속해 봅시다:
class D(C):
pass
D.foo(1)
d = D()
d.foo(1)
이것은 두 번 모두 “classmethod __main__.D
1”을 출력합니다. 즉, foo()
의 첫 번째 인수로 전달된 클래스는 호출에 관련된 클래스이며, foo()
의 정의에 관련된 클래스가 아닙니다.
하지만 이것을 주목하십시오:
class E(C):
def foo(cls, y): # C.foo 재정의
print("E.foo() called")
C.foo(y)
foo = classmethod(foo)
E.foo(1)
e = E()
e.foo(1)
이 예시에서 E.foo()
에서 C.foo()
를 호출하면 첫 번째 인수로 클래스 C
를 보게 될 것이고, 클래스 E
를 보지 않을 것입니다. 이것은 호출이 클래스 C
를 지정하므로 예상되는 결과입니다. 그러나 이것은 이러한 클래스 메서드와 메타클래스에 정의된 메서드 간의 차이를 강조합니다. 메타메서드에 대한 업콜(upcall)은 대상 클래스를 명시적인 첫 번째 인수로 전달할 것입니다. (이것을 이해하지 못해도 걱정하지 마십시오. 혼자가 아닙니다.) cls.foo(y)
를 호출하는 것은 실수일 수 있습니다. 무한 재귀(infinite recursion)를 일으킬 것입니다. 또한 클래스 메서드에 명시적인 cls
인수를 지정할 수 없다는 점도 유의하십시오. (예: PEP 253의 __new__
메서드에 필요한 경우) 대신 클래스를 명시적인 첫 번째 인수로 사용하는 스태틱 메서드를 사용하십시오.
C API
XXX 다음은 다른 독자를 염두에 두고 작성한 매우 거친 텍스트입니다. 더 편집해야 합니다. XXX 또한 C API에 대해 충분히 자세히 다루지 않습니다.
내장 타입은 두 가지 방식으로 특수 데이터 속성을 선언할 수 있습니다: struct memberlist
(structmember.h에 정의됨) 또는 struct getsetlist
(descrobject.h에 정의됨)를 사용합니다. struct memberlist
는 새로운 용도로 사용되는 오래된 메커니즘입니다. 각 속성은 이름, 타입(다양한 C 타입과 PyObject *
지원), 인스턴스의 시작으로부터의 오프셋(offset), 읽기 전용 플래그(read-only flag)를 포함하는 디스크립터 레코드(descriptor record)를 가집니다.
struct getsetlist
메커니즘은 새롭고, 추가적인 검사가 필요하거나 단순히 계산된 속성(calculated attributes)인 경우와 같이 해당 틀에 맞지 않는 경우를 위해 고안되었습니다. 여기에서 각 속성은 이름, getter C 함수 포인터, setter C 함수 포인터, 그리고 컨텍스트 포인터(context pointer)를 가집니다. 함수 포인터는 선택 사항이므로, 예를 들어 setter 함수 포인터를 NULL
로 설정하면 읽기 전용 속성이 됩니다. 컨텍스트 포인터는 일반 getter/setter 함수에 보조 정보(auxiliary information)를 전달하기 위한 것이지만, 아직 이에 대한 필요성을 찾지 못했습니다.
내장 메서드를 선언하는 유사한 메커니즘도 있습니다: 이들은 PyMethodDef
구조체이며, 이름과 C 함수 포인터(및 호출 규칙에 대한 일부 플래그)를 포함합니다.
전통적으로 내장 타입은 이러한 속성 정의가 작동하도록 자체 tp_getattro
및 tp_setattro
슬롯 함수를 정의해야 했습니다 (PyMethodDef
와 struct memberlist
는 꽤 오래되었습니다). PyMethodDef
또는 memberlist
구조체 배열, 객체, 속성 이름을 받아 목록에서 찾으면 속성을 반환하거나 설정하고, 찾지 못하면 예외를 발생시키는 편의 함수가 있습니다. 그러나 이러한 편의 함수는 특정 타입의 tp_getattro
또는 tp_setattro
메서드에 의해 명시적으로 호출되어야 했고, 요청된 속성을 설명하는 배열 요소를 찾기 위해 strcmp()
를 사용하여 배열을 선형 검색했습니다.
이제 저는 이 상황을 상당히 개선하는 아주 새로운 일반 메커니즘을 가지고 있습니다.
PyMethodDef
, memberlist
, getsetlist
구조체 배열에 대한 포인터는 새로운 타입 객체(tp_methods
, tp_members
, tp_getset
)의 일부입니다. 타입 초기화 시점(PyType_InitDict()
)에, 이 세 배열의 각 항목에 대해 디스크립터 객체가 생성되어 타입에 속하는 딕셔너리(tp_dict
)에 배치됩니다. 디스크립터는 주로 해당 구조체를 가리키는 매우 간결한 객체입니다. 구현 세부 사항으로는 모든 디스크립터가 동일한 객체 타입을 공유하며, 식별자 필드(discriminator field)가 어떤 종류의 디스크립터인지(메서드, 멤버 또는 getset) 알려줍니다. PEP 252에 설명된 대로, 디스크립터는 객체 인수를 받아 해당 객체의 속성을 반환하는 get()
메서드를 가집니다. 쓰기 가능한(writable) 속성에 대한 디스크립터는 객체와 값을 받아 해당 객체의 속성을 설정하는 set()
메서드도 가집니다. get()
객체는 메서드에 대한 바인딩(bind()
) 작업으로도 작동하여 언바운드 메서드 구현을 객체에 바인딩합니다. 거의 모든 내장 객체는 이제 자체 tp_getattro
및 tp_setattro
구현을 제공하는 대신, PyObject_GenericGetAttr
및 (쓰기 가능한 속성이 있는 경우) PyObject_GenericSetAttr
을 tp_getattro
및 tp_setattro
슬롯에 배치합니다. (또는 이들을 NULL
로 남겨두고, 첫 번째 인스턴스가 생성되기 전에 타입에 대한 PyType_InitDict()
에 대한 명시적 호출을 준비하는 경우 기본 기본 객체로부터 상속받을 수 있습니다.) 가장 간단한 경우, PyObject_GenericGetAttr()
은 정확히 하나의 딕셔너리 조회(lookup)를 수행합니다: 타입의 딕셔너리(obj->ob_type->tp_dict
)에서 속성 이름을 찾습니다. 성공하면 두 가지 가능성이 있습니다: 디스크립터에 get
메서드가 있거나 없습니다. 속도를 위해 get
및 set
메서드는 타입 슬롯(tp_descr_get
및 tp_descr_set
)입니다. tp_descr_get
슬롯이 NULL
이 아니면, 객체를 유일한 인수로 전달하여 호출되며, 이 호출의 반환 값이 getattr
작업의 결과가 됩니다. tp_descr_get
슬롯이 NULL
이면, 대체로 디스크립터 자체가 반환됩니다 (메서드가 아닌 단순 값인 클래스 속성과 비교). PyObject_GenericSetAttr()
은 매우 유사하게 작동하지만 tp_descr_set
슬롯을 사용하고 객체와 새 속성 값으로 호출합니다. tp_descr_set
슬롯이 NULL
이면 AttributeError
가 발생합니다. 이제 더 복잡한 경우를 살펴봅시다. 위에서 설명한 접근 방식은 리스트, 문자열, 숫자와 같은 대부분의 내장 객체에 적합합니다. 그러나 일부 객체 타입은 각 인스턴스에 임의의 속성을 저장할 수 있는 딕셔너리를 가집니다. 사실, 클래스 문을 사용하여 기존 내장 타입을 서브타입화(subtype)할 때, 이러한 딕셔너리를 자동으로 얻습니다 (다른 고급 기능인 __slots__
를 사용하여 명시적으로 끄지 않는 한). 이것을 타입 딕셔너리(type dict)와 구별하기 위해 인스턴스 딕셔너리(instance dict)라고 부르겠습니다. 더 복잡한 경우, 인스턴스 딕셔너리에 저장된 이름과 타입 딕셔너리에 저장된 이름 사이에 충돌이 발생합니다. 두 딕셔너리가 동일한 키를 가진 항목을 가지고 있다면, 어떤 것을 반환해야 할까요? 고전적인 Python에서 지침을 찾아보면 상충되는 규칙을 발견합니다: 클래스 인스턴스의 경우, 인스턴스 딕셔너리가 클래스 딕셔너리를 재정의하지만, 인스턴스 딕셔너리보다 우선순위를 가지는 특수 속성(__dict__
및 __class__
등)은 예외입니다. 저는 PyObject_GenericGetAttr()
에 구현된 다음 규칙 집합으로 이 문제를 해결했습니다:
- 타입 딕셔너리에서 찾습니다. 데이터 디스크립터를 찾으면
get()
메서드를 사용하여 결과를 생성합니다. 이것은__dict__
및__class__
와 같은 특수 속성을 처리합니다. - 인스턴스 딕셔너리에서 찾습니다. 아무것도 찾으면 그것이 결과입니다. (이것은 일반적으로 인스턴스 딕셔너리가 클래스 딕셔너리를 재정의해야 한다는 요구 사항을 처리합니다.)
- 타입 딕셔너리에서 다시 찾습니다 (실제로는 물론 1단계에서 저장된 결과를 사용합니다). 디스크립터를 찾으면
get()
메서드를 사용하고, 다른 것을 찾으면 그것이 결과이며, 거기에 없으면AttributeError
를 발생시킵니다.
이것은 디스크립터를 데이터 디스크립터(data descriptor)와 비-데이터 디스크립터(nondata descriptor)로 분류하는 것을 요구합니다. 현재 구현은 멤버(member) 및 getset 디스크립터를 데이터로 (읽기 전용이더라도!), 메서드 디스크립터를 비-데이터로 합리적으로 분류합니다. 비-디스크립터(함수 포인터 또는 일반 값과 같은)도 비-데이터로 분류됩니다 (!).
이 체계에는 한 가지 단점이 있습니다: 제가 가장 일반적인 경우라고 가정하는, 인스턴스 딕셔너리에 저장된 인스턴스 변수를 참조하는 경우, 두 번의 딕셔너리 조회를 수행합니다. 반면 고전적인 체계는 두 개의 밑줄로 시작하는 속성에 대한 빠른 테스트와 단일 딕셔너리 조회를 수행했습니다. (구현은 슬프게도 instance_getattr()
가 instance_getattr1()
을 호출하고 instance_getattr1()
이 instance_getattr2()
를 호출하며, instance_getattr2()
가 최종적으로 PyDict_GetItem()
을 호출하는 방식으로 구성되어 있고, 밑줄 테스트는 PyString_AsString()
을 호출하는 대신 인라인(inlining)되지 않습니다. 이 부분을 최적화하는 것이 Python 2.2의 속도를 높이는 좋은 아이디어가 아닐까 궁금합니다. 모두 제거할 계획이 아니었다면 말이죠. :-) 벤치마크 결과 실제로 이것이 고전적인 인스턴스 변수 조회만큼 빠르다는 것을 확인했으므로 더 이상 걱정하지 않습니다. 동적 타입(dynamic types)에 대한 수정: 1단계와 3단계는 타입과 모든 기본 클래스의 딕셔너리에서 찾습니다 (물론 MRO 순서로).
논의 (Discussion)
XXX
예시 (Examples)
리스트를 살펴봅시다. 고전적인 Python에서는 리스트의 메서드 이름이 리스트 객체의 __methods__
속성으로 제공되었습니다:
>>> [].__methods__
['append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>>
새로운 제안에 따라, __methods__
속성은 더 이상 존재하지 않습니다:
>>> [].__methods__
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'list' object has no attribute '__methods__'
>>>
대신, 리스트 타입에서 동일한 정보를 얻을 수 있습니다:
>>> T = [].__class__
>>> T
<class 'list'>
>>> dir(T) # T.__dict__.keys()와 같지만 정렬됨
['__add__', '__class__', '__contains__', '__eq__', '__ge__', '__getattr__', '__getitem__', '__getslice__', '__gt__', '__iadd__', '__imul__', '__init__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__radd__', '__repr__', '__rmul__', '__setitem__', '__setslice__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>>
새로운 인트로스펙션 API는 이전 API보다 더 많은 정보를 제공합니다. 일반 메서드 외에도, 일반적으로 특수 표기법(예: __iadd__
( +=
), __len__
( len
), __ne__
( !=
))을 통해 호출되는 메서드도 보여줍니다. 이 목록의 모든 메서드를 직접 호출할 수 있습니다:
>>> a = ['tic', 'tac']
>>> T.__len__(a) # len(a)와 동일
2
>>> T.append(a, 'toe') # a.append('toe')와 동일
>>> a
['tic', 'tac', 'toe']
>>>
이것은 사용자 정의 클래스와 동일합니다.
목록에서 친숙하지만 놀라운 이름인 __init__
을 주목하십시오. 이것은 PEP 253의 영역입니다.
하위 호환성 (Backwards compatibility)
XXX
경고 및 오류 (Warnings and Errors)
XXX
구현 (Implementation)
이 PEP의 부분적인 구현은 CVS에서 “descr-branch”라는 이름의 브랜치로 사용할 수 있습니다. 이 구현을 실험하려면 http://sourceforge.net/cvs/?group_id=5470의 지침에 따라 CVS에서 Python을 체크아웃(check out)하되, cvs checkout
명령에 인수 “-r descr-branch”를 추가하십시오. (기존 체크아웃에서 시작하여 “cvs update -r descr-branch”를 수행할 수도 있습니다.) 여기에 설명된 기능의 몇 가지 예시는 파일 Lib/test/test_descr.py
를 참조하십시오.
참고: 이 브랜치의 코드는 이 PEP의 범위를 훨씬 넘어섭니다. 또한 PEP 253 (내장 타입 서브타이핑, Subtyping Built-in Types)에 대한 실험 영역이기도 합니다.
참조 (References)
XXX
저작권 (Copyright)
이 문서는 퍼블릭 도메인(public domain)에 공개되었습니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments