PEP 252 – 유형을 클래스처럼 보이게 만들기
- Author:
- Guido van Rossum <guido at python.org>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 19-Apr-2001
- Python-Version:
- 2.2
- Post-History:
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 유형에 대한 인트로스펙션 API를 변경하여 유형을 클래스처럼 보이게 하고, 그 인스턴스를 클래스 인스턴스처럼 보이게 할 것을 제안합니다. 예를 들어, 대부분의 내장 유형에서 type(x)는 x.__class__와 동등해집니다. C가 x.__class__일 때, x.meth(a)는 일반적으로 C.meth(x, a)와 동등하며, C.__dict__에는 x의 메서드와 기타 속성이 포함됩니다.
이 PEP는 또한 속성 디스크립터를 사용하여 속성을 지정하는 새로운 접근 방식, 또는 줄여서 디스크립터를 도입합니다. 디스크립터는 속성을 설명하는 데 사용되는 여러 가지 서로 다른 일반적 메커니즘을 통합하고 일반화합니다. 디스크립터는 메서드, 객체 구조의 타입 지정 필드 또는 getter 및 setter 함수로 표현되는 일반화된 속성을 설명할 수 있습니다.
이 PEP는 일반화된 디스크립터 API를 기반으로 클래스 메서드와 정적 메서드를 선언하는 방법도 도입합니다.
[편집자 주: 이 PEP에서 설명한 아이디어는 Python에 통합되었습니다. 이 PEP는 더 이상 구현을 정확하게 설명하지 않습니다.]
서론
Python의 가장 오래된 언어적 결함 중 하나는 클래스와 유형의 차이입니다. 예를 들어, 딕셔너리 유형을 직접 서브클래스화할 수 없으며, 객체가 어떤 메서드와 인스턴스 변수를 갖는지 알아내는 인트로스펙션 인터페이스가 유형과 클래스에서 서로 다릅니다.
클래스와 유형의 분리를 바로잡는 일은 Python이 구현되는 방식의 여러 측면에 영향을 미치므로 큰 작업입니다. 이 PEP는 유형에 대한 인트로스펙션 API를 클래스에 대한 API와 동일하게 보이도록 만드는 일을 다룹니다. 다른 PEP에서는 클래스를 유형처럼 보이게 하고 내장 유형을 서브클래스화할 수 있도록 하는 방안을 제안할 예정이며, 이러한 주제는 이 PEP에서 다루지 않습니다.
인트로스펙션 API
인트로스펙션은 객체가 어떤 속성을 갖는지 알아내는 일을 다룹니다. Python의 매우 일반적인 getattr/setattr API로 인해 특정 객체가 지원하는 모든 속성의 목록을 얻을 방법이 항상 존재한다고 보장할 수는 없지만, 실제로는 거의 모든 객체에서 함께 작동하는 두 가지 관례가 등장했습니다. 이를 클래스 기반 인트로스펙션 API와 유형 기반 인트로스펙션 API라고 부르겠으며, 줄여서 클래스 API와 유형 API라고 하겠습니다.
클래스 기반 인트로스펙션 API는 주로 클래스 인스턴스에 사용되며, Jim Fulton의 ExtensionClasses에서도 사용됩니다. 이 API는 객체 x의 모든 데이터 속성이 x.__dict__딕셔너리에 저장되고, 모든 메서드와 클래스 변수를 x.__class__로 표기되는 x의 클래스에 대한 검사를 통해 찾을 수 있다고 가정합니다. 클래스에는 클래스 자체에서 정의한 메서드와 클래스 변수를 포함하는 딕셔너리를 반환하는 __dict__속성과, 재귀적으로 검사해야 하는 베이스 클래스의 튜플인 __bases__속성이 있습니다. 여기에는 다음과 같은 몇 가지 가정이 있습니다.
- 인스턴스 딕셔너리에 정의된 속성이 객체의 클래스에 정의된 속성을 재정의합니다.
- 파생 클래스에 정의된 속성이 베이스 클래스에 정의된 속성을 재정의합니다.
- 더 앞선 베이스 클래스(즉,
__bases__에서 더 앞에 나오는 베이스 클래스)의 속성이 더 뒤에 나오는 베이스 클래스의 속성을 재정의합니다.
(마지막 두 규칙은 함께 속성 검색의 왼쪽에서 오른쪽, 깊이 우선 규칙으로 요약되는 경우가 많습니다. 이것이 전통적인 Python 속성 조회 규칙입니다. 참고로 PEP 253은 속성 검색 순서를 변경할 것을 제안할 예정이며, 수락되면 이 PEP도 그에 따릅니다.
타입 기반 인트로스펙션 API는 대부분의 내장 객체에서 어떤 형태로든 지원합니다. 이 API는 두 가지 특수 속성인 __members__와 __methods__를 사용합니다. __methods__속성이 존재하는 경우, 객체가 지원하는 메서드 이름의 목록입니다. __members__속성이 존재하는 경우, 객체가 지원하는 데이터 속성 이름의 목록입니다.
타입 API는 때때로 인스턴스의 경우와 동일하게 작동하는 __dict__와 결합됩니다(예를 들어 Python 2.1의 함수 객체에서는 f.__dict__에 f의 동적 속성이 포함되고, f.__members__에는 f에 정적으로 정의된 속성의 이름이 나열됩니다).
어느 정도 주의해야 합니다. 어떤 객체는 __dict__와 __doc__같은 자신의 “고유” 속성을 __members__에 나열하지 않지만, 다른 객체는 나열합니다. 때로는 속성 이름이 __members__나 __methods__에도 나타나고 __dict__의 키로도 나타나는데, 이 경우 __dict__에서 찾은 값이 사용되는지는 아무도 확실히 알 수 없습니다.
타입 API는 지금까지 신중하게 명세화된 적이 없습니다. 이는 Python의 민간 전승과도 같은 부분이며, 대부분의 서드파티 확장은 이를 지원하는 예제를 따르기 때문에 이를 지원합니다. 또한 Py_FindMethod()및/또는 PyMember_Get()를 tp_getattr 핸들러에서 사용하는 모든 타입도 이를 지원합니다. 이 두 함수가 각각 __methods__와 __members__라는 속성 이름을 특별히 처리하기 때문입니다.
Jim Fulton의 ExtensionClasses는 타입 API를 무시하고, 대신 더 강력한 클래스 API를 모방합니다. 이 PEP에서는 모든 타입에 대해 클래스 API를 지원하는 방향으로 타입 API를 단계적으로 폐지할 것을 제안합니다.
클래스 API를 지지하는 한 가지 주장은 타입이 지원하는 속성을 알아보기 위해 인스턴스를 생성할 필요가 없다는 점이며, 이는 문서 처리기에 유용합니다. 예를 들어 socket 모듈은 SocketType 객체를 내보내지만, 현재로서는 이 객체가 소켓 객체에 어떤 메서드가 정의되어 있는지 알려 주지 않습니다. 클래스 API를 사용하면 SocketType은 소켓 객체의 메서드가 정확히 무엇인지 보여 주며, 소켓을 생성하지 않고도 해당 메서드의 독스트링을 추출할 수 있습니다. (이는 C 확장 모듈이므로 이 경우 독스트링을 추출하기 위한 소스 검색 방식은 실현 가능하지 않습니다.)
클래스 기반 인트로스펙션 API의 명세
객체에는 정적 속성과 동적 속성이라는 두 종류의 속성이 있을 수 있습니다. 정적 속성의 이름과 때로는 기타 속성은 obj.__class__또는 type(obj)를 통해 접근할 수 있는 객체의 타입이나 클래스를 조사하여 알 수 있습니다. (여기서는 type과 class를 같은 의미로 사용하며, 둘 모두에 해당하는 다소 서투르지만 설명적인 용어는 “메타 객체”입니다.)
(XXX 여기서 static과 dynamic은 사용하기에 좋은 용어가 아닙니다. “static” 속성도 실제로는 매우 동적으로 동작할 수 있으며, C++이나 Java의 정적 클래스 멤버와는 아무런 관련이 없기 때문입니다. Barry는 대신 immutable과 mutable을 사용할 것을 제안하지만, 이 단어들은 이미 서로 조금씩 다른 맥락에서 정확하고 서로 다른 의미를 가지므로, 그렇게 해도 여전히 혼란스러울 것이라고 생각합니다.)
동적 속성의 예로는 클래스 인스턴스의 인스턴스 변수와 모듈 속성 등이 있습니다. 정적 속성의 예로는 리스트와 딕셔너리 같은 내장 객체의 메서드와 프레임 및 코드 객체의 속성(f.f_code, c.co_filename 등)이 있습니다. 동적 속성을 가진 객체가 이를 __dict__속성을 통해 노출하면 __dict__는 정적 속성입니다.
동적 프로퍼티의 이름과 값은 일반적으로 딕셔너리에 저장되며, 이 딕셔너리는 일반적으로 obj.__dict__로 접근할 수 있습니다. 이 명세의 나머지 부분은 동적 속성보다 정적 속성의 이름과 프로퍼티를 알아내는 데 더 중점을 둡니다. 동적 속성은 obj.__dict__를 조사하면 쉽게 알아낼 수 있습니다.
아래의 논의에서는 객체를 일반 객체(리스트, 정수, 함수 등)와 메타 객체라는 두 종류로 구분합니다. 타입과 클래스는 메타 객체입니다. 메타 객체도 일반 객체이지만, 일반 객체의 __class__속성(또는 다른 메타 객체의 __bases__속성)이 메타 객체를 참조하기 때문에 여기서는 주로 메타 객체에 관심을 둡니다.
클래스 인트로스펙션 API는 다음 요소로 구성됩니다.
- 일반 객체의
__class__및__dict__속성; - 메타 객체의
__bases__및__dict__속성; - 우선순위 규칙;
- 속성 디스크립터.
이 요소들은 메타 객체가 정의한 모든 속성에 관한 정보를 제공할 뿐 아니라, 주어진 객체의 특정 속성 값을 계산하는 데도 도움을 줍니다.
- 일반 객체의
__dict__속성일반 객체에는
__dict__속성이 있을 수 있습니다. 이 속성이 있으면, 이는 최소한__getitem__(),keys(),has_key()를 지원하는 매핑이어야 합니다(반드시 딕셔너리일 필요는 없습니다). 이는 객체의 동적 속성을 제공합니다. 매핑의 키는 속성 이름을 제공하고, 이에 대응하는 값은 해당 속성의 값을 제공합니다.일반적으로 주어진 이름을 가진 속성의 값은
__dict__에서 해당 이름을 키로 하는 항목의 값과 동일한 객체입니다. 즉,obj.__dict__['spam']은obj.spam입니다. (단, 아래의 우선순위 규칙을 참고하십시오. 같은 이름의 정적 속성이 딕셔너리 항목을 재정의할 수도 있습니다.) - 일반 객체의
__class__속성일반 객체에는 보통
__class__속성이 있습니다. 이 속성이 있으면 메타 객체를 참조합니다. 메타 객체는 해당 일반 객체의__class__인 일반 객체에 대한 정적 속성을 정의할 수 있습니다. 이는 보통 다음 메커니즘을 통해 수행합니다. - 메타 객체의
__dict__속성메타 객체에는 일반 객체의
__dict__속성과 같은 형식의__dict__속성이 있을 수 있습니다(매핑이지만 반드시 딕셔너리일 필요는 없습니다). 이 속성이 있으면 메타 객체의__dict__의 키는 해당 일반 객체에 대한 정적 속성의 이름입니다. 값은 속성 디스크립터이며, 이에 대해서는 나중에 설명합니다. 바인딩되지 않은 메서드는 속성 디스크립터의 특수한 경우입니다.메타 객체도 일반 객체이므로 메타 객체의
__dict__에 있는 항목은 메타 객체의 속성에 대응합니다. 그러나 일부 변환이 적용될 수 있으며, 베이스(아래에서 설명합니다)가 추가적인 동적 속성을 정의할 수 있습니다. 즉,mobj.spam이 항상mobj.__dict__['spam']인 것은 아닙니다. (이 규칙에는 허점이 있습니다. 클래스의 경우C.__dict__['spam']이 함수라면C.spam은 바인딩되지 않은 메서드 객체입니다.) - 메타 객체의
__bases__속성메타 객체에는
__bases__속성이 있을 수 있습니다. 이 속성이 있으면 이는 다른 메타 객체인 베이스들의 시퀀스여야 합니다(반드시 튜플일 필요는 없습니다).__bases__가 없는 것은 빈 베이스 시퀀스와 동일합니다.__bases__속성으로 정의되는 메타 객체 간의 관계에는 순환이 절대로 있어서는 안 됩니다. 즉,__bases__속성은 파생 메타 객체에서 해당 베이스 메타 객체로 호가 향하는 방향성 비순환 그래프를 정의합니다. (여러 클래스가 동일한 베이스 클래스를 가질 수 있으므로 반드시 트리일 필요는 없습니다.) 상속 그래프에 있는 메타 객체의__dict__속성은__class__속성이 상속 트리의 루트를 가리키는 일반 객체에 대한 속성 디스크립터를 제공합니다(이는 상속 계층 구조의 루트와는 같지 않으며, 상속 트리를 일반적으로 그리는 방식에 따르면 오히려 반대쪽인 아래쪽에 있습니다). 디스크립터는 먼저 루트 메타 객체의 딕셔너리에서 검색한 다음, 우선순위 규칙에 따라 해당 메타 객체의 베이스에서 검색합니다(다음 단락을 참고하십시오). - 우선순위 규칙
주어진 일반 객체의 상속 그래프에 있는 두 메타 객체가 같은 이름의 속성 디스크립터를 모두 정의하는 경우, 검색 순서는 메타 객체에 따라 달라집니다. 이를 통해 서로 다른 메타 객체가 서로 다른 검색 순서를 정의할 수 있습니다. 특히 클래식 클래스는 기존의 왼쪽에서 오른쪽 깊이 우선 규칙을 사용하는 반면, 새 스타일 클래스는 더 발전된 규칙을 사용합니다(PEP 253의 메서드 결정 순서 절을 참고하십시오).
동적 속성(일반 객체의
__dict__에 정의된 속성)이 정적 속성(일반 객체의__class__를 루트로 하는 상속 그래프의 메타 객체에 의해 정의된 속성)과 같은 이름을 가질 때, 정적 속성이__set__메서드를 정의하는 디스크립터이면 정적 속성이 우선합니다(아래를 참고하십시오). 그렇지 않으면(__set__메서드가 없으면) 동적 속성이 우선합니다. 즉, 데이터 속성(__set__메서드를 가진 속성)의 경우 정적 정의가 동적 정의를 재정의하지만, 그 밖의 속성에서는 동적 정의가 정적 정의를 재정의합니다.근거: 일부 정적 속성이 실제로 동적 속성을 재정의하므로, “정적 속성이 동적 속성을 재정의한다” 또는 “동적 속성이 정적 속성을 재정의한다”와 같은 단순한 규칙을 사용할 수 없습니다. 예를 들어 인스턴스의
__dict__에 있는 키 ‘__class__’는 정적으로 정의된__class__포인터를 우선하여 무시되지만, 반대로inst.__dict__의 대부분의 키는inst.__class__에 정의된 속성을 재정의합니다. 디스크립터에__set__메서드가 존재한다는 것은 이것이 데이터 디스크립터임을 나타냅니다. (읽기 전용 데이터 디스크립터에도__set__메서드가 있으며, 이 메서드는 항상 예외를 발생시킵니다.) 디스크립터에__set__메서드가 없다는 것은 해당 디스크립터가 할당을 가로채는 데 관심이 없음을 나타내며, 이 경우 고전적인 규칙이 적용됩니다. 메서드와 같은 이름을 가진 인스턴스 변수가 해당 메서드가 삭제될 때까지 메서드를 숨깁니다. - 속성 디스크립터
이 부분부터 흥미롭고 복잡해집니다. 속성 디스크립터(줄여서 디스크립터)는 메타 객체의
__dict__에 저장되거나 그 조상 중 하나의__dict__에 저장되며, 두 가지 용도로 사용됩니다. 디스크립터는 (일반적인 비메타) 객체에서 해당 속성값을 가져오거나 설정하는 데 사용할 수 있고, 문서화 및 인트로스펙션을 목적으로 속성을 설명하는 추가 인터페이스도 제공합니다.값을 가져오거나 설정하는 경우와 그 밖의 방식으로 속성을 설명하는 경우 모두 디스크립터의 인터페이스를 설계하기 위한 Python의 선행 사례는 거의 없으며, 몇 가지 사소한 프로퍼티를 제외하면 그렇습니다(
__name__과__doc__가 속성의 이름과 문서 문자열이어야 한다고 보는 것은 합리적입니다). 아래에서 이러한 API를 제안하겠습니다.메타 객체의
__dict__에서 발견된 객체가 속성 디스크립터가 아니라면, 하위 호환성에 따라 일정한 최소 의미론이 적용됩니다. 이는 기본적으로 그것이 Python 함수이거나 바인딩되지 않은 메서드이면 해당 속성이 메서드이고, 그렇지 않으면 동적 데이터 속성의 기본값이라는 의미입니다. 하위 호환성에 따라 (__setattr__메서드가 없는 경우) 메서드에 해당하는 속성에 할당하는 것이 허용되며, 이렇게 하면 해당 특정 인스턴스에서 메서드를 가리는 데이터 속성이 생성됩니다. 그러나 이러한 의미론은 일반 클래스와의 하위 호환성을 위해서만 필요합니다.
인트로스펙션 API는 읽기 전용 API입니다. 특수 속성(__dict__, __class__및 __bases__)에 할당할 때의 효과나 __dict__의 항목에 할당할 때의 효과는 정의하지 않습니다. 일반적으로 이러한 할당은 금지된 작업으로 간주해야 합니다. 향후 PEP에서 이러한 할당 중 일부에 대한 의미론을 정의할 수 있습니다. (특히 현재 인스턴스는 __class__와 __dict__에 할당할 수 있고, 클래스는 __bases__와 __dict__에 할당할 수 있기 때문입니다.)
속성 디스크립터 API 사양
속성 디스크립터는 다음 속성을 가질 수 있습니다. 예제에서 x는 객체이고, C는 x.__class__이며, x.meth()는 메서드이고, x.ivar는 데이터 속성 또는 인스턴스 변수입니다. 모든 속성은 선택 사항입니다. 특정 디스크립터에 특정 속성이 존재할 수도 있고 존재하지 않을 수도 있습니다. 속성이 없다는 것은 해당 정보가 제공되지 않거나 해당 기능이 구현되지 않았음을 의미합니다.
__name__: 속성 이름입니다. 별칭 지정과 이름 변경으로 인해 해당 속성은 다른 이름으로 추가로 또는 독점적으로 알려질 수 있지만, 이것은 해당 속성이 처음 만들어졌을 때의 이름입니다. 예:C.meth.__name__ == 'meth'입니다.__doc__: 속성의 문서 문자열입니다. 이는 None일 수 있습니다.__objclass__: 이 속성을 선언한 클래스입니다. 디스크립터는 이 클래스의 인스턴스인 객체에만 적용됩니다(하위 클래스의 인스턴스도 포함합니다). 예:C.meth.__objclass__ is C입니다.- 하나 또는 두 개의 인자로 호출할 수 있는 호출 가능 객체인 함수
__get__()는 객체에서 속성 값을 가져옵니다. 이는 “바인딩” 작업이라고도 하는데, 메서드 디스크립터의 경우 “바운드 메서드” 객체를 반환할 수 있기 때문입니다. 첫 번째 인자인 X는 어트리뷰트를 가져오거나 어트리뷰트에 바인딩해야 하는 객체입니다. X가 None이면 선택적 두 번째 인자인 T는 메타 객체여야 하며, 바인딩 작업은 T의 인스턴스로 제한된 unbound 메서드를 반환할 수 있습니다. X와 T가 모두 지정된 경우 X는 T의 인스턴스여야 합니다. 바인딩 작업이 정확히 무엇을 반환하는지는 디스크립터의 의미론에 따라 달라집니다. 예를 들어 정적 메서드와 클래스 메서드는(아래 참조) 인스턴스를 무시하고 대신 타입에 바인딩합니다. __set__(): 객체에 어트리뷰트 값을 설정하는 두 인자의 함수입니다. 어트리뷰트가 읽기 전용이면 이 메서드는 TypeError 또는AttributeError예외를 발생시킬 수 있습니다(정의되지 않았거나 설정할 수 없는 어트리뷰트에 대해 역사적으로 두 예외가 모두 발견되었으므로 둘 다 허용됩니다). 예:C.ivar.set(x, y) ~~ x.ivar = y.
정적 메서드와 클래스 메서드
디스크립터 API를 사용하면 정적 메서드와 클래스 메서드를 추가할 수 있습니다. 정적 메서드는 설명하기 쉽습니다. C++나 Java의 정적 메서드와 거의 동일하게 동작합니다. 다음은 예입니다.:
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”가 선언되지 않으며, 호출할 때 인스턴스도 필요하지 않습니다.
클래스 문에 있는 “foo = staticmethod(foo)” 행이 핵심 요소입니다. 이 행이 foo()를 정적 메서드로 만듭니다. 내장 staticmethod()는 함수 인자를 특수한 종류의 디스크립터로 감싸며, 이 디스크립터의 __get__() 메서드는 원래 함수를 변경 없이 반환합니다. 이것이 없었다면 표준 함수 객체의 __get__() 메서드는 ‘c.foo’에 대해 바운드 메서드 객체를, ‘C.foo’에 대해 언바운드 메서드 객체를 생성했을 것입니다.
(XXX Barry는 “staticmethod” 대신 “sharedmethod”를 사용할 것을 제안합니다. static이라는 단어가 이미 매우 다양한 방식으로 중복 사용되고 있기 때문입니다. 하지만 shared가 올바른 의미를 전달하는지는 확신하지 못하겠습니다.)
클래스 메서드는 호출되는 클래스를 암시적인 첫 번째 인자로 받는 메서드를 선언할 때 이와 유사한 패턴을 사용합니다. 이는 C++나 Java에 대응하는 기능이 없고 Smalltalk의 클래스 메서드와도 정확히 같지는 않지만, 비슷한 목적에 사용할 수 있습니다. Armin Rigo에 따르면 이는 Borland Pascal 방언인 Delphi의 “virtual class methods”와 유사합니다. (Python에는 실제 메타클래스도 있으며, 메타클래스에 정의된 메서드가 “class method”라는 이름을 사용할 더 타당한 권리가 있을 수도 있습니다. 하지만 대부분의 프로그래머는 메타클래스를 사용하지 않을 것으로 예상합니다.) 다음은 예입니다.:
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) 호출은 모두 결국 two 인자로 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): # override C.foo
print "E.foo() called"
C.foo(y)
foo = classmethod(foo)
E.foo(1)
e = E()
e.foo(1)
이 예제에서 E.foo()에서 C.foo()를 호출하면 첫 번째 인자로 클래스 E가 아니라 클래스 C를 보게 됩니다. 호출에서 클래스 C를 지정하므로 이는 예상되는 동작입니다. 그러나 이는 이러한 클래스 메서드와 메타클래스에 정의된 메서드 사이의 차이를 부각합니다. 메타메서드로 상향 호출하면 대상 클래스를 명시적인 첫 번째 인자로 전달하기 때문입니다. (이해하지 못하더라도 걱정하지 마십시오. 혼자만 그런 것은 아닙니다.) cls.foo(y)를 호출하는 것은 실수라는 점에 유의하십시오. 무한 재귀가 발생하기 때문입니다. 또한 클래스 메서드에 명시적인 ‘cls’ 인자를 지정할 수 없다는 점에 유의하십시오. 이를 원한다면(예를 들어 PEP 253에서 __new__메서드가 이를 요구합니다), 클래스가 명시적인 첫 번째 인자인 정적 메서드를 대신 사용하십시오.
C API
XXX 다음 내용은 다른 독자를 염두에 두고 작성한 매우 초보적인 글이므로, 다시 검토하여 더 다듬어야 합니다. XXX 또한 C API에 관해 충분히 자세히 다루지 않습니다.
내장 타입은 두 가지 방법으로 특수 데이터 속성을 선언할 수 있습니다. 하나는 struct memberlist(structmember.h에 정의됨)를 사용하는 방법이고, 다른 하나는 struct getsetlist(descrobject.h에 정의됨)를 사용하는 방법입니다. struct memberlist는 새로운 용도로 사용되는 오래된 메커니즘입니다. 각 속성에는 이름, 해당 타입을 나타내는 열거형(다양한 C 타입과 PyObject *를 지원합니다), 인스턴스 시작 부분으로부터의 오프셋, 읽기 전용 플래그가 포함된 디스크립터 레코드가 있습니다.
struct getsetlist 메커니즘은 새로운 것이며, 추가 검사가 필요하거나 단순히 계산된 속성인 경우처럼 기존 방식에 맞지 않는 사례를 위한 것입니다. 여기의 각 속성에는 이름, getter C 함수 포인터, setter C 함수 포인터, 컨텍스트 포인터가 있습니다. 함수 포인터는 선택 사항이므로, 예를 들어 setter 함수 포인터를 NULL로 설정하면 읽기 전용 속성이 됩니다. 컨텍스트 포인터는 일반 getter/setter 함수에 보조 정보를 전달하기 위한 것이지만, 아직 이를 필요로 하는 경우를 찾지 못했습니다.
내장 메서드를 선언하는 유사한 메커니즘도 있다는 점에 유의하십시오. 이는 이름과 C 함수 포인터(그리고 호출 규약을 위한 몇 가지 플래그)를 포함하는 PyMethodDef 구조체입니다.
전통적으로 내장 타입은 이러한 속성 정의가 작동하도록 자체 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)에 배치합니다. - 디스크립터는 대부분 해당 구조체를 가리키기만 하는 매우 간결한 객체입니다. 구현 세부 사항으로, 모든 디스크립터는 동일한 객체 타입을 공유하며 판별 필드가 해당 디스크립터의 종류(메서드, 멤버 또는 getset)를 나타냅니다.
- 관련 PEP 252에서 설명했듯이, 디스크립터에는 객체 인자를 받아 그 객체의 어트리뷰트를 반환하는
get()메서드가 있으며, 쓰기 가능한 어트리뷰트의 디스크립터에는 객체와 값을 받아 그 객체의 어트리뷰트를 설정하는set()메서드도 있습니다. 또한get()객체는 메서드에 대한bind()연산으로도 기능하여, 바인딩되지 않은 메서드 구현을 객체에 바인딩한다는 점에 유의하십시오. - 이제 거의 모든 내장 객체는 자체 tp_getattro 및 tp_setattro 구현을 제공하는 대신
PyObject_GenericGetAttr및 (쓰기 가능한 속성이 있는 경우)PyObject_GenericSetAttr를 해당tp_getattro및tp_setattro슬롯에 배치합니다. (또는 첫 번째 인스턴스가 생성되기 전에 해당 타입에 대해PyType_InitDict()를 명시적으로 호출하도록 구성한다면, 이를NULL로 남겨 두고 기본 베이스 객체에서 상속할 수도 있습니다.) - 가장 단순한 경우
PyObject_GenericGetAttr()는 정확히 한 번의 딕셔너리 조회를 수행합니다. 즉, 타입의 딕셔너리(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가 발생합니다.- 그러나 이제 더 복잡한 경우를 살펴보겠습니다. 위에서 설명한 접근 방식은 리스트, 문자열, 숫자와 같은 대부분의 내장 객체에 적합합니다. 그러나 일부 객체 유형은 각 인스턴스에 임의의 속성을 저장할 수 있는 딕셔너리를 가지고 있습니다. 실제로 기존 내장 유형을 서브타이핑하기 위해 클래스 문을 사용하면 이러한 딕셔너리를 자동으로 얻게 됩니다 (또 다른 고급 기능인
__slots__를 사용하여 명시적으로 비활성화하지 않는 한). 이를 유형 딕셔너리와 구별하기 위해 인스턴스 딕셔너리라고 부르겠습니다. - 더 복잡한 경우에는 인스턴스 딕셔너리에 저장된 이름과 유형 딕셔너리에 저장된 이름 사이에 충돌이 발생합니다. 두 딕셔너리에 동일한 키를 가진 항목이 있다면 어느 쪽을 반환해야 하겠습니까? 고전적인 Python을 지침으로 살펴보면 상충하는 규칙을 발견할 수 있습니다. 클래스 인스턴스의 경우 인스턴스 딕셔너리가 클래스 딕셔너리보다 우선하지만, 특수한 속성(예:
__dict__및__class__)은 예외이며 인스턴스 딕셔너리보다 우선합니다. - 다음 규칙 집합을
PyObject_GenericGetAttr()에 구현하여 이 문제를 해결했습니다.- 유형 딕셔너리에서 찾아봅니다. 데이터 디스크립터를 찾으면 해당
get()메서드를 사용하여 결과를 생성합니다. 이렇게 하면__dict__및__class__와 같은 특수 속성이 처리됩니다. - 인스턴스 딕셔너리에서 찾아봅니다. 무언가를 찾으면 그것이 결과입니다. (이는 일반적으로 인스턴스 딕셔너리가 클래스 딕셔너리를 재정의해야 한다는 요구 사항을 처리합니다.)
- 유형 딕셔너리에서 다시 찾아봅니다 (실제로는 물론 1단계에서 저장한 결과를 사용합니다). 디스크립터를 찾으면 해당
get()메서드를 사용하고, 다른 것을 찾으면 그것이 결과이며, 아무것도 없으면AttributeError를 발생시킵니다.
이를 위해서는 디스크립터를 데이터 디스크립터와 비데이터 디스크립터로 분류해야 합니다. 현재 구현은 멤버 디스크립터와 getset 디스크립터를 데이터 디스크립터로 분류하고 (읽기 전용인 경우에도!) 메서드 디스크립터를 비데이터 디스크립터로 분류하는데, 이는 상당히 합리적입니다. 비디스크립터(함수 포인터나 일반 값 등)도 비데이터로 분류합니다 (!).
- 유형 딕셔너리에서 찾아봅니다. 데이터 디스크립터를 찾으면 해당
- 이 방식에는 한 가지 단점이 있습니다. 가장 일반적인 경우라고 생각되는, 인스턴스 딕셔너리에 저장된 인스턴스 변수를 참조하는 경우에 이 방식은 두 번의 딕셔너리 조회를 수행하는 반면, 고전적인 방식은 두 개의 밑줄로 시작하는 속성인지 빠르게 검사한 후 딕셔너리 조회를 한 번만 수행했습니다. (구현은 안타깝게도
instance_getattr()이instance_getattr1()을 호출하고, 이것이instance_getattr2()를 호출하며, 마지막으로PyDict_GetItem()을 호출하는 구조이고, 밑줄 검사는 이를 인라인으로 넣는 대신PyString_AsString()을 호출합니다. 이 모든 것을 제거할 예정이 아니라면 Python 2.2의 속도를 높이기 위해 이것을 엄청나게 최적화하는 것이 좋은 생각일지 궁금합니다. :-) - 벤치마크 결과 실제로 이것이 고전적인 인스턴스 변수 조회만큼 빠른 것으로 확인되었으므로 더 이상 걱정하지 않습니다.
- 동적 유형을 위한 수정 사항: 1단계와 3단계에서는 유형과 모든 기반 클래스의 딕셔너리를 살펴봅니다 (물론 MRO 순서로 살펴봅니다).
논의
XXX
예제
리스트를 살펴보겠습니다. 고전적인 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
<type 'list'>
>>> dir(T) # like T.__dict__.keys(), but sorted
['__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) # same as len(a)
2
>>> T.append(a, 'toe') # same as a.append('toe')
>>> a
['tic', 'tac', 'toe']
>>>
이는 사용자 정의 클래스의 경우와 마찬가지입니다.
목록에서 익숙하면서도 놀라운 이름을 하나 발견할 수 있습니다: __init__입니다. 이는 PEP 253의 영역입니다.
하위 호환성
XXX
경고와 오류
XXX
구현
이 PEP의 부분적인 구현은 “descr-branch”라는 이름의 브랜치로 CVS에서 제공됩니다. 이 구현을 실험해보려면, http://sourceforge.net/cvs/?group_id=5470 에 있는 안내에 따라 CVS에서 Python을 체크아웃하되, cvs checkout 명령에 “-r descr-branch” 인자를 추가하십시오. (기존에 체크아웃한 것에서 시작하여 “cvs update -r descr-branch”를 실행할 수도 있습니다.) 여기서 설명한 기능들의 예시는 Lib/test/test_descr.py 파일을 참고하십시오.
참고: 이 브랜치의 코드는 이 PEP의 범위를 훨씬 뛰어넘습니다. 이 브랜치는 PEP 253 (내장 타입의 서브타이핑)의 실험 영역이기도 합니다.
참고 자료
XXX
Copyright
This document has been placed in the public domain.