PEP 487 – 클래스 생성의 더 간단한 사용자 지정
- Author:
- Martin Teichmann <lkb.teichmann at gmail.com>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 27-Feb-2015
- Python-Version:
- 3.6
- Post-History:
- 27-Feb-2015, 05-Feb-2016, 24-Jun-2016, 02-Jul-2016, 13-Jul-2016
- Replaces:
- 422
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
현재 클래스 생성을 사용자 지정하려면 사용자 지정 메타클래스를 사용해야 합니다. 그러면 이 사용자 지정 메타클래스가 클래스의 전체 수명 주기 동안 유지되므로, 불필요한 메타클래스 충돌이 발생할 가능성이 생깁니다.
이 PEP에서는 대신 클래스 본문에 있는 새로운 __init_subclass__ 후크와 특성을 초기화하는 후크를 통해 광범위한 사용자 지정 시나리오를 지원할 것을 제안합니다.
새로운 메커니즘은 사용자 지정 메타클래스를 구현하는 것보다 이해하고 사용하기 쉬워야 하며, 따라서 Python 메타클래스 기능의 모든 강력한 기능을 보다 쉽게 익힐 수 있도록 해야 합니다.
배경
메타클래스는 클래스 생성을 사용자 지정하는 강력한 도구입니다. 그러나 메타클래스를 자동으로 결합할 방법이 없다는 문제가 있습니다. 클래스에 두 메타클래스를 사용하려면 일반적으로 수동으로 두 메타클래스를 결합하는 새로운 메타클래스를 만들어야 합니다.
이러한 필요성은 사용자에게 뜻밖에 발생하는 경우가 많습니다. 서로 다른 두 라이브러리에서 제공하는 두 기반 클래스를 상속하면, 대개 라이브러리의 그러한 세부 사항에는 전혀 관심이 없는 상황에서 갑자기 결합된 메타클래스를 수동으로 만들어야 합니다. 한 라이브러리가 이전에는 사용하지 않던 메타클래스를 사용하기 시작하면 상황은 더욱 악화됩니다. 라이브러리 자체는 완벽하게 계속 작동하지만, 갑자기 해당 클래스들을 다른 라이브러리의 클래스와 결합하는 모든 코드가 실패합니다.
제안
메타클래스를 사용하는 방법은 다양하지만, 대부분의 사용 사례는 클래스 생성 후 실행되는 일부 초기화 코드, 디스크립터의 초기화, 클래스 특성이 정의된 순서 유지라는 단 세 가지 범주에 속합니다.
처음 두 범주는 클래스 생성 과정에 간단한 후크를 두면 쉽게 구현할 수 있습니다.
- 주어진 클래스의 모든 서브클래스를 초기화하는
__init_subclass__후크입니다. - 클래스를 생성할 때 클래스에 정의된 모든 특성(디스크립터)에서
__set_name__후크가 호출되며,
세 번째 범주는 다른 PEP인 PEP 520의 주제입니다.
첫 번째 사용 사례를 예로 들면 다음과 같습니다.:
>>> class QuestBase:
... # this is implicitly a @classmethod (see below for motivation)
... def __init_subclass__(cls, swallow, **kwargs):
... cls.swallow = swallow
... super().__init_subclass__(**kwargs)
>>> class Quest(QuestBase, swallow="african"):
... pass
>>> Quest.swallow
'african'
기본 클래스 object는 협력적 다중 상속의 종단점 역할을 하는 빈 __init_subclass__ 메서드를 포함합니다. 이 메서드에는 키워드 인자가 없으므로, 더 특수화된 모든 메서드는 모든 키워드 인자를 처리해야 한다는 점에 유의하십시오.
이 일반적인 제안은 새로운 아이디어가 아닙니다(언어 정의에 포함하자는 제안이 more than 10 years ago 처음 제시되었고, 유사한 메커니즘이 Zope의 ExtensionClass에서 오랫동안 지원되어 왔습니다). 그러나 최근 몇 년 사이 상황이 충분히 변했으므로, 포함을 위해 이 아이디어를 재검토할 가치가 있습니다.
이 제안의 두 번째 부분에서는 클래스 특성, 특히 디스크립터를 위한 __set_name__ 초기화자를 추가합니다. 디스크립터는 클래스 본문에 정의되지만 해당 클래스에 관한 어떤 것도 알지 못하며, 접근될 때 사용하는 이름조차 알지 못합니다. __get__이 호출되면 소유자를 알게 되지만, 여전히 자신의 이름은 알지 못합니다. 이는 안타까운 일입니다. 예를 들어 디스크립터는 자신의 이름을 모르기 때문에 연관된 값을 해당 이름으로 객체의 __dict__에 넣을 수 없습니다. 이 문제는 여러 번 해결되었으며, 라이브러리에 메타클래스를 두는 가장 중요한 이유 중 하나입니다. 제안의 첫 번째 부분을 사용하여 이러한 메커니즘을 구현하는 것은 쉽겠지만, 모든 사람이 이 문제에 사용할 하나의 해결책을 마련하는 것이 합리적입니다.
그 사용 예를 들면, 약한 참조 값을 나타내는 디스크립터를 생각해 보십시오.:
import weakref
class WeakAttribute:
def __get__(self, instance, owner):
return instance.__dict__[self.name]()
def __set__(self, instance, value):
instance.__dict__[self.name] = weakref.ref(value)
# this is the new initializer:
def __set_name__(self, owner, name):
self.name = name
이러한 WeakAttribute는 예를 들어 부모를 통한 순환 참조를 피하려는 트리 구조에서 사용할 수 있습니다.:
class TreeNode:
parent = WeakAttribute()
def __init__(self, parent):
self.parent = parent
parent 속성은 일반적인 속성처럼 사용되지만, 트리에는 순환 참조가 없으므로 사용되지 않을 때 쉽게 가비지 컬렉션될 수 있습니다. parent 속성은 부모가 더 이상 존재하지 않으면 마법처럼 None이 됩니다.
이 예제는 매우 간단해 보이지만, 지금까지 이러한 속성은 메타클래스를 사용하지 않고는 정의할 수 없었다는 점에 유의해야 합니다. 또한 이러한 메타클래스는 작업을 매우 어렵게 만들 수 있으므로, 이런 종류의 속성은 아직 존재하지 않습니다.
디스크립터를 초기화하는 작업은 __init_subclass__ 훅에서 간단히 수행할 수 있습니다. 그러나 이는 적절한 훅을 가진 클래스에서만 디스크립터를 사용할 수 있다는 뜻이며, 예제와 같은 일반 버전은 일반적으로 작동하지 않습니다. 또한 object.__init_subclass__의 기본 구현 내부에서 __set_name__을 호출할 수도 있습니다. 그러나 super()를 호출하는 것을 잊는 것은 흔한 실수이므로, 디스크립터가 갑자기 초기화되지 않는 일이 너무 자주 발생할 것입니다.
주요 이점
정의 시점 동작의 더 쉬운 상속
파이썬의 메타클래스를 이해하려면 타입 시스템과 클래스 생성 프로세스를 깊이 이해해야 합니다. 여러 구성 요소(코드, 메타클래스 힌트, 실제 메타클래스, 클래스 객체, 클래스 객체의 인스턴스)를 머릿속에서 명확히 구분해야 하므로, 이는 당연히 어려운 일로 여겨집니다. 규칙을 알고 있더라도 극도로 주의하지 않으면 여전히 실수하기 쉽습니다.
제안된 암시적 클래스 초기화 훅을 이해하는 데는 일반적인 메서드 상속만 필요하므로, 그다지 어려운 작업은 아닙니다. 새로운 훅은 클래스 정의 프로세스에 포함된 모든 단계를 더 점진적으로 이해할 수 있는 경로를 제공합니다.
메타클래스 충돌 가능성 감소
라이브러리 작성자가 메타클래스를 사용하기를 꺼리게 만드는 큰 문제 중 하나는 메타클래스 충돌 위험입니다. 이러한 충돌은 클래스 정의에서 원하는 부모들이 서로 관련 없는 두 메타클래스를 사용할 때마다 발생합니다. 이러한 위험 때문에 이전에 메타클래스 없이 공개된 클래스에 메타클래스를 추가하는 것도 매우 어려워집니다.
이와 대조적으로 기존 타입에 __init_subclass__ 메서드를 추가하는 것은 __init__ 메서드를 추가하는 것과 비슷한 수준의 위험을 초래합니다. 기술적으로는 잘못 구현된 서브클래스를 손상시킬 위험이 있지만, 그런 일이 발생하면 라이브러리 작성자가 하위 호환성 보장을 위반한 것이 아니라 서브클래스의 버그로 인식됩니다.
클래스를 사용하는 새로운 방법
서브클래스 등록
특히 플러그인 시스템을 작성할 때는 플러그인 베이스 클래스의 새로운 서브클래스를 등록하고 싶을 수 있습니다. 다음과 같이 수행할 수 있습니다.:
class PluginBase:
subclasses = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls)
이 예제에서 PluginBase.subclasses에는 전체 상속 트리에 있는 모든 서브클래스의 일반 리스트가 포함됩니다. 이는 믹스인 클래스로도 잘 작동한다는 점에 유의해야 합니다.
트레이트 디스크립터
예를 들어 값의 경계를 검사하는 다양한 설계의 파이썬 디스크립터가 사용되고 있습니다. 이러한 “트레이트”가 작동하려면 메타클래스의 지원이 필요한 경우가 많습니다. 이 PEP를 사용하면 다음과 같은 형태가 됩니다.:
class Trait:
def __init__(self, minimum, maximum):
self.minimum = minimum
self.maximum = maximum
def __get__(self, instance, owner):
return instance.__dict__[self.key]
def __set__(self, instance, value):
if self.minimum < value < self.maximum:
instance.__dict__[self.key] = value
else:
raise ValueError("value not in range")
def __set_name__(self, owner, name):
self.key = name
구현 세부 사항
훅은 다음 순서로 호출됩니다. 새 클래스가 초기화된 후 type.__new__가 디스크립터의 __set_name__ 훅을 호출합니다. 그런 다음 기반 클래스에서, 정확히 말하면 super()에서 __init_subclass__를 호출합니다. 이는 서브클래스 초기화 코드가 이미 완전히 초기화된 디스크립터를 볼 수 있음을 의미합니다. 이렇게 하면 필요한 경우 __init_subclass__ 사용자가 모든 디스크립터를 다시 수정할 수 있습니다.
또 다른 방법은 object.__init_subclass__의 기본 구현에서 __set_name__을 호출하는 것입니다. 이렇게 하면 __set_name__이 호출되는 것조차 방지할 수 있습니다. 그러나 대부분의 경우 그러한 방지는 우발적으로 발생할 것입니다. super() 호출을 잊는 일이 자주 발생하기 때문입니다.
세 번째 방법으로는 모든 작업을 type.__init__에서 수행할 수도 있습니다. 문서에서 권장하듯이 대부분의 메타클래스는 __new__에서 작업을 수행합니다. 많은 메타클래스는 인자를 super().__new__에 전달하기 전에 수정합니다. 이러한 종류의 클래스와 하위 호환성을 유지하려면 훅을 __new__에서 호출해야 합니다.
또 하나의 작은 변경도 이루어져야 합니다. 현재 CPython 구현에서는 type.__init__이 키워드 인자의 사용을 명시적으로 금지하는 반면, type.__new__는 해당 속성을 키워드 인자로 전달할 수 있도록 허용합니다. 이는 이상할 정도로 일관성이 없으므로 금지되어야 합니다. 현재 동작을 유지할 수도 있지만, 이는 전혀 사용되지 않을 가능성이 크므로 수정하는 편이 더 낫습니다. 유일한 사용 사례는 메타클래스가 name, bases, dict (그렇습니다. 현대 메타클래스에서 주로 사용하는 네임스페이스 또는 ns가 아니라 dict입니다)를 키워드 인자로 전달하여 super().__new__를 호출하는 경우일 것입니다. 이렇게 해서는 안 됩니다. 이 작은 변경은 Python 전반의 일관성을 향상시키면서 이 PEP의 구현을 크게 단순화합니다.
두 번째 변경으로, 새로운 type.__init__는 키워드 인자를 단순히 무시합니다. 현재는 키워드 인자가 제공되지 않았는지 확인합니다. 메타클래스가 키워드 인자를 처리하지 않는데 클래스 선언에 키워드 인자를 제공하면, 이로 인해 (의도한) 오류가 발생합니다. 키워드 인자를 받아들이려는 메타클래스 작성자는 __init__을 재정의하여 해당 인자를 제거해야 합니다.
새 코드에서는 키워드 인자에 대해 불평하는 것이 __init__이 아니라, 기본 구현이 어떠한 인자도 받지 않는 __init_subclass__입니다. 메서드 결정 순서를 사용하는 고전적인 상속 체계에서는 각 __init_subclass__가 남은 키워드 인자가 없을 때까지 자신의 키워드 인자를 제거할 수 있으며, 이는 __init_subclass__의 기본 구현에서 확인됩니다.
영어보다 Python을 읽는 것을 선호하는 독자를 위해, 이 PEP에서는 현재의 type과 object를 다음과 같이 대체할 것을 제안합니다.:
class NewType(type):
def __new__(cls, *args, **kwargs):
if len(args) != 3:
return super().__new__(cls, *args)
name, bases, ns = args
init = ns.get('__init_subclass__')
if isinstance(init, types.FunctionType):
ns['__init_subclass__'] = classmethod(init)
self = super().__new__(cls, name, bases, ns)
for k, v in self.__dict__.items():
func = getattr(v, '__set_name__', None)
if func is not None:
func(self, k)
super(self, self).__init_subclass__(**kwargs)
return self
def __init__(self, name, bases, ns, **kwargs):
super().__init__(name, bases, ns)
class NewObject(object):
@classmethod
def __init_subclass__(cls):
pass
참조 구현
이 PEP의 참조 구현은 issue 27366에 첨부되어 있습니다.
하위 호환성 문제
type.__new__의 정확한 호출 순서가 약간 변경되므로 하위 호환성에 대한 우려가 제기됩니다. 일반적인 사용 사례가 원하는 대로 동작하는지는 테스트를 통해 확인해야 합니다.
다음 클래스 정의는 (메타클래스를 정의하는 클래스 정의를 제외하고) 불필요한 클래스 인자가 전달되므로 TypeError로 계속 실패합니다.:
class MyMeta(type):
pass
class MyClass(metaclass=MyMeta, otherarg=1):
pass
MyMeta("MyClass", (), otherargs=1)
import types
types.new_class("MyClass", (), dict(metaclass=MyMeta, otherarg=1))
types.prepare_class("MyClass", (), dict(metaclass=MyMeta, otherarg=1))
키워드 인자에 관심이 있는 __new__ 메서드만 정의한 메타클래스는 이제 __init__ 메서드를 더 이상 정의할 필요가 없습니다. 기본 type.__init__가 키워드 인자를 무시하기 때문입니다. 이는 메타클래스에서 __init__ 대신 __new__를 재정의하라는 권고와 잘 부합합니다. 다음 코드는 더 이상 실패하지 않습니다.:
class MyMeta(type):
def __new__(cls, name, bases, namespace, otherarg):
return super().__new__(cls, name, bases, namespace)
class MyClass(metaclass=MyMeta, otherarg=1):
pass
메타클래스에 __init__ 메서드만 정의하는 경우, 키워드 인자가 주어지면 여전히 TypeError로 실패합니다.:
class MyMeta(type):
def __init__(self, name, bases, namespace, otherarg):
super().__init__(name, bases, namespace)
class MyClass(metaclass=MyMeta, otherarg=1):
pass
__init__과 __new__를 모두 정의하는 것은 여전히 잘 작동합니다.
작동하지 않게 되는 유일한 것은 type.__new__의 인자를 키워드 인자로 전달하는 것뿐입니다.:
class MyMeta(type):
def __new__(cls, name, bases, namespace):
return super().__new__(cls, name=name, bases=bases,
dict=namespace)
class MyClass(metaclass=MyMeta):
pass
이제 이는 TypeError를 발생시키지만, 이는 이상한 코드이며 누군가 이 기능을 사용했더라도 고치기 쉽습니다.
거부된 설계 옵션
클래스 자신에 대해 훅을 호출하기
클래스 자신에 대해 호출되는 __autodecorate__ 훅을 추가하는 것이 PEP 422에서 제안된 아이디어였습니다. 대부분의 예제는 훅이 진(strict) 서브클래스에 대해서만 호출되어도 동일하게, 혹은 더 낫게 작동합니다. 일반적으로, (그런 동작을 원하지 않을 때) 훅 본문에서 cls is __class를 확인하는 것을 기억함으로써 옵트아웃하는 것보다, 훅이 정의된 클래스에서 명시적으로 호출하도록 마련하여 (그런 동작을 옵트인하는 것) 훨씬 쉬우며, 이는 훅이 정의된 클래스 자체에 대해서는 호출되기를 원하지 않는다는 것을 의미합니다.
이는 해당 클래스가 믹스인으로 설계된 경우 가장 명확해집니다. 믹스인은 그 자체로 완전한 클래스가 되어서는 안 되므로, 믹스인의 코드가 믹스인 클래스 자체에 대해 실행될 가능성은 매우 낮습니다.
원래 제안은 또한 클래스 초기화 과정에 큰 변경을 가했으며, 이로 인해 해당 제안을 이전 Python 버전으로 백포트하는 것이 불가능해졌습니다.
베이스 클래스에 대해서도 훅을 호출하고자 할 때는 두 가지 메커니즘을 사용할 수 있습니다:
__init_subclass__구현을 담기 위한 추가 믹스인 클래스를 도입합니다. 그러면 원래의 “베이스” 클래스는 새 믹스인을 첫 번째 부모 클래스로 나열할 수 있습니다.- 원하는 동작을 독립적인 클래스 데코레이터로 구현하고, 해당 데코레이터를 베이스 클래스에는 명시적으로 적용한 뒤,
__init_subclass__를 통해 서브클래스에는 암묵적으로 적용합니다.
클래스 데코레이터에서 __init_subclass__를 명시적으로 호출하는 것은 일반적으로 바람직하지 않습니다. 이는 보통 부모 클래스에 대해 __init_subclass__가 두 번째로 호출되는 결과를 낳기 때문이며, 이는 원하는 동작이 아닐 가능성이 큽니다.
훅 호출의 다른 변형들
훅에 대해 __decorate__ 또는 __autodecorate__라는 다른 이름들도 제안되었습니다. 이 제안서는 __init_subclass__를 채택했는데, 이는 클래스를 반환하지 않으므로 데코레이터와는 그다지 가깝지 않은 반면, 서브클래스를 위한 것일 뿐 __init__ 메서드와는 매우 가깝기 때문입니다.
__set_name__ 훅에 대해서도 __set_owner__, __set_ownership__, __init_descriptor__와 같은 다른 이름들이 제안된 바 있습니다.
__init_subclass__에 명시적 데코레이터를 요구하기
__init_subclass__ 데코레이터에 @classmethod의 명시적 사용을 요구할 수도 있었습니다. 이를 생략하는 것에 대해 합리적인 해석이 존재하지 않으며, 유용한 오류 메시지를 제공하려면 어차피 그러한 경우를 감지해야 하기 때문에 암묵적으로 처리되도록 만들었습니다.
이 결정은 __prepare__를 정의하면서 @classmethod 메서드 데코레이터를 빠뜨렸을 때의 사용자 경험이 유난히 이해하기 어렵다는 점을 발견한 후 더욱 힘을 얻었습니다(특히 PEP 3115는 이를 일반 메서드로 문서화하고 있고, 현재 문서에서도 이에 대해 명시적으로 언급하지 않기 때문입니다).
__new__에 더 가까운 훅
관련 PEP 422에서 훅은 __init__ 메서드보다 __new__ 메서드에 더 가깝게 작동했으며, 이는 클래스를 수정하는 대신 반환했다는 뜻입니다. 이는 약간의 유연성을 더 제공하지만, 그 대가로 구현이 훨씬 어려워지고 원치 않는 부작용이 발생합니다.
속성 순서를 담은 클래스 속성 추가하기
이는 별도의 PEP 520으로 만들어졌습니다.
이력
이 문서는 한때 Alyssa Coghlan과 Daniel Urban이 작성한 PEP 422와 경쟁하는 제안서였습니다. PEP 422는 이 PEP와 동일한 목표를 달성하고자 했지만, 구현 방식이 달랐습니다. 그 사이 PEP 422는 이 방식을 지지하며 철회되었습니다.
참고 자료
Copyright
This document has been placed in the public domain.