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

Python 개선 제안 한국어 번역

PEP 520 – 클래스 속성 정의 순서 보존

Author:
Eric Snow <ericsnowcurrently at gmail.com>
Status:
Final
Type:
Standards Track
Created:
07-Jun-2016
Python-Version:
3.6
Post-History:
07-Jun-2016, 11-Jun-2016, 20-Jun-2016, 24-Jun-2016
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

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

Note

3.6에서 compact dict가 도입되면서 __definition_order__는 제거되었습니다. 이제는 대신 cls.__dict__가 대체로 같은 역할을 합니다.

초록

클래스 정의 문법은 본질적으로 순서를 가집니다. 따라서 그 안에서 정의된 클래스 속성에는 순서가 있습니다. 이 순서는 가독성에 도움이 될 뿐만 아니라 때로는 중요한 의미를 가지기도 합니다. 이 순서를 클래스 정의 바깥에서 자동으로 사용할 수 있다면, 추가적인 상용구 코드(메타클래스나 속성 순서를 직접 나열하는 방식 등) 없이도 속성 순서를 활용할 수 있습니다. 이 정보가 이미 존재한다는 점을 고려하면, 속성의 정의 순서에 접근할 수 있으리라 기대하는 것은 합리적입니다. 그러나 현재 Python은 클래스 정의에서의 속성 순서를 보존하지 않습니다.

이 PEP는 클래스 정의 본문에서 속성이 도입되는 순서를 보존하여 이를 바꿉니다. 이제 그 순서는 클래스의 __definition_order__ 속성에 보존됩니다. 이를 통해 클래스 데코레이터 등에서 원래의 정의 순서를 인트로스펙션할 수 있습니다.

또한 이 PEP는 기본 클래스 정의 네임스페이스가 기본적으로 순서를 유지할 것(예: OrderedDict)을 요구합니다. 오래 유지되는 클래스 네임스페이스(__dict__)는 계속 dict로 남습니다.

동기

클래스 정의에서의 속성 순서는 이름 순서에 의존하는 도구에 유용할 수 있습니다. 그러나 정의 순서를 자동으로 사용할 수 없다면, 그러한 도구는 사용자에게 추가적인 요구 사항을 부과해야 합니다. 예를 들어, 그러한 도구를 사용하려면 여러분의 클래스가 특정 메타클래스를 사용해야 할 수 있습니다. 이러한 요구 사항만으로도 도구 사용을 단념하게 되는 경우가 많습니다.

이 PEP를 활용할 수 있는 도구로는 다음과 같은 것들이 있습니다:

  • 문서 생성기
  • 테스트 프레임워크
  • CLI 프레임워크
  • 웹 프레임워크
  • 설정 생성기
  • 데이터 직렬화기
  • enum 팩토리 (제가 처음에 이 제안을 하게 된 동기)

배경

class 문을 사용하여 클래스를 정의하면, 클래스 본문은 네임스페이스 안에서 실행됩니다. 현재 그 네임스페이스의 기본값은 dict입니다. 메타클래스가 __prepare__()를 정의하면, 이를 호출한 결과가 클래스 정의 네임스페이스로 사용됩니다.

실행이 끝나면 정의 네임스페이스는 새 dict로 복사됩니다. 그런 다음 원래의 정의 네임스페이스는 폐기됩니다. 새 복사본은 클래스의 네임스페이스로 저장되며, 읽기 전용 프락시를 통해 __dict__로 노출됩니다.

클래스 속성 정의 순서는 정의 네임스페이스에 이름이 삽입되는 순서로 표현됩니다. 따라서 정의 네임스페이스를 collections.OrderedDict 같은 순서 있는 매핑으로 바꾸면 정의 순서에 접근할 수 있습니다. 앞에서 설명한 것처럼, 이는 메타클래스와 __prepare__를 사용하여 구현할 수 있습니다. 실제로 바로 이것이 __prepare__를 사용하는 가장 흔한 사용 사례이며, 다른 사례와 비교할 수 없을 만큼 많습니다.

그 시점에서 나중에 정의 순서에 접근하기 위해 남은 일은 정의 네임스페이스가 폐기되기 전에 정의 순서를 클래스에 저장하는 것 하나뿐입니다. 이 역시 메타클래스를 사용하여 할 수 있습니다. 하지만 이렇게 하면 그러한 메타클래스를 사용하는 클래스에서만 정의 순서가 보존됩니다. 여기에는 실질적인 문제가 두 가지 있습니다:

첫째, 메타클래스를 사용해야 합니다. 메타클래스는 코드의 복잡도를 한 단계 더 높이며, 경우에 따라(예: 충돌) 문제가 되기도 합니다. 그러므로 기회가 있을 때 메타클래스의 필요성을 줄이는 것은 해 볼 만한 일입니다. PEP 422와 PEP 487에서 이 문제를 자세히 논의합니다. 기본 클래스 정의 네임스페이스에 순서 있는 매핑(예: 적어도 CPython에서는 OrderedDict)을 사용하면 바로 그런 기회를 얻을 수 있으며, __prepare__()의 필요성을 사실상 없앨 수 있습니다.

둘째, OrderedDict 기반 메타클래스를 사용하기로 선택한 클래스만 정의 순서에 접근할 수 있습니다. 이는 정의 순서에 보편적으로 접근할 수 있어야 하는 경우에 문제가 됩니다.

명세

1부:

  • 모든 클래스는 __definition_order__ 속성을 가집니다
  • __definition_order__는 식별자로 이루어진 tuple(또는 None)입니다
  • __definition_order__는 항상 설정됩니다:
    1. 클래스 본문을 실행하는 동안 클래스 정의 네임스페이스에 이름이 삽입된 순서가 튜플에 저장됩니다
    2. 클래스 본문에서 __definition_order__를 정의한다면 그 값은 식별자로 이루어진 tuple 또는 None이어야 합니다. 그 밖의 값을 사용하면 TypeError가 발생합니다
    3. 클래스 정의가 없는 클래스(예: 내장 클래스)는 __definition_order__가 None으로 설정됩니다
    4. __prepare__()가 OrderedDict(또는 그 서브클래스)가 아닌 것을 반환한 클래스는 __definition_order__가 None으로 설정됩니다(단, #2가 적용되는 경우는 제외합니다)

변경되지 않는 사항:

  • dir()은 __definition_order__에 의존하지 않습니다
  • 디스크립터와 사용자 정의 __getattribute__ 메서드는 __definition_order__와 관련하여 아무런 제약을 받지 않습니다

2부:

  • 이제 기본 클래스 정의 네임스페이스는 순서 있는 매핑(예: OrderdDict)입니다
  • cls.__dict__는 변경되지 않으며, 계속 dict를 감싸는 읽기 전용 프락시로 남습니다

순서가 유지되는 dict를 갖춘 Python 구현은 아무것도 변경할 필요가 없다는 점에 유의하십시오.

다음 코드는 1부와 2부 모두에 대해 대략적으로 동등한 의미를 보여 줍니다:

class Meta(type):
    @classmethod
    def __prepare__(cls, *args, **kwargs):
        return OrderedDict()

class Spam(metaclass=Meta):
    ham = None
    eggs = 5
    __definition_order__ = tuple(locals())

왜 튜플입니까?

튜플을 사용하는 것은 클래스의 속성이 정의된 순서를 노출한다는 사실을 반영합니다. __definition_order__가 설정되는 시점에는 정의가 이미 완료된 상태이므로, 값의 내용과 순서는 바뀌지 않습니다. 따라서 이러한 불변 상태를 드러내는 타입을 사용합니다.

왜 읽기 전용 속성으로 하지 않습니까?

__definition_order__를 (cls.__dict__처럼) 읽기 전용 속성으로 만들어야 한다는 타당한 인자가 몇 가지 있습니다. 무엇보다도 읽기 전용 속성은 해당 속성이 “완결된” 것이라는 성격을 전달하며, 이는 __definition_order__에 정확히 들어맞습니다. 이 속성은 특정한 일회성 사건(클래스 정의 본문의 실행)의 상태를 나타내므로, 값을 교체할 수 있게 하면 이 속성이 원래의 클래스 본문과 일치한다는 신뢰가 떨어집니다. 게다가 기본적으로 불변으로 두는 접근 방식은 데이터를 추론하기 쉽게 만드는 데 도움이 되는 경우가 많습니다.

그러나 이 경우에는 Python에서 오랫동안 이어져 온 선례를 거스를 강력한 이유가 여전히 없습니다. Guido의 말에 따르면:

I don't see why it needs to be a read-only attribute. There are
very few of those -- in general we let users play around with
things unless we have a hard reason to restrict assignment (e.g.
the interpreter's internal state could be compromised). I don't
see such a hard reason here.

또한 쓰기 가능한 __definition_order__를 사용하면 (예를 들어 Cython으로) 동적으로 생성된 클래스도 __definition_order__를 올바르게 설정할 수 있다는 점에 유의하십시오. 이는 분명히 type()이나 C-API 같은 특정 클래스 생성 도구를 통해 처리할 수 있으며, 그렇게 하면 읽기 전용 속성의 의미를 잃을 필요도 없습니다. 그러나 쓰기 가능한 속성이라면 이는 논의할 필요가 없는 문제입니다.

왜 “__attribute_order__”가 아닙니까?

__definition_order__는 클래스 정의 본문에 초점을 맞춥니다. 정의 이후에 클래스 네임스페이스(__dict__)를 다루는 사용 사례는 별개의 문제입니다. 클래스 정의 이상의 것에 초점을 맞춘 기능이라면 __definition_order__는 상당히 오해를 불러일으키는 이름이 될 것입니다.

왜 “던더” 이름을 무시하지 않습니까?

“__”로 시작하고 끝나는 이름은 인터프리터가 사용하도록 예약되어 있습니다. 실제로 이러한 이름은 __definition_order__ 사용자와 관련이 없을 것입니다. 오히려 거의 모든 사람에게 이러한 이름은 방해물일 뿐이며, 대다수에게 똑같은 추가 작업(던더 이름 걸러 내기)을 요구할 것입니다. 던더 이름이 중요한 경우에는 클래스 정의에서 __definition_order__를 수동으로 설정할 수도 있으므로, 일반적인 경우가 더 단순해집니다.

그러나 __definition_order__에서 던더 이름을 제외하면 정의 순서에서 그 이름들이 차지하던 위치를 복구할 수 없게 잃어버립니다. 기본적으로 던더 이름을 제외하면 던더 이름을 관례에서 벗어나게 사용하는 클래스에 의도치 않게 문제가 생길 수 있습니다. 이 경우에는 안전을 우선하여 클래스 정의의 모든 이름을 보존하는 편이 낫습니다. 던더 이름은 쉽게 걸러 낼 수 있으므로 이는 큰 문제가 아닙니다:

(name for name in cls.__definition_order__
      if not (name.startswith('__') and name.endswith('__')))

실제로 일부 애플리케이션 맥락에서는 “_”로 시작하는 이름을 모두 무시하거나, 모든 메서드를 제외하거나, 디스크립터만 포함하는 것처럼 비슷한 필터링을 적용할 다른 기준이 있을 수 있습니다. 결국 던더 이름은 예외적으로 취급할 만큼 특별한 경우가 아닙니다.

몇몇 던더 이름(__name__과 __qualname__)은 컴파일러가 기본적으로 주입한다는 점에 유의하십시오. 따라서 이 이름들은 엄밀히 말해 클래스 정의 본문의 일부가 아니더라도 포함됩니다.

왜 빈 튜플 대신 None을 사용합니까?

__definition_order__를 추가하는 핵심 목표는 이 PEP 이전에는 손실되던 클래스 정의의 정보를 보존하는 것입니다. 그 결과 중 하나로, __definition_order__는 원래의 클래스 정의가 있었음을 암시합니다. None을 사용하면 정의 순서가 없는 클래스를 명확하게 구별할 수 있습니다. 빈 튜플은 정의 문에서 만들어졌지만 그 안에서 어떤 속성도 정의하지 않은 클래스임을 명확하게 나타냅니다.

왜 속성을 설정하지 않는 대신 None을 사용합니까?

__definition_order__를 사용하는 쪽에서는 속성이 없는 경우를 None보다 더 복잡하게 처리해야 합니다.

왜 수동으로 설정한 값을 제한합니까?

클래스 본문에서 __definition_order__를 수동으로 설정하면 그 값이 사용됩니다. __definition_order__를 사용하는 쪽에서 값에 대해 일관된 기대를 가질 수 있도록, 이 값은 식별자의 튜플(또는 None)이어야 합니다. 이는 이 기능의 유용성을 극대화하는 데 도움이 됩니다.

수동으로 설정하는 __definition_order__에 임의의 이터러블을 허용하고 이를 튜플로 변환할 수도 있습니다. 그러나 모든 이터러블이 정의 순서를 내포하는 것은 아닙니다(예: set). 따라서 튜플을 요구하는 쪽을 선택합니다.

왜 타입이 아닌 객체에서 __definition_order__를 숨기지 않습니까?

Python은 클래스의 인스턴스에서 조회할 때 클래스 전용 속성을 숨기려고 크게 애쓰지 않습니다. __definition_order__를 객체에서 조회할 때는 숨겨지는 클래스 전용 속성으로 간주하는 것이 타당할 수도 있지만, 그러한 선례를 만드는 것은 이 PEP의 목표를 벗어납니다.

__slots__는 어떻습니까?

__slots__는 클래스 정의 본문의 다른 이름과 마찬가지로 __definition_order__에 추가됩니다. 실제 슬롯 이름은 정의 네임스페이스에 이름으로 설정되지 않으므로 __definition_order__에 추가되지 않습니다.

애초에 __definition_order__가 왜 필요합니까?

정의 순서는 __dict__에 보존되지 않으므로 클래스 정의 실행이 완료되면 손실됩니다. 클래스가 본문의 마지막에서 이 속성을 명시적으로 설정할 수도 있습니다. 그러나 그렇게 하면 독립적인 데코레이터는 그렇게 한 클래스만 활용할 수 있을 것입니다. 대신 __definition_order__는 클래스 본문의 이 정보 하나를 보존하여 어디서나 사용할 수 있게 합니다.

C-API 타입 지원

C로 정의된 대부분의 Python 타입(예: 내장 타입, 확장 모듈)에도 정의 순서와 대략 동등한 개념이 있다고 볼 수 있습니다. 따라서 이러한 타입에 __definition_order__를 자동으로 설정하는 것도 생각해 볼 수 있습니다. 이 PEP는 그러한 지원을 도입하지 않습니다. 그러나 이를 금지하지도 않습니다. 그러나 __definition_order__는 일반적인 속성 할당을 통해 언제든지 설정할 수 있으므로 C-API에서 특별히 처리할 필요가 없습니다.

구체적인 경우는 다음과 같습니다:

  • 내장 타입
  • PyType_Ready
  • PyType_FromSpec

호환성

이 PEP는 누군가 클래스 정의 네임스페이스로 dict에 엄격하게 의존하는 경우를 제외하면 하위 호환성을 깨뜨리지 않습니다. issubclass(OrderedDict, dict)가 참이므로 이는 문제가 되지 않을 것입니다.

변경 사항

클래스 문법 외에도 다음 항목들이 새로운 동작을 드러냅니다:

  • builtins.__build_class__ 내장 함수
  • types.prepare_class
  • types.new_class

또한 builtins.type()의 3인자 형식에서는 전달되는 네임스페이스에 __definition_order__를 포함할 수 있게 됩니다. 이는 클래스 본문에서 __definition_order__를 명시적으로 정의할 때와 동일한 제약을 받습니다.

다른 Python 구현

피드백을 더 받아 봐야 하지만, Python 구현에 미치는 영향은 미미할 것으로 예상됩니다. 모든 호환 구현은 이 PEP에 설명된 대로 __definition_order__를 설정해야 합니다.

구현

구현은 트래커에서 찾을 수 있습니다.

대안

순서를 보존하는 cls.__dict__

정의 순서를 __definition_order__에 저장하는 대신, 이제 순서가 유지되는 정의 네임스페이스를 새 OrderedDict로 복사할 수도 있습니다. 그런 다음 이것을 __dict__로 프록시되는 매핑으로 사용하게 됩니다. 이렇게 하면 대체로 동일한 의미론을 제공할 것입니다.

그러나 __dict__에 OrderedDict를 사용하면 정의 네임스페이스와의 관계가 모호해져 유용성이 떨어집니다.

또한 (특히 OrderedDict의 경우) 이렇게 하려면 구체적인 dict C-API의 의미론을 크게 변경해야 합니다.

삽입 순서를 (대체로) 보존하는 컴팩트 딕셔너리 구현으로 전환하는 것에 대한 논의가 있었습니다. 그러나 명시적인 __definition_order__가 없다는 점은 여전히 불편한 점으로 남을 것입니다.

클래스 정의를 위한 “네임스페이스” 키워드 인자

PEP 422는 클래스 정의에 새로운 “네임스페이스” 키워드 인자를 도입하여 __prepare__()의 필요성을 사실상 대체했습니다. 그러나 이 제안은 더 단순한 PEP 487이 채택되면서 철회되었습니다.

OrderedDict로 __prepare__()를 구현하는 표준 라이브러리 메타클래스

이 방식은 메타클래스를 직접 작성할 때와 똑같은 문제를 모두 안고 있습니다. 유일한 장점은 이 메타클래스를 실제로 작성하지 않아도 된다는 것입니다. 따라서 이 PEP의 맥락에서는 아무런 이점도 제공하지 않습니다.

컴파일 시점에 __definition_order__ 설정하기

각 클래스의 __qualname__은 컴파일 시점에 결정됩니다. 이와 같은 개념을 __definition_order__에도 적용할 수 있습니다. __definition_order__를 컴파일 시점에 구성한 결과는 실행 시점에 구성한 결과와 거의 같을 것입니다.

구현 난이도의 차이는 차치하더라도, 핵심적인 차이는 클래스 본문에서 동적으로 설정되는 속성(예: locals()[name] = value)의 정의 순서를 컴파일 시점에 보존하는 것이 현실적이지 않다는 점입니다. 하지만 그러한 어트리뷰트도 여전히 정의 순서에 반영되어야 합니다. 한 가지 가능한 해결책은 클래스 속성을 동적으로 정의하는 경우 클래스 작성자가 __definition_order__를 직접 설정하도록 요구하는 것입니다.

결국 실행 시점에 OrderedDict를 사용하느냐 컴파일 시점에 파악하느냐는 거의 전적으로 구현 세부 사항입니다.

참고 문헌