PEP 3115 – Python 3000의 메타클래스
- Author:
- Talin <viridia at gmail.com>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 07-Mar-2007
- Python-Version:
- 3.0
- Post-History:
- 11-Mar-2007, 14-Mar-2007
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 메타클래스를 선언하는 구문을 변경하고, 메타클래스를 가진 클래스를 구성하는 방식의 의미 체계를 변경할 것을 제안합니다.
근거
이 PEP에는 두 가지 근거가 있으며, 둘 다 다소 미묘합니다.
메타클래스의 작동 방식을 변경하는 주된 이유는 현재 가능한 것보다 클래스 구성 과정에 더 일찍 메타클래스가 관여해야 하는 흥미로운 사용 사례가 여러 가지 있기 때문입니다. 현재 메타클래스 메커니즘은 본질적으로 후처리 단계입니다. 클래스 데코레이터가 등장하면서 이러한 후처리 작업의 상당 부분을 데코레이터 메커니즘이 맡을 수 있습니다.
특히 클래스 멤버가 선언되는 순서를 보존하는 것이 유용한 중요한 사용 사례들이 있습니다. 일반적인 Python 객체는 멤버를 딕셔너리에 저장하며, 이때 순서는 중요하지 않고 멤버는 오직 이름으로만 접근합니다. 그러나 Python은 멤버가 암묵적인 순서에 따라 구성되는 외부 시스템과 인터페이스하는 데 자주 사용됩니다. 예로는 C 구조체 선언, COM 객체, ORM에서 사용하는 것과 같은 Python 클래스의 IDL 또는 데이터베이스 스키마로의 자동 변환 등이 있습니다.
이러한 경우 Python 프로그래머가 클래스 멤버의 선언 순서를 사용하여 그러한 순서를 직접 지정할 수 있다면 유용할 것입니다. 현재 이러한 순서는 다른 메커니즘을 사용하여 명시적으로 지정해야 합니다(예를 들어 ctypes 모듈을 참조하십시오).
안타깝게도 현재 메타클래스를 선언하는 방식으로는 이를 허용할 수 없습니다. 메타클래스가 관여할 때가 되면 순서 정보가 이미 사라져 있기 때문입니다. 새로운 시스템은 메타클래스가 클래스 구성 과정에 더 일찍 관여하도록 함으로써 순서 또는 구성 과정에서 더 일찍 생성된 다른 산출물을 보존하고 검사할 수 있도록 합니다.
제안된 메타클래스 메커니즘은 선언 순서 보존 외에도 여러 가지 흥미로운 사용 사례를 지원합니다. 한 가지 사용 사례는 클래스 구성 중에만 유효한 기호를 클래스 본문의 네임스페이스에 삽입하는 것입니다. 예를 들어 클래스 멤버를 생성할 때 사용하는 작은 함수인 “필드 생성자”가 이에 해당할 수 있습니다. 또 다른 흥미로운 가능성은 전방 참조, 즉 클래스 본문 아래쪽에서 선언되는 Python 기호에 대한 참조를 지원하는 것입니다.
더 약한 또 다른 근거는 순전히 외관상의 것입니다. 현재 메타클래스를 지정하는 방식은 특수 변수 __metaclass__에 대입하는 것이며, 일부 사람들은 이것이 미적으로 그다지 이상적이지 않다고 생각합니다. 다른 사람들은 그 의견에 강하게 동의하지 않습니다. 이 PEP는 이 문제를 언급하는 것 외에는 다루지 않습니다. 미적 논쟁은 논리적 증명으로 해결할 수 없기 때문입니다.
사양
새로운 모델에서는 기본 클래스 목록에서 키워드 인자를 통해 메타클래스를 지정하는 구문을 사용합니다.:
class Foo(base1, base2, metaclass=mymeta):
...
여기에서는 추가 키워드도 허용되며, 다음 예와 같이 메타클래스에 전달됩니다.:
class Foo(base1, base2, metaclass=mymeta, private=True):
...
이 PEP는 이러한 다른 키워드가 무엇일 수 있는지 정의하려고 하지 않는다는 점에 유의하십시오. 이는 메타클래스 구현자가 결정할 사항입니다.
더 일반적으로 클래스 정의에 전달되는 매개변수 목록은 이제 함수 호출의 모든 기능을 지원합니다. 즉, 이제 클래스 기본 클래스 목록에서 *args 및 **kwargs 스타일의 인자를 사용할 수 있습니다.:
class Foo(*bases, **kwds):
...
메타클래스 호출
현재 메타클래스 시스템에서 메타클래스 객체는 어떤 호출 가능 객체 타입이든 될 수 있습니다. 이 점은 변경되지 않지만, 새로운 기능을 모두 충분히 활용하려면 메타클래스에 클래스 사전 구성 중 사용되는 추가 속성이 있어야 합니다.
이 속성의 이름은 __prepare__이며, 클래스 본문을 평가하기 전에 함수로 호출됩니다. __prepare__함수는 두 개의 위치 인자와 임의 개수의 키워드 인자를 받습니다. 두 개의 위치 인자는 다음과 같습니다.
| name | 생성되는 클래스의 이름입니다. |
| bases | 베이스 클래스의 목록입니다. |
인터프리터는 호출하기 전에 항상 __prepare__의 존재 여부를 검사합니다. 존재하지 않으면 다음 Python 코드 조각에 나와 있듯이 일반 딕셔너리를 사용합니다.
def prepare_class(name, *bases, metaclass=None, **kwargs):
if metaclass is None:
metaclass = compute_default_metaclass(bases)
prepare = getattr(metaclass, '__prepare__', None)
if prepare is not None:
return prepare(name, bases, **kwargs)
else:
return dict()
위의 예제는 ‘class’에 전달되는 인자가 어떻게 해석되는지 보여 줍니다. 클래스 이름이 첫 번째 인자이고, 그 뒤에 임의 길이의 베이스 클래스 목록이 이어집니다. 베이스 클래스 뒤에는 하나 이상의 키워드 인자가 올 수 있으며, 그중 하나가 메타클래스일 수 있습니다. 메타클래스 인자는 일반 매개변수 할당 알고리즘에 의해 필터링되므로 kwargs에 포함되지 않는다는 점에 유의하십시오. (또한 PEP 3102에 따라 메타클래스는 키워드 전용 인자라는 점에도 유의하십시오.)
__prepare__는 필수가 아니지만, 기본 메타클래스(‘type’)는 서브클래스가 super()를 통해 이를 호출할 수 있도록 편의를 위해 구현합니다.
__prepare__는 클래스 본문을 평가하는 동안 클래스 멤버 정의를 저장하는 데 사용되는 딕셔너리와 유사한 객체를 반환합니다. 즉, 클래스 본문은 함수 블록으로 평가되지만(현재와 동일함), 지역 변수 딕셔너리가 __prepare__가 반환한 딕셔너리로 대체된다는 점이 다릅니다. 이 딕셔너리 객체는 일반 딕셔너리일 수도 있고 사용자 지정 매핑 타입일 수도 있습니다.
이 딕셔너리와 유사한 객체는 딕셔너리 인터페이스 전체를 지원할 필요가 없습니다. 제한된 딕셔너리 연산 집합을 지원하는 딕셔너리는 클래스 본문을 평가하는 동안 수행할 수 있는 동작의 종류를 제한합니다. 최소한의 구현은 딕셔너리에 값을 추가하고 딕셔너리에서 값을 가져오는 기능만 지원할 수 있으며, 대부분의 클래스 본문은 평가 중에 그 이상을 수행하지 않습니다. 일부 클래스에서는 삭제도 지원하는 것이 바람직할 수 있습니다. 많은 메타클래스는 이후에 이 딕셔너리의 복사본을 만들어야 하므로, 반복이나 딕셔너리 내용을 읽어 내는 다른 방법도 유용할 수 있습니다.
__prepare__메서드는 메타클래스 인스턴스(즉, 클래스 자체)가 생성되기 전에 호출되므로 인스턴스 메서드보다는 클래스 메서드로 구현되는 경우가 가장 많습니다.
클래스 본문의 평가가 끝나면 메타클래스는 현재의 메타클래스 메커니즘과 다르지 않게, 클래스 딕셔너리를 인자로 받아 호출 가능 객체로 호출됩니다.
일반적으로 메타클래스는 dict의 서브클래스이거나 dict를 감싼 래퍼인 사용자 지정 딕셔너리를 생성하며, 이 딕셔너리에는 클래스 본문을 평가하기 전이나 평가하는 동안 설정되는 추가 속성이 포함됩니다. 그런 다음 두 번째 단계에서 메타클래스는 이러한 추가 속성을 사용하여 클래스를 더욱 사용자 지정할 수 있습니다.
한 가지 예로 멤버 선언 순서에 관한 정보를 사용하여 C 구조체를 생성하는 메타클래스를 들 수 있습니다. 메타클래스는 삽입 순서의 기록을 단순히 유지하는 사용자 지정 딕셔너리를 제공합니다. 이는 완전한 ‘ordered dict’ 구현일 필요는 없으며, 각 삽입 시 추가되는 (key,value) 쌍의 Python 리스트이면 충분합니다.
이러한 경우 메타클래스는 중복 키가 발생할 가능성을 처리해야 하지만, 대부분의 경우 이는 간단합니다. 메타클래스는 첫 번째 선언이나 마지막 선언을 사용할 수도 있고, 어떤 방식으로든 이들을 결합할 수도 있으며, 단순히 예외를 발생시킬 수도 있습니다. 해당 경우를 어떻게 처리할지는 메타클래스가 결정합니다.
예제
다음은 선언된 순서대로 모든 클래스 멤버의 이름 목록을 생성하는 메타클래스의 간단한 예입니다.:
# The custom dictionary
class member_table(dict):
def __init__(self):
self.member_names = []
def __setitem__(self, key, value):
# if the key is not already defined, add to the
# list of keys.
if key not in self:
self.member_names.append(key)
# Call superclass
dict.__setitem__(self, key, value)
# The metaclass
class OrderedClass(type):
# The prepare function
@classmethod
def __prepare__(metacls, name, bases): # No keywords in this case
return member_table()
# The metaclass invocation
def __new__(cls, name, bases, classdict):
# Note that we replace the classdict with a regular
# dict before passing it to the superclass, so that we
# don't continue to record member names after the class
# has been created.
result = type.__new__(cls, name, bases, dict(classdict))
result.member_names = classdict.member_names
return result
class MyClass(metaclass=OrderedClass):
# method1 goes in array element 0
def method1(self):
pass
# method2 goes in array element 1
def method2(self):
pass
샘플 구현입니다.
Guido van Rossum이 새 기능을 구현하는 패치를 작성했습니다: https://bugs.python.org/issue1681101
대안 제안입니다.
Josiah Carlson은 실제로 지정되는 것은 타입의 타입이라는 이론에 근거하여 ‘메타클래스’ 대신 ‘type’이라는 이름을 사용할 것을 제안했습니다. 이는 기술적으로는 정확하지만, 새 클래스를 생성하는 프로그래머의 관점에서는 혼란을 일으키기도 합니다. 애플리케이션 프로그래머의 관점에서 관심을 두는 ‘type’은 자신이 작성하는 클래스이며, 그 타입의 타입이 메타클래스입니다.
메타클래스를 한 번은 클래스 딕셔너리를 생성하기 위해 호출하고, 또 한 번은 클래스를 ‘finish’하기 위해 호출하는 ‘two-phase’ 생성 프로세스에 대해 논의 중 일부 이의가 있었습니다. 일부 사람들은 이 두 단계가 완전히 분리되어야 하며, 사용자 지정 딕셔너리를 지정하는 구문과 메타클래스를 지정하는 구문이 별도로 있어야 한다고 생각했습니다. 그러나 대부분의 경우 이 둘은 밀접하게 연결되며, 메타클래스는 클래스 딕셔너리의 내부 세부 사항을 매우 잘 알고 있을 가능성이 높습니다. 프로그래머에게 올바른 딕셔너리 타입과 올바른 메타클래스 타입을 함께 사용하도록 요구하면, 프로그래머에게 추가적이고 불필요한 부담이 발생합니다.
또 다른 좋은 제안은 모든 클래스에 단순히 순서가 있는 딕셔너리를 사용하고, 전체 ‘custom dict’ 메커니즘을 생략하자는 것이었습니다. 이는 사용자 지정 딕셔너리의 대부분의 용례가 순서 정보를 보존하기 위한 것이라는 관찰에 근거했습니다. 그러나 이 아이디어에는 몇 가지 단점이 있습니다. 첫째, Python의 내장 타입 집합에 순서가 있는 딕셔너리 구현을 추가해야 하며, 둘째, 모든 클래스 선언에 약간의 속도 및 복잡성 비용이 발생하기 때문입니다. 이후 여러 사람이 필드 순서 보존 이외에 사용자 지정 딕셔너리를 사용할 수 있는 용례를 제시했으므로, 이 아이디어는 폐기되었습니다.
하위 호환성입니다.
기존 __metaclass__ 구문을 그대로 유지할 수도 있습니다. 또는 Py3K 변환 도구의 구문 규칙을 수정하여 이전 구문을 새 구문으로 변환하는 것도 그다지 어렵지 않을 것입니다.
참고 문헌입니다.
[1] [Python-3000] Py3K의 메타클래스(원래 제안) https://mail.python.org/pipermail/python-3000/2006-December/005030.html
[2] [Python-3000] Py3K의 메타클래스(Guido가 제안한 구문) https://mail.python.org/pipermail/python-3000/2006-December/005033.html
[3] [Python-3000] Py3K의 메타클래스(2단계 초기화에 대한 이의) https://mail.python.org/pipermail/python-3000/2006-December/005108.html
[4] [Python-3000] Py3K의 메타클래스(항상 순서가 있는 딕셔너리 사용) https://mail.python.org/pipermail/python-3000/2006-December/005118.html
Copyright
This document has been placed in the public domain.