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

Python 개선 제안 한국어 번역

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는 한 번도 신중하게 명세된 적이 없습니다. 이 API는 Python 전승의 일부이며, 대부분의 서드파티 확장은 이를 지원하는 예제를 따르기 때문에 이를 지원합니다. 또한 tp_getattr 핸들러에서 Py_FindMethod()나 PyMember_Get(), 또는 둘 다를 사용하는 타입은 모두 이 API를 지원하는데, 이 두 함수가 각각 __methods__와 __members__라는 속성 이름을 특별히 처리하기 때문입니다.

Jim Fulton의 ExtensionClasses는 타입 API를 무시하고, 대신 더 강력한 클래스 API를 흉내 냅니다. 이 PEP에서 저는 타입 API를 단계적으로 폐지하고, 대신 모든 타입에서 클래스 API를 지원할 것을 제안합니다.

클래스 API를 지지하는 인자 하나는, 타입이 어떤 속성을 지원하는지 알아내기 위해 인스턴스를 생성할 필요가 없다는 점입니다. 이는 다시 문서화 처리기에 유용합니다. 예를 들어 socket 모듈은 SocketType 객체를 내보내지만, 현재 이 객체는 소켓 객체에 어떤 메서드가 정의되어 있는지 알려 주지 않습니다. 클래스 API를 사용하면, 소켓을 생성하지 않고도 SocketType이 소켓 객체의 메서드가 정확히 무엇인지 보여 줄 것이며, 그 독스트링까지 추출할 수 있습니다. (이것은 C 확장 모듈이므로, 이 경우에는 소스를 스캔하여 독스트링을 추출하는 방식을 사용할 수 없습니다.)

클래스 기반 인트로스펙션 API의 명세

객체는 정적 속성과 동적 속성이라는 두 종류의 속성을 가질 수 있습니다. 정적 속성의 이름과 때로는 다른 특성까지도 객체의 타입이나 클래스를 조사하여 알아낼 수 있으며, 타입이나 클래스는 obj.__class__나 type(obj)를 통해 접근할 수 있습니다. (저는 타입과 클래스를 같은 의미로 사용하고 있습니다. 둘 모두에 들어맞는, 어색하지만 설명적인 용어는 “메타 객체”입니다.)

(XXX 정적과 동적은 여기서 사용하기에 좋은 용어가 아닙니다. “정적” 속성이 실제로는 상당히 동적으로 동작할 수 있고, C++나 Java의 정적 클래스 멤버와는 아무 관계가 없기 때문입니다. Barry는 대신 불변(immutable)과 가변(mutable)을 사용하자고 제안하지만, 이 단어들은 조금씩 다른 맥락에서 이미 정확하고 서로 다른 의미를 가지고 있으므로, 그래도 여전히 혼란스러울 것이라고 생각합니다.)

동적 속성의 예로는 클래스 인스턴스의 인스턴스 변수, 모듈 속성 등이 있습니다. 정적 속성의 예로는 리스트나 딕셔너리 같은 내장 객체의 메서드, 그리고 프레임 객체와 코드 객체의 속성(f.f_code, c.co_filename 등)이 있습니다. 동적 속성을 가진 객체가 이를 __dict__ 속성을 통해 노출할 때, __dict__는 정적 속성입니다.

동적 특성의 이름과 값은 일반적으로 딕셔너리에 저장되며, 이 딕셔너리는 일반적으로 obj.__dict__로 접근할 수 있습니다. 이 명세의 나머지 부분은 동적 속성보다는 정적 속성의 이름과 특성을 알아내는 데 더 초점을 맞춥니다. 동적 속성은 obj.__dict__를 조사하여 쉽게 알아낼 수 있습니다.

아래 논의에서는 두 종류의 객체, 즉 일반 객체(리스트, 정수, 함수 등)와 메타 객체를 구분합니다. 타입과 클래스는 메타 객체입니다. 메타 객체도 일반 객체이지만, 우리가 메타 객체에 주로 관심을 두는 것은 메타 객체가 일반 객체의 __class__ 속성(또는 다른 메타 객체의 __bases__ 속성)에 의해 참조되기 때문입니다.

클래스 인트로스펙션 API는 다음 요소로 구성됩니다.

  • 일반 객체의 __class__와 __dict__ 속성;
  • 메타 객체의 __bases__와 __dict__ 속성;
  • 우선순위 규칙;
  • 속성 디스크립터.

이 요소들은 함께 메타 객체가 정의하는 모든 속성에 대해 알려 줄 뿐만 아니라, 주어진 객체의 특정 속성 값을 계산하는 데에도 도움을 줍니다.

  1. 일반 객체의 __dict__ 속성

    일반 객체에는 __dict__ 속성이 있을 수 있습니다. 이 속성이 있다면, 최소한 __getitem__(), keys(), has_key()를 지원하는 매핑(반드시 딕셔너리일 필요는 없음)이어야 합니다. 이 속성은 객체의 동적 속성을 제공합니다. 매핑의 키는 속성 이름을 나타내고, 그에 대응하는 값은 해당 속성의 값을 나타냅니다.

    일반적으로 주어진 이름을 가진 속성의 값은 __dict__에서 그 이름을 키로 하는 항목의 값과 같은 객체입니다. 다시 말해, obj.__dict__['spam']은 obj.spam입니다. (다만 아래의 우선순위 규칙을 참고하십시오. 같은 이름의 정적 속성이 딕셔너리 항목을 재정의할 수도 있습니다.)

  2. 일반 객체의 __class__ 속성

    일반 객체에는 보통 __class__ 속성이 있습니다. 이 속성이 있다면, 이는 메타 객체를 참조합니다. 메타 객체는 자신을 __class__로 가지는 일반 객체에 대해 정적 속성을 정의할 수 있습니다. 이는 보통 다음과 같은 메커니즘을 통해 이루어집니다:

  3. 메타 객체의 __dict__ 속성

    메타 객체에는 일반 객체의 __dict__ 속성과 같은 형태(매핑이지만 반드시 딕셔너리일 필요는 없음)의 __dict__ 속성이 있을 수 있습니다. 이 속성이 있다면, 메타 객체의 __dict__에 있는 키는 대응하는 일반 객체의 정적 속성 이름입니다. 값은 속성 디스크립터이며, 이에 대해서는 뒤에서 설명합니다. 언바운드 메서드는 속성 디스크립터의 특수한 경우입니다.

    메타 객체 역시 일반 객체이므로, 메타 객체의 __dict__에 있는 항목은 메타 객체의 속성에 대응합니다. 하지만 어떤 변환이 적용될 수 있으며, 베이스(아래 참조)가 추가적인 동적 속성을 정의할 수도 있습니다. 다시 말해, mobj.spam이 항상 mobj.__dict__['spam']인 것은 아닙니다. (이 규칙에는 허점이 있습니다. 클래스의 경우 C.__dict__['spam']이 함수라면 C.spam은 언바운드 메서드 객체이기 때문입니다.)

  4. 메타 객체의 __bases__ 속성

    메타 객체에는 __bases__ 속성이 있을 수 있습니다. 이 속성이 있다면, 이는 다른 메타 객체들, 즉 베이스들의 시퀀스(반드시 튜플일 필요는 없음)여야 합니다. __bases__가 없는 것은 빈 베이스 시퀀스와 같습니다. __bases__ 속성으로 정의되는 메타 객체 간의 관계에는 절대로 순환이 있어서는 안 됩니다. 다시 말해, __bases__ 속성은 파생 메타 객체에서 그 베이스 메타 객체를 가리키는 간선을 가진 방향성 비순환 그래프를 정의합니다. (여러 클래스가 같은 베이스 클래스를 가질 수 있으므로 반드시 트리인 것은 아닙니다.) 상속 그래프에 있는 메타 객체의 __dict__ 속성은 __class__ 속성이 상속 트리의 루트를 가리키는 일반 객체에 속성 디스크립터를 제공합니다(이 루트는 상속 계층의 루트와 같지 않으며, 오히려 그 반대에 가깝습니다. 상속 트리를 흔히 그리는 방식으로 보면 맨 아래에 위치합니다). 디스크립터는 우선순위 규칙(다음 단락 참조)에 따라 먼저 루트 메타 객체의 딕셔너리에서 검색되고, 그다음 그 베이스들에서 검색됩니다.

  5. 우선순위 규칙

    주어진 일반 객체의 상속 그래프에 있는 두 메타 객체가 모두 같은 이름의 속성 디스크립터를 정의하는 경우, 검색 순서는 메타 객체가 결정합니다. 이를 통해 서로 다른 메타 객체가 서로 다른 검색 순서를 정의할 수 있습니다. 특히 클래식 클래스는 예전의 왼쪽에서 오른쪽으로 가는 깊이 우선 규칙을 사용하는 반면, 새 스타일 클래스는 더 발전된 규칙을 사용합니다(PEP 253의 메서드 결정 순서에 관한 절을 참고하십시오).

    동적 속성(일반 객체의 __dict__에 정의된 속성)이 정적 속성(일반 객체의 __class__를 루트로 하는 상속 그래프에서 메타 객체가 정의한 속성)과 같은 이름을 가질 때, 정적 속성이 __set__ 메서드를 정의하는 디스크립터라면(아래 참조) 정적 속성이 우선합니다. 그렇지 않으면(__set__ 메서드가 없으면) 동적 속성이 우선합니다. 다시 말해, 데이터 속성(__set__ 메서드가 있는 속성)의 경우에는 정적 정의가 동적 정의를 재정의하지만, 그 밖의 속성의 경우에는 동적 정의가 정적 정의를 재정의합니다.

    근거: “정적인 것이 동적인 것을 재정의한다”나 “동적인 것이 정적인 것을 재정의한다” 같은 단순한 규칙은 둘 수 없습니다. 일부 정적 속성은 실제로 동적 속성을 재정의하기 때문입니다. 예를 들어 인스턴스의 __dict__에 있는 ‘__class__’ 키는 무시되고 정적으로 정의된 __class__ 포인터가 우선합니다. 하지만 반대로 inst.__dict__에 있는 키 대부분은 inst.__class__에 정의된 속성을 재정의합니다. 디스크립터에 __set__ 메서드가 있으면 데이터 디스크립터입니다. (읽기 전용 데이터 디스크립터에도 __set__ 메서드가 있으며, 이 메서드는 항상 예외를 발생시킵니다.) 디스크립터에 __set__ 메서드가 없으면 그 디스크립터는 대입을 가로챌 생각이 없다는 뜻입니다. 이때는 기존 규칙이 적용됩니다. 메서드와 이름이 같은 인스턴스 변수는 삭제되기 전까지 그 메서드를 가립니다.

  6. 속성 디스크립터

    여기서부터 흥미로워지지만 복잡해지기도 합니다. 속성 디스크립터(줄여서 디스크립터)는 메타 객체의 __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의 인스턴스로 제한된 언바운드 메서드를 반환할 수 있습니다. 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는 static이라는 단어가 이미 너무 많은 의미로 쓰이고 있으므로 “staticmethod” 대신 “sharedmethod”를 사용하자고 제안합니다. 하지만 shared가 올바른 의미를 전달하는지는 확신하지 못합니다.)

클래스 메서드는 비슷한 패턴을 사용하여, 자신이 호출된 대상 클래스를 암묵적인 첫 번째 인자로 받는 메서드를 선언합니다. 이는 C++나 Java에는 대응하는 것이 없으며, Smalltalk의 클래스 메서드와도 완전히 같지는 않지만, 비슷한 목적으로 쓰일 수 있습니다. Armin Rigo에 따르면, 이는 Borland Pascal 방언인 Delphi의 “가상 클래스 메서드”와 비슷합니다. (Python에는 진짜 메타클래스도 있으며, 어쩌면 메타클래스에 정의된 메서드가 “클래스 메서드”라는 이름에 더 어울릴지도 모릅니다. 하지만 대부분의 프로그래머는 메타클래스를 사용하지 않을 것으로 예상합니다.) 다음은 예제입니다:

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): # 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는 오래된 메커니즘을 새로운 용도로 활용한 것입니다. 각 속성에는 디스크립터 레코드가 있으며, 여기에는 이름, 타입을 나타내는 enum(PyObject *뿐만 아니라 다양한 C 타입이 지원됩니다), 인스턴스 시작 위치로부터의 오프셋, 읽기 전용 플래그가 들어 있습니다.

struct getsetlist 메커니즘은 새로 도입된 것입니다. 추가 검사가 필요하거나 단순히 계산되는 속성이어서 기존 틀에 맞지 않는 경우를 위한 것입니다. 여기서 각 속성에는 이름, getter C 함수 포인터, setter C 함수 포인터, 컨텍스트 포인터가 있습니다. 함수 포인터는 선택 사항입니다. 예를 들어 setter 함수 포인터를 NULL로 설정하면 읽기 전용 속성이 됩니다. 컨텍스트 포인터는 범용 getter/setter 함수에 보조 정보를 전달하기 위한 것이지만, 아직 이것이 필요한 경우를 찾지 못했습니다.

내장 메서드를 선언하는 유사한 메커니즘도 있다는 점에 유의하십시오. 바로 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)에 저장됩니다.
  • 디스크립터는 대부분 해당 구조체를 가리키기만 하는 매우 가벼운 객체입니다. 구현 세부 사항으로, 모든 디스크립터는 같은 객체 타입을 공유하며, 판별 필드가 디스크립터의 종류(method, member, getset)를 나타냅니다.
  • PEP 252에서 설명했듯이, 디스크립터에는 get() 메서드가 있습니다. 이 메서드는 객체 인자를 받아 그 객체의 속성을 반환합니다. 쓰기 가능한 속성의 디스크립터에는 set() 메서드도 있으며, 이 메서드는 객체와 값을 받아 그 객체의 속성을 설정합니다. get() 객체는 메서드에 대해 bind() 연산 역할도 하여, 바인딩되지 않은 메서드 구현을 객체에 바인딩한다는 점에 유의하십시오.
  • 이제 거의 모든 내장 객체는 tp_getattro 및 tp_setattro 구현을 직접 제공하지 않습니다. 대신 tp_getattro 및 tp_setattro 슬롯에 PyObject_GenericGetAttr와 (쓰기 가능한 속성이 있다면) PyObject_GenericSetAttr를 넣습니다. (또는 첫 번째 인스턴스가 생성되기 전에 해당 타입에 대해 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가 발생합니다.
  • 그러면 이제 더 복잡한 경우를 살펴보겠습니다. 위에서 설명한 접근 방식은 리스트, 문자열, 숫자와 같은 대부분의 내장 객체에 적합합니다. 그러나 일부 객체 타입은 인스턴스마다 임의의 속성을 저장할 수 있는 딕셔너리를 가지고 있습니다. 실제로 class 문을 사용하여 기존 내장 타입의 서브타입을 만들면 이러한 딕셔너리가 자동으로 생깁니다(또 다른 고급 기능인 __slots__를 사용하여 명시적으로 끄지 않는 한). 타입 딕셔너리와 구별하기 위해 이것을 인스턴스 딕셔너리라고 부릅시다.
  • 더 복잡한 경우에는 인스턴스 딕셔너리에 저장된 이름과 타입 딕셔너리에 저장된 이름 사이에 충돌이 발생합니다. 두 딕셔너리 모두에 같은 키를 가진 항목이 있다면 어느 쪽을 반환해야 합니까? 지침을 얻고자 클래식 Python을 살펴보면 상충하는 규칙이 있습니다. 클래스 인스턴스의 경우 인스턴스 딕셔너리가 클래스 딕셔너리보다 우선하지만, 특수 속성(__dict__와 __class__ 등)은 예외로 인스턴스 딕셔너리보다 우선합니다.
  • 저는 PyObject_GenericGetAttr()에 구현된 다음 규칙들로 이 문제를 해결했습니다:
    1. 타입 딕셔너리를 찾아봅니다. 데이터 디스크립터를 찾으면 그 get() 메서드를 사용하여 결과를 만듭니다. 이것으로 __dict__와 __class__ 같은 특수 속성이 처리됩니다.
    2. 인스턴스 딕셔너리를 찾아봅니다. 무엇이든 찾으면 그것으로 끝입니다. (이것으로 보통은 인스턴스 딕셔너리가 클래스 딕셔너리보다 우선한다는 요구 사항이 처리됩니다.)
    3. 타입 딕셔너리를 다시 찾아봅니다(물론 실제로는 1단계에서 저장해 둔 결과를 사용합니다). 디스크립터를 찾으면 그 get() 메서드를 사용하고, 다른 것을 찾으면 그것으로 끝이며, 아무것도 없으면 AttributeError를 발생시킵니다.

    이를 위해서는 디스크립터를 데이터 디스크립터와 비데이터 디스크립터로 분류해야 합니다. 현재 구현은 매우 합리적으로 member 디스크립터와 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의 부분 구현은 CVS에서 “descr-branch”라는 이름의 브랜치로 제공됩니다. 이 구현을 실험해 보려면 http://sourceforge.net/cvs/?group_id=5470 의 안내에 따라 CVS에서 Python을 체크아웃하되, cvs checkout 명령에 “-r descr-branch” 인자를 추가하십시오. (기존 체크아웃에서 시작하여 “cvs update -r descr-branch”를 실행할 수도 있습니다.) 여기에서 설명한 기능의 몇 가지 예시는 Lib/test/test_descr.py 파일을 참조하십시오. descr-branch는 브랜치의 이름입니다.

참고: 이 브랜치의 코드는 이 PEP의 범위를 훨씬 넘어섭니다. 이 브랜치는 PEP 253 (내장 타입의 서브타이핑)을 위한 실험 공간이기도 합니다.

참고 문헌

XXX