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

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

현재 클래스 생성을 사용자 정의하려면 사용자 정의 메타클래스를 사용해야 합니다. 이 사용자 정의 메타클래스는 클래스의 전체 수명 주기 동안 유지되므로, 불필요한 메타클래스 충돌이 일어날 가능성이 생깁니다.

이 PEP는 그 대신 클래스 헤더의 새로운 namespace 매개변수와 클래스 본문의 새로운 __autodecorate__ 훅을 통해 다양한 사용자 정의 시나리오를 지원할 것을 제안합니다.

새 메커니즘은 사용자 정의 메타클래스를 구현하는 것보다 이해하고 사용하기 쉬워야 하며, 따라서 Python 메타클래스 메커니즘의 모든 기능을 접하기 위한 더 완만한 입문 과정을 제공해야 합니다.

PEP 철회

이 제안은 PEP 487에 있는 Martin Teichmann의 제안을 채택하면서 철회되었습니다. 그 제안은 더 간단하고 사용하기 쉬운 __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() 내장 함수의 인자 없는 형태입니다. 이 기능은 Python 2에서 필요했던 “이름을 통한” 참조를 대체하기 위해, 정의 중인 클래스에 대한 암시적 __class__ 참조를 사용합니다. Python 2 메타클래스 실행 중에 호출된 코드가 클래스를 이름으로 참조하는 메서드를 호출할 수 없었던 것처럼(그 이름이 아직 바깥 스코프에 바인딩되지 않았기 때문입니다), 마찬가지로 Python 3 메타클래스도 암시적 __class__ 참조에 의존하는 메서드를 호출할 수 없습니다(메타클래스가 클래스 생성 메커니즘에 제어를 반환한 이후에야 이 참조가 채워지기 때문입니다).

마지막으로, 클래스가 사용자 정의 메타클래스를 사용하면 다중 상속을 사용하는 데 추가적인 어려움이 생길 수 있습니다. 새로운 클래스는 서로 관련 없는 메타클래스를 가진 부모 클래스들을 상속할 수 없기 때문입니다. 이는 이미 공개된 클래스에 메타클래스를 추가하는 것이 불가능하다는 뜻입니다. 메타클래스 충돌의 위험 때문에 그러한 추가는 하위 호환성을 깨뜨리는 변경이 됩니다.

제안

이 PEP는 다음 기준을 충족하는, 클래스 생성을 사용자 정의하기 위한 새 메커니즘을 Python 3.4에 추가할 것을 제안합니다:

  1. 클래스 상속 구조(믹스인과 다중 상속 포함)와 잘 통합됩니다.
  2. PEP 3135에서 도입된 암묵적인 __class__ 참조 및 인자 없는 super() 문법과 잘 통합됩니다.
  3. 하위 호환성 문제를 일으킬 큰 위험 없이 기존 베이스 클래스에 추가할 수 있습니다
  4. 클래스 네임스페이스가 (네임스페이스 자체를 채우는 것 이상으로) 클래스 생성 과정에 어느 정도 영향을 미칠 수 있는 능력을 되살립니다. 다만 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__ 동적 설정의 많은 사용 사례 대체

정의된 클래스를 완전히 대체하지 않는 사용 사례라면, __metaclass__를 동적으로 설정하던 Python 2 코드는 이제 그 대신 __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__ 훅과 잘 어울리는 type의 새로운 기본 동작으로서 더 나은 선택일 수 있으며, 더 복잡한 구성 가능 네임스페이스 팩토리 아이디어는 아래에 보인 것과 같은 사용자 정의 메타클래스에 맡길 수 있습니다.

클래스를 사용하는 새로운 방법

클래스 헤더의 새로운 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 매개변수도 구현하지 않습니다.

TODO

참고 문헌