PEP 422 – 클래스 생성의 더 간단한 사용자 정의
- Author:
- Alyssa Coghlan <ncoghlan at gmail.com>, Daniel Urban <urban.dani+py at gmail.com>
- Status:
- Withdrawn
- Type:
- Standards Track
- Created:
- 05-Jun-2012
- Python-Version:
- 3.5
- Post-History:
- 05-Jun-2012, 10-Feb-2013
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
현재 클래스 생성을 사용자 정의하려면 사용자 정의 메타클래스를 사용해야 합니다. 그러면 이 사용자 정의 메타클래스가 클래스의 전체 수명 주기 동안 유지되므로, 불필요한 메타클래스 충돌이 발생할 가능성이 생깁니다.
이 PEP는 대신 클래스 헤더의 새로운 namespace 매개변수와 클래스 본문의 새로운 __autodecorate__ 훅을 통해 광범위한 사용자 정의 시나리오를 지원할 것을 제안합니다.
새로운 메커니즘은 사용자 정의 메타클래스를 구현하는 것보다 이해하고 사용하기 쉬워야 하며, 따라서 Python 메타클래스 기능의 모든 강력한 기능을 더 쉽게 익힐 수 있도록 해야 합니다.
PEP 철회
이 제안은 Martin Teichmann의 PEP 487 제안을 지지하는 방향으로 철회되었습니다. 해당 제안은 더 단순하고 사용하기 쉬운 __init_subclass__ 훅을 통해 동일한 목표를 달성하며, 이 훅을 정의하는 베이스 클래스에 대해서는 해당 훅을 호출하지 않을 뿐입니다.
배경
이미 생성된 클래스 cls에서 “메타클래스”라는 용어는 명확한 의미를 가집니다. 즉, type(cls)의 값입니다.
클래스 생성 중에는, 이 용어는 또 다른 의미를 가집니다. 클래스 정의의 일부로 제공될 수 있는 메타클래스 힌트를 가리키는 데에도 사용됩니다. 많은 경우 이 두 의미가 결국 동일한 하나의 객체를 가리키지만, 그렇지 않은 경우가 두 가지 있습니다.
- 메타클래스 힌트가
type의 인스턴스를 가리키는 경우, 해당 힌트는 정의 중인 클래스의 모든 부모 클래스 메타클래스와 함께 후보 메타클래스로 간주됩니다. 후보 중에서 더 적절한 메타클래스가 발견되면, 메타클래스 힌트에 지정된 메타클래스 대신 해당 메타클래스가 사용됩니다. - 그렇지 않으면 명시적 메타클래스 힌트가 팩토리 함수로 간주되고, 클래스 객체를 생성하기 위해 직접 호출됩니다. 이 경우 최종 메타클래스는 팩토리 함수 정의에 의해 결정됩니다. 일반적인 경우(팩토리 함수가 단순히
type또는 Python 3.3 이상에서는types.new_class를 호출하는 경우)에는 실제 메타클래스가 부모 클래스에 따라 결정됩니다.
실제 메타클래스만 상속된다는 점은 주목할 만합니다. 메타클래스 훅으로 사용되는 팩토리 함수는 현재 정의 중인 클래스만 확인하며, 어떤 서브클래스에 대해서도 호출되지 않습니다.
Python 3에서는 클래스 헤더의 metaclass=Meta 키워드 구문을 사용하여 메타클래스 힌트를 제공합니다. 이를 통해 메타클래스의 __prepare__ 메서드를 사용하여 클래스 본문 실행 중에 사용되는 locals() 네임스페이스를 생성할 수 있습니다(예를 들어 일반 dict 대신 collections.OrderedDict를 사용하도록 지정할 수 있습니다).
Python 2에는 __prepare__ 메서드가 없었습니다(이 API는 PEP 3115를 통해 Python 3에 추가되었습니다). 대신 클래스 본문에서 __metaclass__ 속성을 설정할 수 있었으며, 클래스 생성 과정에서 클래스 네임스페이스에서 해당 값을 추출하여 메타클래스 힌트로 사용했습니다. 이 기능을 사용하는 published code가 있습니다.
Python 3의 또 다른 새로운 기능은 PEP 3135에서 도입된 인자 없는 super() 내장 함수 형식입니다. 이 기능은 암시적 __class__ 참조를 사용하여 Python 2에서 필요했던 “이름으로 지정하는” 참조를 정의 중인 클래스로 대체합니다. Python 2 메타클래스 실행 중 호출된 코드가 클래스 이름을 참조하는 메서드를 호출할 수 없었던 것과 마찬가지로(해당 이름이 포함 범위에 아직 바인딩되지 않았기 때문입니다), Python 3 메타클래스도 암시적 __class__ 참조에 의존하는 메서드를 호출할 수 없습니다(메타클래스가 클래스 생성 메커니즘에 제어를 반환한 후에야 해당 참조가 채워지기 때문입니다).
마지막으로 클래스가 사용자 정의 메타클래스를 사용하면 다중 상속 사용에 추가적인 문제가 발생할 수 있습니다. 새로운 클래스는 서로 관련이 없는 메타클래스를 가진 부모 클래스에서 상속할 수 없기 때문입니다. 이는 이미 공개된 클래스에 메타클래스를 추가하는 것이 불가능하다는 의미입니다. 메타클래스 충돌 위험 때문에 그러한 추가는 하위 호환성이 없는 변경이기 때문입니다.
제안
이 PEP는 다음 기준을 충족하는 클래스 생성 사용자 정의 메커니즘을 Python 3.4에 추가할 것을 제안합니다.
- 클래스 상속 구조(믹스인 및 다중 상속 포함)와 원활하게 통합됩니다.
- 암시적
__class__참조 및 PEP 3135에서 도입된 인자가 없는super()구문과 원활하게 통합됩니다. - 기존 베이스 클래스에 하위 호환성 문제를 초래할 상당한 위험 없이 추가할 수 있습니다
- 클래스 네임스페이스 자체를 채우는 것 이상으로 클래스 생성 과정에 어느 정도 영향을 미칠 수 있는 능력을 복원하지만, 잠재적으로 Python 2 스타일의
__metaclass__훅이 제공하는 완전한 유연성은 제공하지 않습니다
이 목표를 달성할 수 있는 한 가지 메커니즘은 기존의 명시적 클래스 데코레이터를 직접 모델로 삼은 새로운 암시적 클래스 데코레이션 훅을 추가하는 것입니다. 다만 이 훅은 클래스 정의 헤더의 일부가 아니라 클래스 본문이나 부모 클래스에 정의됩니다
구체적으로는 클래스 정의에서 다음과 같은 클래스 초기화 훅을 제공할 수 있도록 하는 방안이 제안됩니다:
class Example:
def __autodecorate__(cls):
# This is invoked after the class is created, but before any
# explicit decorators are called
# The usual super() mechanisms are used to correctly support
# multiple inheritance. The class decorator style signature helps
# ensure that invoking the parent class is as simple as possible.
cls = super().__autodecorate__()
return cls
협력적 다중 상속의 경우를 단순화하기 위해 object은 클래스를 수정하지 않고 반환하는 훅의 기본 구현을 갖게 됩니다:
class object:
def __autodecorate__(cls):
return cls
어떤 이유로 메타클래스가 암시적 클래스 데코레이션을 차단하려는 경우, cls.__autodecorate__가 AttributeError를 발생시키도록 조치해야 합니다
새로 생성된 객체에 이 훅이 있으면, 클래스 생성 메커니즘은 __class__ 참조가 초기화된 후에 이 훅을 호출합니다 types.new_class()의 경우 생성된 클래스 객체를 반환하기 직전의 마지막 단계로 호출됩니다. 클래스가 생성될 때 __autodecorate__는 훅이 호출되기 전에 암묵적으로 클래스 메서드로 변환됩니다
__autodecorate__가 호출될 때 클래스 이름은 아직 새 클래스 객체에 바인딩되지 않았다는 점에 유의하십시오 그 결과 메서드를 호출할 때 super()의 두 인자 형식을 사용할 수 없습니다(예를 들어 super(Example, cls)는 위의 예제에서 작동하지 않습니다) 그러나 __class__ 참조가 이미 초기화되어 있으므로 super()의 인자 없는 형식은 예상대로 작동합니다
이 일반적인 제안은 새로운 아이디어가 아닙니다(언어 정의에 포함하자는 제안은 more than 10 years ago에 처음 제기되었고, 유사한 메커니즘은 Zope’s ExtensionClass에서 오랫동안 지원되어 왔습니다). 그러나 최근 몇 년 사이 상황이 충분히 변했으므로 이 아이디어를 네이티브 언어 기능으로 포함하는 방안을 재고할 가치가 있습니다
또한 PEP 3115에서 메타클래스 __prepare__ 메서드가 도입됨에 따라 Python 2에서는 불가능했던 추가 개선이 가능해졌습니다. 이 PEP에서는 type.__prepare__를 업데이트하여 namespace 를 키워드 전용 인자로 받는 팩토리 함수를 허용할 것을 제안합니다 namespace 인자가 제공되면 해당 인자에 지정된 값이 인자 없이 호출되어, 새로 생성된 딕셔너리 인스턴스를 사용하는 대신 type.__prepare__의 결과를 생성합니다 예를 들어 다음 코드는 순서가 있는 딕셔너리를 클래스 네임스페이스로 사용합니다:
class OrderedExample(namespace=collections.OrderedDict):
def __autodecorate__(cls):
# cls.__dict__ is still a read-only proxy to the class namespace,
# but the underlying storage is an OrderedDict instance
Note
이 PEP는 여러 클래스 객체 사이에서 단일 네임스페이스를 공유하기 위해 __prepare__를 사용할 수 있는 기존 기능과 함께, 속성 조회 캐싱의 잠재적인 문제를 부각합니다. 기본 매핑이 다른 수단으로 업데이트되면 속성 조회 캐시가 올바르게 무효화되지 않습니다(이는 클래스 __dict__ 속성이 기본 저장소를 읽기 전용 뷰로 제공하는 주요 이유 중 하나입니다)
해당 캐시가 제공하는 최적화는 매우 유용하므로, 기존 네임스페이스를 클래스 네임스페이스로 사용하는 것은 공식적으로 지원되지 않는 것으로 선언해야 할 수 있습니다(캐시의 동기화가 어긋났을 때 관찰되는 동작이 상당히 이상하기 때문입니다)
주요 이점
클래스에서 사용자 정의 네임스페이스를 더 쉽게 사용
현재 클래스 네임스페이스에 다른 타입(예: collections.OrderedDict)을 사용하거나 미리 채워진 네임스페이스를 사용하려면 사용자 정의 메타클래스를 작성하여 사용해야 합니다 이 PEP를 사용하면 클래스 헤더에 적절한 팩토리 함수를 지정하는 것만으로 사용자 정의 네임스페이스를 간단하게 사용할 수 있습니다
정의 시점 동작을 더 쉽게 상속
Python의 메타클래스를 이해하려면 타입 시스템과 클래스 생성 과정을 깊이 이해해야 합니다 여러 구성 요소(코드, 메타클래스 힌트, 실제 메타클래스, 클래스 객체, 클래스 객체의 인스턴스)를 머릿속에서 명확히 구분해야 하므로 이것이 어렵다고 보는 것은 타당합니다 규칙을 알고 있더라도 극도로 주의하지 않으면 실수하기 쉽습니다 이 PEP의 이전 버전에는 실제로 이러한 실수가 포함되어 있었습니다. 실제로는 “type의 인스턴스”인 제약 조건을 “type의 서브클래스”라고 기술했습니다
제안된 암시적 클래스 데코레이션 훅을 이해하려면 데코레이터와 일반적인 메서드 상속만 이해하면 되므로, 그다지 위압적인 작업은 아닙니다 새 훅은 클래스 정의 과정에 포함된 모든 단계를 이해하는 방향으로 보다 점진적인 경로를 제공합니다
메타클래스 충돌 가능성 감소
라이브러리 작성자가 메타클래스 사용을 꺼리게 만드는 큰 문제 중 하나는 메타클래스 충돌의 위험입니다(메타클래스가 적절한 경우에도 그렇습니다) 이는 클래스 정의에서 원하는 부모 클래스들이 서로 관련 없는 두 메타클래스를 사용할 때마다 발생합니다 이러한 위험 때문에 이전에 메타클래스 없이 공개된 클래스에 메타클래스를 추가하는 일도 매우 어려워집니다
이와 대조적으로, 기존 타입에 __autodecorate__ 메서드를 추가하는 것은 __init__ 메서드를 추가하는 것과 비슷한 수준의 위험을 초래합니다. 기술적으로는 제대로 구현되지 않은 서브클래스를 손상시킬 위험이 있지만, 그런 일이 발생하면 라이브러리 작성자가 하위 호환성 보장을 위반한 것이 아니라 서브클래스의 버그로 인식됩니다. 실제로 __autodecorate__의 제한된 시그니처로 인해 이 경우의 위험은 __init__의 경우보다도 훨씬 낮습니다.
PEP 3135와 깔끔하게 통합됩니다.
메타클래스의 일부로 실행되는 코드와 달리, 새로운 훅의 일부로 실행되는 코드는 PEP 3135가 도입한 암시적 __class__참조에 의존하는 클래스 메서드를 자유롭게 호출할 수 있으며, 여기에는 super()의 인자 없는 형식을 사용하는 메서드도 포함됩니다.
__metaclass__를 동적으로 설정하는 많은 사용 사례를 대체합니다.
정의된 클래스를 완전히 대체하지 않는 사용 사례에서는 Python 2 코드가 동적으로 설정하던 __metaclass__대신 이제 __autodecorate__를 동적으로 설정할 수 있습니다. 더 고급 사용 사례에서는 Python 3을 지원하기 위해 명시적 메타클래스(필수 베이스 클래스로 제공될 수도 있음)를 도입해야 합니다.
설계 참고 사항
데코레이트되는 클래스가 베이스 클래스인지 판별하기
다른 모든 클래스 메서드와 마찬가지로 __autodecorate__ 메서드의 본문에서 __class__는 해당 메서드를 선언한 클래스에 바인딩되지만, 전달된 값은 서브클래스일 수 있습니다.
따라서 필요한 경우 베이스 클래스의 처리를 건너뛰는 작업을 비교적 간단하게 수행할 수 있습니다.:
class Example:
def __autodecorate__(cls):
cls = super().__autodecorate__()
# Don't process the base class
if cls is __class__:
return
# Process subclasses here
...
클래스를 다른 종류의 객체로 대체하기
암시적 데코레이터인 __autodecorate__는 정의된 클래스를 다른 종류의 객체로 비교적 쉽게 대체할 수 있습니다. 기술적으로는 사용자 정의 메타클래스와 __new__메서드도 이미 이를 암시적으로 수행할 수 있지만, 데코레이터 모델을 사용하면 이러한 코드를 훨씬 쉽게 이해하고 구현할 수 있습니다.
class BuildDict:
def __autodecorate__(cls):
cls = super().__autodecorate__()
# Don't process the base class
if cls is __class__:
return
# Convert subclasses to ordinary dictionaries
return cls.__dict__.copy()
명시적 데코레이터를 사용하는 대신 상속을 기반으로 이를 암시적으로 수행해야 하는 이유는 분명하지 않지만, 이러한 가능성은 언급할 가치가 있어 보입니다.
미해결 질문
namespace개념이 추가적인 복잡성을 감수할 가치가 있습니까?
새로운 __autodecorate__ 훅과 달리, 제안된 namespace 키워드 인자는 서브클래스에 자동으로 상속되지 않습니다. 이 제안이 현재 작성된 방식으로는 서브클래스에서 일관되게 사용되는 특수 네임스페이스를 얻을 수 있는 유일한 방법은 여전히 적절한 __prepare__구현을 갖춘 사용자 정의 메타클래스를 작성하는 것입니다.
사용자 정의 네임스페이스 팩토리도 상속되도록 변경하면 이 제안의 복잡성이 크게 증가하고, 사용자 정의 메타클래스 사용 시 발생하는 것과 동일한 잠재적 베이스 클래스 충돌 문제도 다수 도입됩니다.
Eric Snow는 대신 클래스 본문의 실행 네임스페이스를 기본적으로 순서가 보존되는 딕셔너리로 만들고, 향후 참조를 위해 클래스 속성 정의 순서를 클래스 객체의 속성(예: __definition_order__)으로 저장하자는 별도의 제안을 제시했습니다.
Eric이 제안한 접근 방식은 제안된 __autodecorate__ 훅과 잘 결합되는 타입의 새로운 기본 동작으로 더 나은 선택일 수 있으며, 더 복잡한 구성 가능한 네임스페이스 팩토리 아이디어는 아래에 보이는 것과 같은 사용자 정의 메타클래스에 맡길 수 있습니다.
클래스를 사용하는 새로운 방법
클래스 헤더의 새로운 namespace 키워드를 사용하면 클래스가 초기화되는 방식을 제어하는 여러 흥미로운 선택지가 가능해지며, 여기에는 JavaScript와 Ruby의 객체 모델 일부도 포함됩니다.
아래의 모든 예제는 실제로 오늘날에도 사용자 정의 메타클래스를 사용하여 구현할 수 있습니다.:
class CustomNamespace(type):
@classmethod
def __prepare__(meta, name, bases, *, namespace=None, **kwds):
parent_namespace = super().__prepare__(name, bases, **kwds)
return namespace() if namespace is not None else parent_namespace
def __new__(meta, name, bases, ns, *, namespace=None, **kwds):
return super().__new__(meta, name, bases, ns, **kwds)
def __init__(cls, name, bases, ns, *, namespace=None, **kwds):
return super().__init__(name, bases, ns, **kwds)
새로운 키워드를 type.__prepare__에서 직접 구현할 때의 장점은 지속되는 효과가 오직 클래스 속성의 기본 저장 방식 변경뿐이라는 점입니다. 클래스의 메타클래스는 변경되지 않으므로 이러한 종류의 사용자 정의와 일반적으로 관련된 단점이 많이 제거됩니다.
순서를 보존하는 클래스
class OrderedClass(namespace=collections.OrderedDict):
a = 1
b = 2
c = 3
미리 채워진 네임스페이스
seed_data = dict(a=1, b=2, c=3)
class PrepopulatedClass(namespace=seed_data.copy):
pass
프로토타입 클래스 복제
class NewClass(namespace=Prototype.__dict__.copy):
pass
클래스 확장
Note
PEP가 비교적 깔끔하게 이렇게 하는 것을 가능하게 만든다고 해서 누구나 이렇게 해야 한다는 뜻은 아닙니다!
from collections import MutableMapping
# The MutableMapping + dict combination should give something that
# generally behaves correctly as a mapping, while still being accepted
# as a class namespace
class ClassNamespace(MutableMapping, dict):
def __init__(self, cls):
self._cls = cls
def __len__(self):
return len(dir(self._cls))
def __iter__(self):
for attr in dir(self._cls):
yield attr
def __contains__(self, attr):
return hasattr(self._cls, attr)
def __getitem__(self, attr):
return getattr(self._cls, attr)
def __setitem__(self, attr, value):
setattr(self._cls, attr, value)
def __delitem__(self, attr):
delattr(self._cls, attr)
def extend(cls):
return lambda: ClassNamespace(cls)
class Example:
pass
class ExtendedExample(namespace=extend(Example)):
a = 1
b = 2
c = 3
>>> Example.a, Example.b, Example.c
(1, 2, 3)
거부된 설계안
type.__init__에서 __autodecorate__ 호출하기
type.__init__에서 새 훅을 자동으로 호출하는 방식은 이 PEP의 목표 대부분을 달성할 수 있을 것입니다. 하지만 이러한 접근 방식을 사용하면 __autodecorate__ 구현이 __class__ 참조에 의존하는(또는 인자가 없는 형태의 super()를 사용하는) 어떤 메서드도 호출할 수 없게 되며, 그 자체로도 이러한 기능을 사용할 수 없게 됩니다.
대신 현재의 설계는 초기 클래스 생성이 이미 완료된 후에 암묵적 데코레이터 훅을 실행함으로써, 명시적 데코레이터가 할 수 있는 모든 것을 이 훅도 할 수 있도록 보장합니다.
자동 데코레이션 훅을 __init_class__로 부르기
이전 버전의 PEP에서는 새 훅의 이름으로 __init_class__를 사용했습니다. 이 이름에는 세 가지 중대한 문제가 있었습니다:
- 올바른 철자가
__init_class__인지__class_init__인지 기억하기 어려웠습니다 - 이름에 “init”이 사용된 탓에 시그니처가
type.__init__의 것과 일치해야 한다는 인상을 주었지만, 실제로는 그렇지 않았습니다 - 이름에 “init”이 사용된 탓에 이 메서드가 초기 클래스 객체 생성 과정의 일부로 실행된다는 인상을 주었지만, 실제로는 그렇지 않았습니다
새 이름 __autodecorate__는 이 새로운 초기화 훅이 __init__ 메서드와 유사한 것이라기보다는 암묵적으로 호출되는 클래스 데코레이터로 이해하는 것이 가장 유용하다는 점을 명확히 하기 위해 선택되었습니다.
__autodecorate__에 명시적 데코레이터를 요구하기
원래 이 PEP는 __autodecorate__ 데코레이터에 @classmethod를 명시적으로 사용하도록 요구했습니다. 이를 생략했을 때 타당한 해석이 존재하지 않고, 어차피 유용한 오류 메시지를 제공하려면 이 경우를 감지해야 했기 때문에 암묵적으로 처리하도록 변경되었습니다.
이 결정은 __prepare__를 정의하면서 @classmethod 메서드 데코레이터를 빠뜨렸을 때의 사용자 경험이 유독 이해하기 어렵다는 점을 발견한 후 더욱 굳어졌습니다(특히 PEP 3115에서는 이를 일반 메서드로 문서화하고 있으며, 현재 문서에서도 이에 대해 명시적으로 언급하지 않고 있기 때문입니다).
__new__처럼 __autodecorate__를 암묵적으로 정적으로 만들기
__new__는 인스턴스화될 클래스를 첫 번째 인자로 받아들이지만, 실제로는 클래스 메서드가 아니라 암묵적으로 정적 메서드로 취급됩니다. 이 덕분에 정의된 클래스로부터 손쉽게 추출되어 서브클래스에서 직접 호출될 수 있으며, 가져온 클래스 객체에 결합되지 않습니다.
이러한 동작은 처음에는 새로운 __autodecorate__ 훅에도 잠재적으로 유용해 보이는데, __autodecorate__ 메서드를 다른 클래스에서 명시적 데코레이터로 손쉽게 사용할 수 있게 해주기 때문입니다.
하지만 이러한 겉보기 지원은 착각에 불과한데, 서브클래스에서 호출될 때만 올바르게 동작하며, 이 경우 해당 메서드는 서브클래스에서 그대로 손쉽게 가져와 그런 방식으로 호출할 수 있기 때문입니다. __new__와 달리, 상속 체인의 여러 지점에서 메서드 시그니처가 잠재적으로 바뀔 수 있다는 문제는 존재하지 않습니다.
팩토리 함수 대신 네임스페이스를 직접 전달하기
한때 이 PEP는 팩토리 함수를 전달하는 대신 클래스 네임스페이스를 키워드 인자로 직접 전달하도록 제안했습니다. 하지만 이는 지원되지 않는 동작(즉, 동일한 네임스페이스를 여러 클래스에 전달하거나, 클래스 네임스페이스로 사용되는 매핑에 대한 직접적인 쓰기 접근 권한을 계속 유지하는 것)을 부추기기 때문에, API는 팩토리 함수 버전으로 전환되었습니다.
참조 구현
__autodecorate__에 대한 참조 구현이 issue tracker에 게시되었습니다. 이 구현은 원래의 __init_class__ 명명법을 사용합니다. 암묵적 데코레이터가 클래스를 다른 객체로 대체하는 것은 아직 허용하지 않으며, type.__prepare__에 제안된 namespace 매개변수도 구현하지 않았습니다.
할 일
- https://mail.python.org/pipermail/python-dev/2013-February/123970.html 에 있는 다섯 가지 사항을 처리하기
참고 자료
Copyright
This document has been placed in the public domain.