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

Python 개선 제안 한국어 번역

PEP 649 – 디스크립터를 사용한 어노테이션의 지연 평가

Author:
Larry Hastings <larry at hastings.org>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
11-Jan-2021
Python-Version:
3.14
Post-History:
11-Jan-2021, 12-Apr-2021, 18-Apr-2021, 09-Aug-2021, 20-Oct-2021, 20-Oct-2021, 17-Nov-2021, 15-Mar-2022, 23-Nov-2022, 07-Feb-2023, 11-Apr-2023
Replaces:
563
Resolution:
08-May-2023

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at Annotations.

×

See PEP 1 for how to propose changes.

초록

어노테이션은 Python 함수, 클래스, 모듈에 대한 타입 정보와 기타 메타데이터를 표현할 수 있게 해 주는 Python 기술입니다. 하지만 어노테이션에 대한 Python의 원래 의미는 어노테이션이 붙은 객체가 바인딩되는 시점에 어노테이션을 즉시 평가하도록 요구했습니다. 이 때문에 전방 참조 및 순환 참조 문제가 발생하여, “타입 힌트”를 사용하는 정적 타입 분석 사용자들이 만성적인 문제를 겪었습니다.

Python은 PEP 563을 수락하여 이 문제를 해결했으며, 이를 통해 Python이 어노테이션을 자동으로 문자열로 변환하는 “문자열화된 어노테이션”이라는 새로운 방식을 도입했습니다. 이를 통해 전방 참조 및 순환 참조 문제가 해결되었고, 어노테이션 메타데이터의 흥미로운 새 용도도 생겨났습니다. 그러나 이번에는 문자열화된 어노테이션이 어노테이션을 런타임에 사용하는 사용자들에게 만성적인 문제를 일으켰습니다.

이 PEP는 어노테이션을 표현하고 계산하기 위한 새롭고 포괄적인 세 번째 접근 방식을 제안합니다. 이 방식은 __annotate__라는 새 객체 메서드를 통해 필요할 때 어노테이션을 지연 계산하는 새로운 내부 메커니즘을 추가합니다. 이 접근 방식을 어노테이션 값을 다른 형식으로 강제 변환하는 새로운 기법과 결합하면, 앞서 언급한 모든 문제를 해결하고 기존의 모든 사용 사례를 지원하며, 앞으로 어노테이션 분야의 혁신도 촉진할 것입니다.

개요

이 PEP는 어노테이션을 지원하는 객체인 함수, 클래스, 모듈에 새로운 던더 속성을 추가합니다. 새 속성의 이름은 __annotate__이며, 해당 객체의 어노테이션 딕셔너리를 계산하여 반환하는 함수에 대한 참조입니다.

컴파일 시점에 객체 정의에 어노테이션이 포함되어 있으면, Python 컴파일러는 어노테이션을 계산하는 표현식을 별도의 함수로 작성합니다. 이 함수를 실행하면 어노테이션 딕셔너리를 반환합니다. 그런 다음 Python 컴파일러는 이 함수에 대한 참조를 객체의 __annotate__에 저장합니다.

또한 __annotations__는 이 어노테이션 함수를 한 번 호출하고 그 결과를 캐시하는 “데이터 디스크립터”로 재정의됩니다.

이 메커니즘은 어노테이션을 검사할 때까지 어노테이션 표현식의 평가를 지연시키며, 이를 통해 많은 순환 참조 문제를 해결합니다.

이 PEP는 또한 Python 표준 라이브러리에 있는 두 함수 inspect.get_annotations와 typing.get_type_hints에 새로운 기능을 정의합니다. 이 기능은 새로운 키워드 전용 매개변수인 format을 통해 사용합니다. format을 사용하면 사용자가 이 함수들에 특정 형식의 어노테이션을 요청할 수 있습니다. 형식 식별자는 항상 미리 정의된 정수 값입니다. 이 PEP에서 정의하는 형식은 다음과 같습니다.

  • inspect.VALUE = 1

    기본값입니다. 함수는 어노테이션에 대해 기존의 Python 값을 반환합니다. 이 형식은 Python 3.11에서 이 함수들이 반환하는 반환값과 동일합니다.

  • inspect.FORWARDREF = 2

    함수는 어노테이션에 대해 기존의 Python 값을 반환하려고 시도합니다. 그러나 정의되지 않은 이름이나 아직 값과 연결되지 않은 자유 변수를 만나면, 표현식에서 해당 값을 대신하는 프록시 객체(ForwardRef)를 동적으로 생성한 다음 평가를 계속합니다. 결과 딕셔너리에는 프록시와 실제 값이 섞여 있을 수 있습니다. 함수를 호출하는 시점에 모든 실제 값이 정의되어 있다면, inspect.FORWARDREF와 inspect.VALUE는 동일한 결과를 냅니다.

  • inspect.SOURCE = 3

    이 함수는 값을 어노테이션 표현식의 원본 소스 코드가 담긴 문자열로 바꾼 어노테이션 딕셔너리를 생성합니다. 이 문자열은 원본 소스 코드를 보존한 것이 아니라 다른 형식에서 역으로 재구성한 것일 수 있으므로 근사치에 그칠 수 있지만, 그 차이는 미미합니다.

이 PEP가 수락되면 PEP 563을 대체하게 되며, PEP 563의 동작은 폐지 예정(deprecated)으로 지정된 후 결국 제거됩니다.

어노테이션 의미 비교

Note

이 절에서 제시하는 코드는 명확성을 위해 단순화되었으며, 몇 가지 중요한 측면에서는 의도적으로 부정확하게 작성되었습니다. 이 예제는 세부 사항에 매몰되지 않고 관련된 고수준 개념을 전달하기 위한 것일 뿐입니다. 하지만 독자는 실제 구현이 몇 가지 중요한 점에서 상당히 다르다는 점에 유의해야 합니다. 이 PEP가 기술적 수준에서 제안하는 내용에 대한 훨씬 더 정확한 설명은 이 PEP 뒷부분의 Implementation 절을 참고하십시오.

다음 예제 코드를 살펴보십시오:

def foo(x: int = 3, y: MyType = None) -> float:
    ...
class MyType:
    ...
foo_y_annotation = foo.__annotations__['y']

여기서 볼 수 있듯이, 어노테이션은 런타임에 함수, 클래스, 모듈의 __annotations__ 속성을 통해 사용할 수 있습니다. 이러한 객체 중 하나에 어노테이션이 지정되면, __annotations__는 필드 이름을 해당 필드의 어노테이션으로 지정된 값에 매핑하는 딕셔너리입니다.

Python의 기본 동작은 함수, 클래스 또는 모듈이 바인딩되는 시점에 어노테이션 표현식을 평가하고 어노테이션 딕셔너리를 구성하는 것입니다. 런타임에 위 코드는 실제로 대략 다음과 같이 동작합니다:

annotations = {'x': int, 'y': MyType, 'return': float}
def foo(x = 3, y = "abc"):
    ...
foo.__annotations__ = annotations
class MyType:
    ...
foo_y_annotation = foo.__annotations__['y']

여기서 핵심적인 세부 사항은 int, MyType, float 값이 함수 객체가 바인딩되는 시점에 조회되고, 이 값들이 어노테이션 딕셔너리에 저장된다는 것입니다. 하지만 이 코드는 실행되지 않습니다. MyType이 아직 정의되지 않았기 때문에 첫 번째 줄에서 NameError가 발생합니다.

PEP 563의 해법은 컴파일 중에 표현식을 다시 문자열로 디컴파일하고, 그 문자열을 어노테이션 딕셔너리의 값으로 저장하는 것입니다. 이에 상응하는 런타임 코드는 대략 다음과 같습니다:

annotations = {'x': 'int', 'y': 'MyType', 'return': 'float'}
def foo(x = 3, y = "abc"):
    ...
foo.__annotations__ = annotations
class MyType:
    ...
foo_y_annotation = foo.__annotations__['y']

이제 이 코드는 성공적으로 실행됩니다. 그러나 foo_y_annotation은 더 이상 MyType에 대한 참조가 아니라 문자열 'MyType'입니다. 이 문자열을 실제 값 MyType으로 바꾸려면 사용자는 eval, inspect.get_annotations 또는 typing.get_type_hints를 사용하여 문자열을 평가해야 합니다.

이 PEP는 세 번째 접근 방식을 제안합니다. 어노테이션을 별도의 함수에서 계산하도록 하여 그 평가를 지연시키는 것입니다. 이 PEP가 적용된다면, 생성된 코드는 대략 다음과 같이 동작합니다:

class function:
    # __annotations__ on a function object is already a
    # "data descriptor" in Python, we're just changing
    # what it does
    @property
    def __annotations__(self):
        return self.__annotate__()

# ...

def annotate_foo():
    return {'x': int, 'y': MyType, 'return': float}
def foo(x = 3, y = "abc"):
    ...
foo.__annotate__ = annotate_foo
class MyType:
   ...
foo_y_annotation = foo.__annotations__['y']

중요한 변화는 어노테이션 딕셔너리를 구성하는 코드가 이제 함수 안에 있다는 것입니다. 여기서는 그 함수를 annotate_foo()라고 부릅니다. 하지만 이 함수는 foo.__annotations__의 값을 요청할 때까지 호출되지 않으며, 우리는 MyType을 정의한 이후에야 그 값을 요청합니다. 따라서 이 코드도 성공적으로 실행되며, MyType이 어노테이션이 정의된 이후에야 정의되었음에도 불구하고 foo_y_annotation은 이제 올바른 값, 즉 MyType 클래스를 갖게 됩니다.

2017년 11월 이 접근 방식에 대한 잘못된 기각

PEP 563에 관한 논의 초기에, comp.lang.python-dev의 2017년 11월 스레드에서 코드를 사용하여 어노테이션 평가를 지연시키는 아이디어가 잠시 논의되었습니다. 당시 이 기법은 “암묵적 람다 표현식(implicit lambda expression)”이라고 불렸습니다.

당시 Python의 BDFL이었던 Guido van Rossum은 이러한 “암묵적 람다 표현식”은 모듈 수준 스코프의 심볼만 해석할 수 있으므로 동작하지 않을 것이라고 답했습니다:

제 생각에는 메서드의 어노테이션에서 클래스 수준 정의를 참조할 수 없다는 점이 이 아이디어를 사실상 무산시킵니다.

https://mail.python.org/pipermail/python-dev/2017-November/150109.html

이를 계기로, 클래스 수준 스코프에 대한 참조를 유지하여 메서드의 람다화된 어노테이션이 클래스 수준 정의를 참조할 수 있도록 확장하자는 짧은 논의가 이어졌습니다. 이 아이디어 역시 곧바로 기각되었습니다.

PEP 563은 위 논의를 요약하고 있습니다

이 PEP가 취하는 접근 방식에는 이러한 제약이 없습니다. 어노테이션은 모듈 수준 정의와 클래스 수준 정의는 물론, 지역 변수와 자유 변수에도 접근할 수 있습니다.

동기

어노테이션의 역사

Python 3.0에는 PEP 3107에 정의된 새로운 문법 기능인 “어노테이션”이 포함되었습니다. 이를 통해 Python 함수의 매개변수나 그 함수가 반환하는 값에 연결될 Python 값을 지정할 수 있게 되었습니다. 달리 말하면, 어노테이션은 Python 사용자에게 함수 매개변수나 반환값에 대한 풍부한 메타데이터(예를 들어 타입 정보)를 제공할 수 있는 인터페이스를 마련해 주었습니다. 함수의 모든 어노테이션은 새 속성 __annotations__에, 매개변수 이름(반환 어노테이션의 경우 'return'이라는 이름을 사용)을 해당 Python 값에 매핑하는 “어노테이션 딕셔너리” 형태로 함께 저장되었습니다.

실험을 장려하기 위해 Python은 이 메타데이터가 어떤 형태를 취해야 하는지, 어떤 값을 사용해야 하는지를 의도적으로 정의하지 않았습니다. 사용자 코드는 거의 즉시 이 새로운 기능을 가지고 실험하기 시작했습니다. 하지만 이 기능을 활용하는 인기 있는 라이브러리는 더디게 등장했습니다.

수년간 별다른 진전이 없던 끝에, BDFL은 PEP 484에 정의된, 타입 힌트라고 불리는 정적 타입 정보 표현을 위한 특정 접근 방식을 선택했습니다. Python 3.5에는 새로운 typing 모듈이 포함되었고, 이 모듈은 빠르게 큰 인기를 얻었습니다.

Python 3.6은 PEP 526에서 제안된 방식을 사용하여 지역 변수, 클래스 속성, 모듈 속성에 어노테이션을 붙이는 문법을 추가했습니다. 정적 타입 분석의 인기는 계속해서 높아졌습니다.

그러나 정적 타입 분석 사용자들은 전방 참조라는 불편한 문제로 점점 더 좌절하게 되었습니다. 전통적인 Python에서는 클래스 C가 나중에 정의되는 클래스 D에 의존하더라도 일반적으로 문제가 되지 않습니다. 사용자 코드는 보통 둘 다 정의될 때까지 기다린 뒤에 어느 하나를 사용하려 하기 때문입니다. 하지만 어노테이션은 새로운 문제를 더했습니다. 어노테이션은 어노테이션이 달린 객체(함수, 클래스 또는 모듈)가 바인딩되는 시점에 계산되었기 때문입니다. 클래스 C의 메서드에 타입 D로 어노테이션이 달려 있고, 이 어노테이션 표현식이 메서드가 바인딩되는 시점에 계산된다면, D는 아직 정의되지 않았을 수 있습니다. 그리고 D의 메서드에도 타입 C로 어노테이션이 달려 있다면, 이제 해결할 수 없는 순환 참조 문제가 생깁니다.

처음에 정적 타입 사용자들은 문제가 되는 어노테이션을 문자열로 정의하는 방식으로 이 문제를 우회했습니다. 타입 힌트를 담은 문자열도 정적 타입 분석 도구에서는 똑같이 사용할 수 있었기 때문에 이 방법이 통했습니다. 또한 정적 타입 분석 도구 사용자들은 런타임에 어노테이션을 들여다보는 일이 드물었기 때문에, 이러한 표현 방식 자체가 불편을 주지는 않았습니다. 하지만 타입 힌트를 수동으로 문자열화하는 것은 번거롭고 실수하기 쉬웠습니다. 게다가 코드베이스에 추가되는 어노테이션이 점점 늘어나면서, 이를 생성하고 바인딩하는 데 드는 CPU 시간도 점점 더 늘어났습니다.

이러한 문제를 해결하기 위해 BDFL은 PEP 563을 승인했고, 이를 통해 Python 3.7에 “문자열화된 어노테이션”이라는 새로운 기능이 추가되었습니다. 이 기능은 future 임포트로 활성화되었습니다:

from __future__ import annotations

일반적으로 어노테이션 표현식은 객체가 바인딩될 때 평가되었으며, 그 값은 어노테이션 딕셔너리에 저장되었습니다. 문자열화된 어노테이션이 활성화되면 이러한 의미가 바뀌었습니다. 대신 컴파일 시점에 컴파일러가 해당 모듈의 모든 어노테이션을 소스 코드의 문자열 표현으로 변환했습니다. 즉, 사용자의 어노테이션을 자동으로 문자열로 바꾸어, 이전처럼 수동으로 문자열화할 필요를 없앴습니다. PEP 563은 런타임에 실제 값이 필요하다면 사용자가 eval로 이 문자열을 평가할 수 있다고 제안했습니다.

(이후 이 PEP에서는 객체가 바인딩되는 시점에 어노테이션 표현식의 값을 계산하는 PEP 3107과 PEP 526의 고전적인 의미를, 새로운 PEP 563 “문자열화된” 어노테이션 의미와 구별하기 위해 “기본(stock)” 의미라고 부르겠습니다.)

어노테이션 사용 사례의 현재 상태

어노테이션에는 구체적인 사용 사례가 많지만, 이 PEP를 둘러싼 논의에 참여한 어노테이션 사용자들은 대체로 다음 네 가지 범주 중 하나에 속했습니다.

정적 타이핑 사용자

정적 타이핑 사용자는 코드에 타입 정보를 추가하기 위해 어노테이션을 사용합니다. 하지만 이들은 대체로 런타임에 어노테이션을 검사하지 않습니다. 대신 정적 타입 분석 도구(mypy, pytype)를 사용하여 소스 트리를 검사하고 코드가 타입을 일관되게 사용하는지 판단합니다. 이는 거의 틀림없이 오늘날 가장 널리 쓰이는 어노테이션 사용 사례입니다.

많은 어노테이션이 PEP 484(및 이후의 여러 PEP)에서 말하는 타입 힌트를 사용합니다. 타입 힌트는 수동적인 객체로, 타입 정보를 표현할 뿐이며 실제로 아무 일도 하지 않습니다. 타입 힌트는 흔히 다른 타입이나 다른 타입 힌트로 매개변수화됩니다. 타입 힌트는 이러한 실제 값이 무엇인지와 무관하므로 ForwardRef 프록시 객체와도 잘 동작합니다. 정적 타입 힌트 사용자들은 기본 의미에서 타입 힌트를 광범위하게 사용하면 해결하기 어려운 대규모 순환 참조 및 순환 임포트 문제가 자주 발생한다는 것을 발견했습니다. PEP 563은 바로 이 문제를 해결하기 위해 설계되었으며, 이 해결책은 이러한 사용자들에게 매우 잘 작동했습니다. 이 사용자들은 런타임에 어노테이션을 검사하는 일이 매우 드물었기 때문에, 문자열화된 어노테이션을 실제 값으로 바꾸기 어렵다는 점이 대체로 불편을 주지 않았습니다.

정적 타이핑 사용자는 타입 힌트가 런타임에 로드되지 않도록 PEP 563을 if typing.TYPE_CHECKING 관용구와 함께 사용하는 경우가 많습니다. 이는 이들이 런타임에 문자열화된 어노테이션을 평가하여 실제 값을 만들어 내지 못하는 경우가 많다는 뜻입니다. 드물게 런타임에 어노테이션을 검사하는 경우에도, 이들은 eval을 사용하지 않고 문자열화된 어노테이션에 직접 어휘 분석을 적용하는 경우가 많습니다.

이 PEP에서 정적 타이핑 사용자는 아마도 FORWARDREF 또는 SOURCE 형식을 선호할 것입니다.

런타임 어노테이션 사용자

런타임 어노테이션 사용자는 함수와 클래스에 대한 풍부한 메타데이터를 표현하는 수단으로 어노테이션을 사용하며, 이를 런타임 동작의 입력으로 활용합니다. 구체적인 사용 사례로는 런타임 타입 검증(Pydantic)과 Python API를 다른 도메인에 노출하는 연결 로직(FastAPI, Typer)이 있습니다. 이 어노테이션은 타입 힌트일 수도 있고 아닐 수도 있습니다.

런타임 어노테이션 사용자는 런타임에 어노테이션을 검사하므로, 전통적으로 기본 의미가 더 적합했습니다. 이 사용 사례는 PEP 563과, 특히 if typing.TYPE_CHECKING 관용구와 대체로 호환되지 않습니다.

이 PEP에서 런타임 어노테이션 사용자는 VALUE 형식을 선호할 가능성이 가장 높지만, 일부(예: 데코레이터에서 어노테이션을 즉시 평가하면서 전방 참조를 지원하려는 경우)는 FORWARDREF 형식을 사용할 수도 있습니다.

래퍼

래퍼는 사용자 함수나 클래스를 감싸서 기능을 추가하는 함수나 클래스입니다. 그 예로는 dataclass(), functools.partial(), attrs, wrapt 등이 있습니다.

래퍼는 런타임 어노테이션 사용자 중에서도 별개의 하위 범주에 속합니다. 래퍼는 런타임에 어노테이션을 사용하기는 하지만, 감싸는 객체의 어노테이션을 실제로 검사할 수도 있고 검사하지 않을 수도 있습니다–이는 래퍼가 제공하는 기능에 따라 달라집니다. 원칙적으로 래퍼는 감싼 객체의 어노테이션을 자신이 생성하는 래퍼로 전파해야 하지만, 그 어노테이션을 수정할 수도 있습니다.

래퍼는 일반적으로 기본 의미에서 잘 동작하도록 설계되었습니다. PEP 563 의미에서 래퍼가 잘 동작하는지는 래핑된 객체의 어노테이션을 얼마나 검사하는지에 달려 있습니다. 래퍼는 값 자체에는 관심이 없고 어노테이션에 관한 특정 정보만 필요로 하는 경우가 많습니다. 그렇더라도 PEP 563과 if typing.TYPE_CHECKING 관용구 때문에 래퍼가 런타임에 필요한 정보를 확실하게 알아내기 어려울 수 있습니다. 이는 계속 이어지고 있는 만성적인 문제입니다. 이 PEP에서 래퍼는 내부 로직에 FORWARDREF 형식을 선호할 것으로 보입니다. 하지만 감싼 객체는 그 사용자를 위해 모든 형식을 지원해야 합니다.

문서

PEP 563의 문자열화된 어노테이션은 문서를 기계적으로 생성하는 도구에 큰 도움이 되었습니다.

문자열화된 타입 힌트는 훌륭한 문서가 됩니다. 소스 코드에 표현된 타입 힌트는 대개 간결하고 읽기 쉽기 때문입니다. 그러나 런타임에는 바로 이 타입 힌트가 만들어 내는 값의 repr이 장황하게 중첩되어 읽을 수 없을 만큼 뒤엉킨 모습이 될 수 있습니다. 따라서 문서화 사용자에게는 PEP 563이 잘 맞았지만 기본 의미는 잘 맞지 않았습니다.

이 PEP에서 문서화 사용자는 SOURCE 형식을 사용할 것으로 예상됩니다.

이 PEP의 동기

어노테이션에 대한 Python의 원래 의미는 전방 참조 문제로 인해 정적 타입 분석에 사용하기 어렵게 만들었습니다. PEP 563은 전방 참조 문제를 해결했고, 많은 정적 타입 분석 사용자가 이를 기꺼이 조기에 도입했습니다. 하지만 그 색다른 해결책은 앞서 언급한 사용 사례 중 두 가지, 즉 런타임 어노테이션 사용자와 래퍼에 새로운 문제를 일으켰습니다.

첫째, 문자열화된 어노테이션에서는 지역 변수나 자유 변수를 참조할 수 없었으므로, 어노테이션을 만드는 유용하고 합리적인 방법 상당수를 더는 쓸 수 없게 되었습니다. 이는 기존 함수와 클래스를 감싸는 데코레이터에 특히 불편했습니다. 이런 데코레이터는 클로저를 사용하는 경우가 많기 때문입니다.

둘째, eval이 문자열화된 어노테이션에서 전역 변수를 올바르게 조회하려면 먼저 올바른 모듈에 대한 참조를 얻어야 합니다. 하지만 클래스 객체는 자신의 전역 변수에 대한 참조를 유지하지 않습니다. PEP 563은 sys.modules에서 클래스의 모듈을 이름으로 조회하도록 제안하는데—언어 수준 기능치고는 놀라운 요구 사항입니다.

또한 복잡하지만 적법한 구성 때문에, 문자열화된 어노테이션을 제대로 평가하기 위해 eval에 넘길 올바른 전역 및 지역 딕셔너리를 결정하기 어려울 수 있습니다. 더 나쁜 것은, 어떤 상황에서는 그것이 아예 불가능할 수도 있다는 점입니다.

예를 들어 일부 라이브러리(예: typing.TypedDict, dataclasses)는 사용자 클래스를 감싼 다음, 그 클래스의 모든 베이스 클래스에 있는 어노테이션을 모두 하나의 누적 어노테이션 딕셔너리로 병합합니다. 그 어노테이션이 문자열화되어 있다면 나중에 eval을 호출해도 제대로 동작하지 않을 수 있습니다. eval에 사용되는 전역 딕셔너리는 사용자 클래스가 정의된 모듈의 것이고, 이 모듈은 어노테이션이 정의된 모듈과 다를 수 있기 때문입니다. 그러나 전방 참조 문제 때문에 어노테이션이 문자열화된 경우라면, 전방 참조를 아직 해석할 수 없으므로 일찍 eval을 호출해도 동작하지 않을 수 있습니다. 이 문제는 조율하기 어려운 것으로 드러났습니다. 아래에 링크된 세 개의 버그 보고 중 수정된 것으로 표시된 것은 하나뿐입니다.

적절한 globals 와 locals가 있더라도 문자열화된 어노테이션에 대해 eval은 신뢰할 수 없는 결과를 낼 수 있습니다. eval은 어노테이션에서 참조하는 모든 심볼이 정의되어 있어야만 성공할 수 있습니다. 문자열화된 어노테이션이 정의된 심볼과 정의되지 않은 심볼을 섞어서 참조한다면, 그 문자열을 단순히 eval하면 실패합니다. 이는 어노테이션을 검사해야 하는 라이브러리에 문제가 됩니다. 이러한 라이브러리는 문자열화된 어노테이션을 실제 값으로 신뢰성 있게 변환할 수 없기 때문입니다.

  • 일부 라이브러리(예: dataclasses)는 실제 값을 포기하고 문자열화된 어노테이션에 대해 어휘 분석을 수행하는 방식으로 이 문제를 해결했지만, 이를 올바르게 구현하려면 많은 작업이 필요합니다.
  • 다른 라이브러리들은 여전히 이 문제로 어려움을 겪고 있으며, 이로 인해 예상치 못한 런타임 동작이 발생할 수 있습니다. https://github.com/python/cpython/issues/97727

또한 eval()은 느리고 항상 사용할 수 있는 것도 아닙니다. 특정 플랫폼에서는 공간상의 이유로 제거되기도 합니다. MicroPython의 eval()은 locals 인자를 지원하지 않으므로, 런타임에 문자열화된 어노테이션을 실제 값으로 변환하기가 더욱 어렵습니다.

마지막으로, PEP 563은 Python 구현체가 어노테이션을 문자열화하도록 요구합니다. 이는 놀라운 동작입니다. 언어 수준 기능으로서는 전례가 없고, 구현이 복잡하며, 언어에 새로운 연산자가 추가될 때마다 갱신되어야 합니다.

이러한 문제들이 어노테이션 사용자가 직면한 문제를 해결할 새로운 접근 방식을 찾는 연구의 동기가 되었고, 그 결과가 이 PEP입니다.

구현

어노테이션 표현식에 대해 관찰되는 의미 체계

어노테이션을 지원하는 모든 객체 o에 대해, 어노테이션 표현식에서 평가되는 모든 이름이 o가 정의되기 전에 바인딩되고 이후에 다시 바인딩되지 않는다면, o.__annotations__는 “기본(stock)” 의미 체계가 활성화된 경우와 이 PEP가 활성화된 경우 모두에서 동일한 어노테이션 딕셔너리를 생성합니다. 특히 이름 해석은 두 경우 모두에서 동일하게 수행됩니다.

이 PEP가 활성화되면 o.__annotations__의 값은 o.__annotations__ 자체가 처음 평가될 때까지 계산되지 않습니다. 어노테이션 표현식의 모든 평가는 이 시점까지 지연되며, 이는 또한 다음을 의미합니다.

  • 어노테이션 표현식에서 참조되는 이름은 이 시점의 현재 값을 사용합니다.
  • 어노테이션 표현식을 평가하는 중에 예외가 발생하면, 그 예외는 이 시점에 발생합니다.

o.__annotations__가 처음으로 성공적으로 계산되면 이 값은 캐시되며, 이후 o.__annotations__를 요청할 때 이 값이 반환됩니다.

__annotate__ 및 __annotations__

Python은 함수, 클래스, 모듈의 세 가지 타입에 대해 어노테이션을 지원합니다. 이 PEP는 이 세 가지 타입 모두의 의미 체계를 비슷한 방식으로 수정합니다.

먼저, 이 PEP는 새로운 “던더(dunder)” 속성인 __annotate__를 추가합니다. __annotate__는 가져오기(get), 설정(set), 삭제(delete)의 세 가지 동작을 모두 구현하는 “데이터 디스크립터”여야 합니다. __annotate__ 속성은 항상 정의되어 있으며, None 또는 호출 가능 객체로만 설정할 수 있습니다. (__annotate__는 삭제할 수 없습니다.) 객체에 어노테이션이 없으면 __annotate__는 빈 딕셔너리를 반환하는 함수가 아니라 None으로 초기화되어야 합니다.

__annotate__ 데이터 디스크립터는 자신의 값에 대한 참조를 저장하기 위해 객체 내부에 전용 저장 공간을 가져야 합니다. 런타임에 이 저장 공간이 위치하는 곳은 구현 세부 사항입니다. Python 코드에서 보이더라도 이는 여전히 내부 구현 세부 사항으로 간주되어야 하며, Python 코드는 가급적 __annotate__ 속성을 통해서만 이와 상호 작용해야 합니다.

__annotate__에 저장된 호출 가능 객체는 format이라는 필수 위치 인자 하나를 받아야 하며, 이 인자는 항상 int(또는 int의 서브클래스)입니다. 이 호출 가능 객체는 딕셔너리(또는 딕셔너리의 서브클래스)를 반환하거나 NotImplementedError()를 발생시켜야 합니다.

다음은 Python 언어 레퍼런스의 “매직 메서드” 절에 실릴 __annotate__의 공식 정의입니다.

__annotate__(format: int) -> dict

속성/매개변수 이름을 해당 어노테이션 값에 매핑하는 새 딕셔너리 객체를 반환합니다.

어노테이션 값을 어떤 형식으로 제공할지 지정하는 format 매개변수를 받습니다. 다음 중 하나여야 합니다:

inspect.VALUE (int 상수 1과 동등함)

값은 어노테이션 표현식을 평가한 결과입니다.

inspect.FORWARDREF (int 상수 2와 동등함)

값은 정의된 값에 대해서는 (inspect.VALUE 형식에서와 같은) 실제 어노테이션 값이고, 정의되지 않은 값에 대해서는 ForwardRef 프록시입니다. 실제 객체가 ForwardRef 프록시 객체에 노출되거나, 그러한 객체에 대한 참조를 포함할 수 있습니다.

inspect.SOURCE (int 상수 3과 동등함)

값은 소스 코드에 나타나는 그대로의 어노테이션 텍스트 문자열입니다. 근사치일 수 있습니다. 공백이 정규화될 수 있고, 상수 값이 최적화될 수 있습니다. 이러한 문자열의 정확한 값은 향후 Python 버전에서 바뀔 수 있습니다.

__annotate__ 함수가 요청된 형식을 지원하지 않으면 NotImplementedError()를 발생시켜야 합니다. __annotate__ 함수는 항상 1 (inspect.VALUE) 형식을 지원해야 하며, format=1로 호출되었을 때 NotImplementedError()를 발생시켜서는 안 됩니다.

format=1로 호출되었을 때 __annotate__ 함수는 NameError를 발생시킬 수 있지만, 그 밖의 다른 형식을 요청하며 호출되었을 때는 NameError를 발생시켜서는 안 됩니다.

객체에 어노테이션이 전혀 없다면, __annotate__를 빈 dict를 반환하는 함수로 설정하기보다는 가급적 None으로 설정하는 것이 좋습니다(__annotate__는 삭제할 수 없습니다).

Python 컴파일러는 어노테이션이 있는 객체를 컴파일할 때 그에 맞는 annotate 함수도 동시에 컴파일합니다. 이 함수는 단일 위치 인자 inspect.VALUE로 호출되면, 해당 객체에 정의된 어노테이션 dict를 계산하여 반환합니다. Python 컴파일러와 런타임은 함께 작동하여 이 함수가 적절한 네임스페이스에 바인딩되도록 보장합니다:

  • 함수와 클래스의 경우, globals 딕셔너리는 객체가 정의된 모듈이 됩니다. 객체 자체가 모듈이면, 그 globals 딕셔너리는 모듈 자신의 dict가 됩니다.
  • 클래스의 메서드와 클래스의 경우, locals 딕셔너리는 클래스 딕셔너리가 됩니다.
  • 어노테이션이 자유 변수를 참조하면, 클로저는 자유 변수에 대한 셀을 담은 적절한 클로저 튜플이 됩니다.

둘째, 이 PEP는 기존의 __annotations__가 get, set, delete의 세 가지 동작을 모두 구현하는 “데이터 디스크립터”여야 한다고 요구합니다. 또한 __annotations__는 어노테이션 dict에 대한 참조를 캐시하는 데 사용하는 자체 내부 저장소를 가져야 합니다:

  • 클래스와 모듈 객체는 __annotations__ 키를 사용하여 어노테이션 dict를 자신의 __dict__에 캐시해야 합니다. 이는 하위 호환성을 위해 필요합니다.
  • 함수 객체의 경우, 어노테이션 dict 캐시를 위한 저장소는 구현 세부 사항입니다. 이 저장소는 함수 객체 내부에 두어 Python에서는 보이지 않게 하는 것이 바람직합니다.

이 PEP는 __annotations__와 __annotate__를 구현하는 세 가지 타입 모두에 대해, 이 둘이 어떻게 상호 작용하는지에 관한 의미론을 정의합니다. 다음 예제에서 fn은 함수를, cls는 클래스를, mod는 모듈을 나타내며, o는 이 세 가지 타입 중 어느 것이든 그 타입의 객체를 나타냅니다:

  • o.__annotations__가 평가될 때, o.__annotations__의 내부 저장소가 설정되어 있지 않고 o.__annotate__가 호출 가능 객체로 설정되어 있으면, o.__annotations__의 getter는 o.__annotate__(1)을 호출한 다음 그 결과를 내부 저장소에 캐시하고 반환합니다.
    • 여러 번 제기된 질문 하나를 명확히 짚고 넘어가겠습니다. 이 o.__annotations__ 캐시는 이 PEP에 정의된 유일한 캐싱 메커니즘입니다. 이 PEP에 정의된 캐싱 메커니즘은 이것 외에는 없습니다. Python 컴파일러가 생성하는 __annotate__ 함수는 자신이 계산하는 값을 명시적으로 어떤 것도 캐시하지 않습니다.
  • o.__annotate__를 호출 가능 객체로 설정하면 캐시된 어노테이션 딕셔너리가 무효화됩니다.
  • o.__annotate__를 None으로 설정해도 캐시된 어노테이션 딕셔너리에는 아무런 영향이 없습니다.
  • o.__annotate__를 삭제하면 TypeError가 발생합니다. __annotate__는 항상 설정되어 있어야 합니다. 이렇게 하면 어노테이션이 없는 서브클래스가 베이스 클래스 중 하나의 __annotate__ 메서드를 상속하지 않습니다.
  • o.__annotations__를 유효한 값으로 설정하면 o.__annotate__가 자동으로 None으로 설정됩니다.
    • 그 밖의 경우에 cls.__annotations__ 또는 mod.__annotations__를 None으로 설정하면 다른 속성과 똑같이 동작합니다. 즉, 해당 속성이 None으로 설정됩니다.
    • fn.__annotations__를 None으로 설정하면 캐시된 어노테이션 딕셔너리가 무효화됩니다. fn.__annotations__에 캐시된 어노테이션 값이 없고 fn.__annotate__가 None이면, fn.__annotations__ 데이터 디스크립터가 비어 있는 새 딕셔너리를 생성하고 캐시한 뒤 반환합니다. (이는 PEP 3107 의미 체계와의 하위 호환성을 유지하기 위한 것입니다.)

허용되는 어노테이션 문법의 변경 사항

이제 __annotate__는 나중에 __annotations__가 참조될 때까지 어노테이션 평가를 미룹니다. 또한 이는 어노테이션이 정의된 객체가 바인딩된 원래 컨텍스트가 아니라 새 함수 안에서 어노테이션이 평가된다는 의미이기도 합니다. 런타임 부작용이 큰 연산자가 네 가지 있습니다. 이 연산자들은 기본 의미 체계에서는 허용되었지만 from __future__ import annotations가 활성화되어 있으면 금지되며, 이 PEP가 활성화되어 있을 때에도 금지되어야 합니다:

  • :=
  • yield
  • yield from
  • await

inspect.get_annotations와 typing.get_type_hints의 변경 사항

(이 PEP는 이 두 함수를 자주 언급합니다. 이 두 함수는 사용자 코드가 어노테이션을 다루도록 돕기 때문에, 앞으로는 둘을 통틀어 “헬퍼 함수”라고 부르겠습니다.)

이 두 함수는 객체에서 어노테이션을 추출하여 반환합니다. inspect.get_annotations는 어노테이션을 변경하지 않고 그대로 반환합니다. 정적 타이핑 사용자의 편의를 위해 typing.get_type_hints는 어노테이션을 반환하기 전에 일부 수정합니다.

이 PEP는 이 두 함수에 새로운 키워드 전용 매개변수 format을 추가합니다. format은 어노테이션 딕셔너리의 값을 어떤 형식으로 반환할지 지정합니다. 이 두 함수의 format 매개변수는 앞에서 정의한 __annotate__ 매직 메서드의 format 매개변수와 같은 값을 받습니다. 다만 이 두 함수의 format 매개변수에는 inspect.VALUE라는 기본값도 있습니다.

객체에서 __annotations__나 __annotate__ 중 하나가 갱신되면 나머지 속성은 더 이상 최신 상태가 아니므로 그 속성도 갱신하거나 삭제해야 합니다(삭제할 수 없는 __annotate__의 경우에는 None으로 설정해야 합니다). 일반적으로는 앞 절에서 정한 의미 체계가 이 작업을 자동으로 처리합니다. 하지만 실질적으로 자동 처리가 불가능한 경우가 하나 있습니다. o.__annotations__가 캐시한 딕셔너리 자체가 수정되거나, 그 딕셔너리 안의 가변 값이 수정되는 경우입니다.

이 경우는 코드로 처리할 수 없으므로 문서로 안내해야 합니다. 이 PEP는 inspect.get_annotations의 문서(그리고 마찬가지로 typing.get_type_hints의 문서)를 다음과 같이 수정할 것을 제안합니다:

객체의 __annotations__ 딕셔너리를 직접 수정하면, 기본적으로 그 객체에 대해 SOURCE 또는 FORWARDREF 형식을 요청할 때 inspect.get_annotations가 반환하는 딕셔너리에 해당 변경 사항이 반영되지 않을 수 있습니다. __annotations__ 딕셔너리를 직접 수정하는 대신, 원하는 값으로 어노테이션 딕셔너리를 계산하는 함수로 해당 객체의 __annotate__ 메서드를 교체하는 것을 고려하십시오. 그렇게 할 수 없다면 객체의 __annotate__ 메서드를 None으로 덮어쓰는 것이 가장 좋습니다. 그러면 inspect.get_annotations가 SOURCE 및 FORWARDREF 형식에 대해 오래된 결과를 생성하지 않습니다.

stringizer와 fake globals 환경

처음 제안되었을 때 이 PEP는 런타임 어노테이션 사용자의 여러 사용 사례와 정적 타입 사용자의 여러 사용 사례를 지원했습니다. 그러나 이것으로는 충분하지 않았습니다. 이 PEP는 현존하는 모든 사용 사례를 충족하기 전까지는 수락될 수 없었습니다. 이는 Carl Meyer가 아래에 설명된 “stringizer”와 “fake globals” 환경을 제안하기 전까지 오랫동안 이 PEP의 걸림돌이었습니다. 이 기법들 덕분에 이 PEP는 FORWARDREF 형식과 SOURCE 형식을 모두 지원할 수 있으며, 남아 있던 모든 사용 사례를 훌륭하게 충족합니다.

요컨대, 이 기법은 Python 컴파일러가 생성한 __annotate__ 함수를 특이한 런타임 환경에서 실행하는 것입니다. 이 함수의 일반적인 globals 딕셔너리는 이른바 “fake globals” 딕셔너리로 대체됩니다. “fake globals” 딕셔너리는 한 가지 중요한 차이점이 있는 딕셔너리입니다. 매핑되지 않은 키를 이 딕셔너리에서 “가져올” 때마다 (딕셔너리의 __missing__ 콜백에 따라) 해당 키에 대한 새 값을 생성하고, 캐시하고, 반환합니다. 그 값은 “stringizer”라고 부르는 새로운 타입의 인스턴스입니다.

“stringizer”는 매우 특이한 동작을 하는 Python 클래스입니다. 모든 stringizer는 자신의 “값”으로 초기화되며, 이 값은 처음에는 “fake globals” 딕셔너리에서 누락된 키의 이름입니다. 그런 다음 stringizer는 연산자를 구현하는 데 사용되는 모든 Python “던더(dunder)” 메서드를 구현하며, 해당 메서드가 반환하는 값은 그 연산의 텍스트 표현을 값으로 가지는 새로운 stringizer입니다.

이러한 stringizer가 표현식에 사용되면, 표현식의 결과는 그 표현식을 텍스트로 나타내는 이름을 가진 새로운 stringizer입니다. 예를 들어, 값 'f'로 초기화된 stringizer를 참조하는 변수 f가 있다고 합시다. 다음은 f에 수행할 수 있는 연산과 그 연산이 반환하는 값의 몇 가지 예입니다:

>>> f
Stringizer('f')
>>> f + 3
Stringizer('f + 3')
>> f["key"]
Stringizer('f["key"]')

이 모든 것을 종합하면, Python이 생성한 __annotate__ 함수를 실행하되 그 globals를 “fake globals” 딕셔너리로 대체하면, 이 함수가 참조하는 정의되지 않은 모든 심볼은 해당 심볼을 나타내는 stringizer 프록시 객체로 대체되고, 이러한 프록시에 수행되는 모든 연산은 다시 그 표현식을 나타내는 프록시를 결과로 냅니다. 이를 통해 __annotate__는 실행을 완료하고 어노테이션 딕셔너리를 반환할 수 있으며, 그렇지 않았다면 평가할 수 없었을 이름과 표현식 전체를 stringizer 인스턴스가 대신합니다.

실제로 “stringizer” 기능은 현재 typing 모듈에 정의된 ForwardRef 객체에 구현될 것입니다. ForwardRef는 모든 stringizer 기능을 구현하도록 확장되며, 또한 자신이 담고 있는 문자열을 평가하여 실제 값을 생성하는 기능도 지원하도록 확장됩니다(참조된 모든 심볼이 정의되어 있다고 가정합니다). 이는 ForwardRef 객체가 표현식을 평가하는 데 필요한 적절한 “globals”, “locals”, 나아가 “closure” 정보에 대한 참조까지 유지한다는 것을 의미합니다.

이 기법은 inspect.get_annotations가 FORWARDREF 형식과 SOURCE 형식을 지원하는 방식의 핵심입니다. 처음에 inspect.get_annotations는 원하는 형식을 요청하면서 객체의 __annotate__ 메서드를 호출합니다. 이 호출이 NotImplementedError를 발생시키면, inspect.get_annotations는 “fake globals” 환경을 구성한 다음 객체의 __annotate__ 메서드를 호출합니다.

  • inspect.get_annotations는 비어 있는 새 “fake globals” 딕셔너리를 생성하고, 이를 객체의 __annotate__ 메서드에 바인딩하고, VALUE 형식을 요청하며 그 메서드를 호출한 다음, 결과 딕셔너리의 각 ForwardRef 객체에서 문자열 “값”을 추출하는 방식으로 SOURCE 형식을 생성합니다.
  • inspect.get_annotations는 비어 있는 새 “fake globals” 딕셔너리를 생성하고, 여기에 __annotate__ 메서드의 globals 딕셔너리의 현재 내용을 미리 채우고, “fake globals” 딕셔너리를 객체의 __annotate__ 메서드에 바인딩하고, VALUE 형식을 요청하며 그 메서드를 호출한 다음, 그 결과를 반환하는 방식으로 FORWARDREF 형식을 생성합니다.

이 기법 전체가 동작하는 이유는 컴파일러가 생성한 __annotate__ 함수가 Python 자체의 제어를 받으며, 단순하고 예측 가능하기 때문입니다. 이 함수들은 사실상 어노테이션 딕셔너리를 계산하여 반환하는 단일 return 문입니다. 어노테이션을 계산하는 데 필요한 대부분의 연산은 Python에서 던더 메서드를 사용하여 구현되고, stringizer는 관련된 모든 던더 메서드를 지원하므로, 이 접근 방식은 신뢰할 수 있고 실용적인 해결책입니다.

그러나 아무 __annotate__ 메서드에나 이 기법을 시도하는 것은 합리적이지 않습니다. 이 PEP는 서드파티 라이브러리가 자체 __annotate__ 메서드를 구현할 수 있다고 가정하며, 그러한 함수는 이 “fake globals” 환경에서 실행될 때 거의 확실하게 잘못 동작할 것입니다. 이러한 이유로 이 PEP는 코드 객체에 플래그 하나를 할당합니다. 이 플래그는 co_flags에서 사용되지 않는 비트 중 하나이며, “이 코드 객체는 ‘fake globals’ 환경에서 실행될 수 있음”을 의미합니다. 이로써 “fake globals” 환경은 엄격하게 옵트인(opt-in) 방식이 되며, Python 컴파일러가 생성한 __annotate__ 메서드만 이 플래그를 설정할 것으로 예상됩니다.

이 기법의 약점은 객체의 던더 메서드에 직접 대응되지 않는 연산자를 처리하는 데 있습니다. 이들은 모두 분기나 반복 같은 어떤 형태의 흐름 제어를 구현하는 연산자입니다:

  • 단락 평가(short-circuiting) or
  • 단락 평가하는 and
  • 삼항 연산자(if / then 연산자)
  • 제너레이터 표현식
  • 리스트 / 딕셔너리 / 집합 컴프리헨션
  • 이터러블 언패킹

일반적으로 이러한 기법은 어노테이션에 사용되지 않으므로 실제로는 문제가 되지 않습니다. 하지만 최근 Python에 추가된 TypeVarTuple은 이터러블 언패킹을 실제로 사용합니다. 관련된 던더 메서드(__iter__와 __next__)는 반복의 사용 사례를 구별할 수 없게 합니다. 어떤 사용 사례인지 올바르게 감지하려면 단순한 “가짜 전역”과 “문자열화기”만으로는 충분하지 않으며, SOURCE와 FORWARDREF 형식을 생성하도록 특별히 설계된 맞춤형 바이트코드 인터프리터가 필요합니다.

다행히 잘 동작하는 지름길이 있습니다. 문자열화기는 반복 관련 던더 메서드가 호출되면 그것이 TypeVarTuple이 수행하는 이터레이터 언패킹을 위한 것이라고 단순히 가정합니다. 이 동작은 하드코딩됩니다. 따라서 반복을 사용하는 다른 기법은 동작하지 않지만, 실제 사용 사례에서 이것이 불편을 주지는 않습니다.

마지막으로, “가짜 전역” 환경은 이에 대응하는 “가짜 지역” 딕셔너리도 구성해야 하며, FORWARDREF 형식에서는 이 딕셔너리가 관련 지역 딕셔너리로 미리 채워진다는 점에 유의하십시오. 또한 “가짜 전역” 환경은 가짜 “클로저”도 생성해야 합니다. 이는 __annotate__ 메서드가 참조하는 자유 변수의 이름으로 미리 생성된 ForwardRef 객체의 튜플입니다.

자유 변수를 참조하는 __annotate__ 메서드로부터 생성된 ForwardRef 프록시는 해당 자유 변수의 이름과 클로저 값을 지역 딕셔너리에 매핑합니다. 이렇게 하여 eval이 그 이름에 대해 올바른 값을 사용하도록 보장합니다.

컴파일러가 생성한 __annotate__ 함수

이전 절에서 언급했듯이, 컴파일러가 생성하는 __annotate__ 함수는 단순합니다. 이 함수는 주로 어노테이션 딕셔너리를 계산하여 반환하는 단일 return 문으로 이루어집니다.

하지만 inspect.get_annotations가 FORWARDREF 또는 SOURCE 형식을 요청하는 프로토콜은 먼저 __annotate__ 메서드에 해당 형식을 생성하도록 요청해야 합니다. Python 컴파일러가 생성한 __annotate__ 메서드는 이 두 형식 중 어느 것도 지원하지 않으며 NotImplementedError()를 발생시킵니다.

서드파티 __annotate__ 함수

서드파티 클래스와 함수는 해당 객체의 다운스트림 사용자가 어노테이션을 최대한 활용할 수 있도록 자체 __annotate__ 메서드를 구현해야 할 가능성이 높습니다. 특히 래퍼는 래핑된 객체가 생성한 어노테이션 딕셔너리를 변환해야 할 가능성이 높습니다. 딕셔너리에 항목을 추가하거나 제거하는 등 어떤 방식으로든 수정해야 합니다.

대부분의 경우 서드파티 코드는 기존의 어떤 업스트림 객체에 대해 inspect.get_annotations를 호출하여 자신의 __annotate__ 메서드를 구현할 것입니다. 예를 들어 래퍼는 자신에게 요청된 형식으로 래핑된 객체의 어노테이션 딕셔너리를 요청할 것입니다. 그런 다음 반환된 어노테이션 딕셔너리를 적절히 수정하여 반환할 것입니다. 이를 통해 서드파티 코드는 “가짜 전역” 기법을 이해하거나 그에 관여하지 않고도 이 기법을 활용할 수 있습니다.

PEP 649 이전과 이후의 Python 버전을 모두 지원하는 서드파티 라이브러리는 두 가지를 모두 지원하는 방법에 대해 자체적인 모범 사례를 고안해야 합니다. 한 가지 합리적인 접근 방식은 래퍼가 항상 __annotate__를 지원하도록 하는 것입니다. 그런 다음 VALUE 형식을 요청하여 이를 호출하고, 그 결과를 래퍼 객체의 __annotations__로 저장합니다. 이렇게 하면 649 이전의 Python 의미를 지원하면서도 649 이후의 의미와 상위 호환될 수 있습니다.

의사 코드

다음은 inspect.get_annotations의 고수준 의사 코드입니다.:

def get_annotations(o, format):
    if format == VALUE:
        return dict(o.__annotations__)

    if format == FORWARDREF:
        try:
            return dict(o.__annotations__)
        except NameError:
            pass

    if not hasattr(o.__annotate__):
        return {}

    c_a = o.__annotate__
    try:
        return c_a(format)
    except NotImplementedError:
        if not can_be_called_with_fake_globals(c_a):
            return {}
        c_a_with_fake_globals = make_fake_globals_version(c_a, format)
        return c_a_with_fake_globals(VALUE)

다음은 Python 컴파일러가 생성하는 __annotate__ 메서드를 Python으로 작성한다면 어떤 모습일지 보여 줍니다:

def __annotate__(self, format):
    if format != 1:
        raise NotImplementedError()
    return { ... }

다음은 서드파티 래퍼 클래스가 __annotate__를 구현하는 방법의 예입니다. 이 예에서 래퍼는 functools.partial처럼 동작하여 래핑된 호출 가능 객체의 매개변수 하나를 미리 바인딩합니다. 단순화를 위해 이 매개변수의 이름은 반드시 arg여야 합니다.:

def __annotate__(self, format):
    ann = inspect.get_annotations(self.wrapped_fn, format)
    if 'arg' in ann:
        del ann['arg']
    return ann

Python 런타임에 대한 기타 수정 사항

이 PEP는 구체적인 구현 방식을 규정하지 않으며, 이는 언어 구현 유지 관리자에게 맡깁니다. 그러나 이 PEP를 가장 잘 구현하려면 기존 Python 객체에 추가 정보를 더해야 할 수도 있으며, 이는 이 PEP의 수락으로 암묵적으로 용인됩니다.

예를 들어, 클래스의 __annotate__ 함수를 필요할 때만 지연 바인딩할 수 있도록 클래스 객체에 __globals__ 속성을 추가해야 할 수도 있습니다. 또한 클래스 안에 정의된 메서드의 __annotate__ 함수는 그 클래스에 바인딩된 이름을 올바르게 평가하기 위해 클래스의 __dict__에 대한 참조를 유지해야 할 수도 있습니다. 이 PEP의 CPython 구현에는 이 두 가지 새 속성이 모두 포함될 것으로 예상됩니다.

기존 Python 객체에 추가되는 이러한 새 정보는 모두 “dunder” 속성으로 추가해야 합니다. 이는 당연히 구현 세부 사항이기 때문입니다.

대화형 REPL 셸

이 PEP에서 정한 의미는 Python의 대화형 REPL 셸에서 코드를 실행할 때도 그대로 적용됩니다. 다만 대화형 모듈(__main__) 자체의 모듈 어노테이션은 예외입니다. 그 모듈은 결코 “완료”되지 않으므로 __annotate__ 함수를 컴파일할 수 있는 특정 시점이 없습니다.

단순성을 위해 이 경우에는 지연 평가를 하지 않습니다. REPL 셸의 모듈 수준 어노테이션은 계속해서 “기존 의미 체계(stock semantics)”와 똑같이 동작하여, 즉시 평가되고 그 결과가 __annotations__ 딕셔너리에 직접 설정됩니다.

함수 내부의 지역 변수에 대한 어노테이션

Python은 함수 안에서 지역 변수 어노테이션을 위한 문법을 지원합니다. 그러나 이러한 어노테이션은 런타임에 아무런 효과가 없으며, 컴파일 시점에 버려집니다. 따라서 기존 의미 체계 및 PEP 563과 마찬가지로, 이 PEP도 이를 지원하기 위해 따로 할 일이 없습니다.

프로토타입

이 PEP의 최초 프로토타입 구현은 다음에서 찾을 수 있습니다:

https://github.com/larryhastings/co_annotations/

이 글을 쓰는 시점에 해당 구현은 매우 오래되었습니다. Python 3.10을 기반으로 하며, 2021년 초에 작성된 이 PEP의 첫 번째 초안의 의미 체계를 구현합니다. 곧 갱신될 예정입니다.

성능 비교

이 PEP의 성능은 대체로 양호합니다. 고려할 시나리오는 네 가지입니다:

  • 어노테이션이 정의되지 않았을 때의 런타임 비용,
  • 어노테이션이 정의되었지만 참조되지 않을 때의 런타임 비용,
  • 어노테이션이 정의되고 객체로 참조될 때의 런타임 비용.
  • 어노테이션이 정의되고 문자열로 참조될 때의 런타임 비용.

이 각 시나리오를 어노테이션의 세 가지 의미 체계, 즉 기존 의미 체계, PEP 563, 이 PEP의 관점에서 모두 살펴보겠습니다.

어노테이션이 없으면 세 가지 의미 체계 모두 런타임 비용이 같으며, 그 비용은 0입니다. 어노테이션 딕셔너리가 생성되지 않으며, 이를 위한 코드도 생성되지 않습니다. 따라서 런타임에 프로세서 시간이 전혀 들지 않고 메모리도 소비하지 않습니다.

어노테이션이 정의되었지만 참조되지 않는 경우, 이 PEP를 적용한 Python의 런타임 비용은 PEP 563과 거의 같으며 기존 방식보다 개선됩니다. 구체적인 내용은 어노테이션 대상 객체에 따라 다릅니다:

  • 기존 의미에서는 어노테이션 딕셔너리가 항상 만들어지고, 어노테이션이 달린 객체의 속성으로 설정됩니다.
  • PEP 563 의미에서는 함수 객체의 경우 미리 컴파일된 상수(특별히 구성된 튜플)가 함수의 속성으로 설정됩니다. 클래스 객체와 모듈 객체의 경우에는 어노테이션 딕셔너리가 항상 만들어지고 클래스나 모듈의 속성으로 설정됩니다.
  • 이 PEP에서는 어노테이션이 달린 객체의 속성으로 단일 객체 하나가 설정됩니다. 대부분의 경우 이 객체는 상수(코드 객체)이지만, 어노테이션에 클래스 네임스페이스나 클로저가 필요한 경우에는 바인딩 시점에 구성되는 튜플이 됩니다.

어노테이션이 정의되고 객체로서 참조되기도 하는 경우, 이 PEP를 사용하는 코드는 PEP 563보다 훨씬 빨라야 하며 기존 방식과 같거나 더 빨라야 합니다. PEP 563 의미에서는 어노테이션 딕셔너리 안의 모든 값에 대해 eval()을 호출해야 하는데, 이는 엄청나게 느립니다. 또한 이 PEP의 구현은 클래스와 모듈 어노테이션에 대해 기존 의미보다 측정 가능할 만큼 더 효율적인 바이트코드를 생성합니다. 함수 어노테이션의 경우 이 PEP와 기존 의미는 속도가 거의 같아야 합니다.

이 PEP가 PEP 563보다 눈에 띄게 느린 유일한 경우는 어노테이션을 문자열로 요청할 때입니다. “이미 문자열이다”라는 점을 이기기는 어렵습니다. 그러나 문자열화된 어노테이션은 온라인 문서화 사용 사례를 위한 것이며, 이런 경우에는 성능이 핵심 요소일 가능성이 낮습니다.

메모리 사용량 역시 세 가지 의미 맥락 모두에서 세 가지 시나리오 모두 비슷해야 합니다. 첫 번째와 세 번째 시나리오에서는 모든 경우에 메모리 사용량이 거의 같을 것입니다. 두 번째 시나리오, 즉 어노테이션이 정의되었지만 참조되지 않는 경우에는 이 PEP의 의미를 사용하면 함수/클래스/모듈이 사용되지 않는 코드 객체 하나(사용되지 않는 함수 객체에 바인딩되었을 수도 있음)를 저장하게 됩니다. 다른 두 의미에서는 사용되지 않는 딕셔너리 하나나 상수 튜플 하나를 저장합니다.

하위 호환성

기존 의미와의 하위 호환성

이 PEP는 기존 의미에서의 어노테이션 동작을 거의 모두 유지합니다:

  • __annotations__ 속성에 저장되는 어노테이션 딕셔너리의 형식은 바뀌지 않습니다. 어노테이션 딕셔너리에는 PEP 563에서처럼 문자열이 아니라 실제 값이 들어 있습니다.
  • 어노테이션 딕셔너리는 변경할 수 있으며, 변경 사항은 모두 유지됩니다.
  • __annotations__ 속성은 명시적으로 설정할 수 있으며, 이렇게 설정한 유효한 값은 모두 유지됩니다.
  • __annotations__ 속성은 del 문을 사용하여 삭제할 수 있습니다.

기존 의미에서 동작하는 코드는 대부분 이 PEP가 활성화되어도 아무 수정 없이 계속 동작해야 합니다. 하지만 다음과 같은 예외가 있습니다.

첫째, 클래스 어노테이션에 접근하는 잘 알려진 관용구가 있는데, 이 관용구는 이 PEP가 활성화되어 있을 때 올바르게 동작하지 않을 수 있습니다. 클래스 어노테이션의 원래 구현에는 버그라고밖에 할 수 없는 문제가 있었습니다. 클래스가 자체 어노테이션을 하나도 정의하지 않았지만 베이스 클래스 중 하나가 어노테이션을 정의했다면, 그 클래스가 해당 어노테이션을 “상속”했습니다. 이 동작은 결코 바람직하지 않았기 때문에 사용자 코드는 우회 방법을 찾아냈습니다. cls.__annotations__를 통해 클래스의 어노테이션에 직접 접근하는 대신, cls.__dict__.get("__annotations__", {})처럼 클래스의 딕셔너리를 통해 어노테이션에 접근하는 방법입니다. 이 관용구가 동작한 이유는 클래스가 어노테이션을 __dict__에 저장했고, 이런 방식으로 접근하면 베이스 클래스를 조회하지 않았기 때문입니다. 이 기법은 CPython의 구현 세부 사항에 의존했으므로, 필요하기는 했지만 지원되는 동작이었던 적은 없습니다. 하지만 이 PEP가 활성화되어 있으면, 클래스에 어노테이션이 정의되어 있지만 아직 __annotate__를 호출해 결과를 캐시하지 않은 상태일 수 있습니다. 이 경우 이 방법을 쓰면 클래스에 어노테이션이 없다고 잘못 판단하게 됩니다. 어쨌든 이 버그는 Python 3.10에서 수정되었으므로 이 관용구는 더 이상 사용하지 말아야 합니다. 또한 Python 3.10부터는 어노테이션을 다루는 모범 사례를 정의하는 Annotations HOWTO가 있습니다. 이 지침을 따르는 코드는 이 PEP가 활성화되어 있어도 올바르게 동작합니다. 이 지침은 코드가 실행되는 파이썬 버전에 따라 클래스 객체에서 어노테이션을 가져오는 방법을 다르게 쓰도록 권장하기 때문입니다.

어노테이션의 평가를 인트로스펙션 시점까지 지연하면 언어의 의미 체계가 바뀌므로, 이 차이는 언어 내부에서 관찰할 수 있습니다. 따라서 어노테이션이 바인딩 시점에 평가되는지 접근 시점에 평가되는지에 따라 다르게 동작하는 코드를 작성하는 것이 가능합니다. 예를 들면 다음과 같습니다.

mytype = str
def foo(a:mytype): pass
mytype = int
print(foo.__annotations__['a'])

이 코드는 기존 의미 체계에서는 <class 'str'>을 출력하고, 이 PEP가 활성화되면 <class 'int'>를 출력합니다. 따라서 이는 하위 호환되지 않는 변경입니다. 그러나 이 예제는 좋지 않은 프로그래밍 스타일이므로, 이 변경은 수용할 만한 것으로 보입니다.

클래스 및 모듈 어노테이션과 관련하여, 기존 의미 체계에서는 동작하지만 이 PEP가 활성화되면 더 이상 동작하지 않는 흔하지 않은 상호 작용이 두 가지 있습니다. 이 두 가지 상호 작용은 금지되어야 합니다. 다행히 둘 다 흔하지 않으며, 어느 쪽도 좋은 관행으로 여겨지지 않습니다. 사실 이들은 Python 자체의 회귀 테스트 스위트 밖에서는 거의 볼 수 없습니다. 그 두 가지는 다음과 같습니다.

  • 어떤 종류의 흐름 제어 문 안에서든 모듈이나 클래스 속성에 어노테이션을 설정하는 코드. 현재는 if나 try 문 안에서 어노테이션과 함께 모듈 및 클래스 속성을 설정할 수 있으며, 예상한 대로 동작합니다. 이 PEP가 활성화된 상태에서는 이 동작을 지원할 수 없습니다.
  • 모듈이나 클래스 스코프에서 지역 __annotations__ 딕셔너리를 직접 참조하거나 수정하는 코드. 현재는 모듈이나 클래스 속성에 어노테이션을 설정할 때 생성된 코드가 단순히 지역 __annotations__ 딕셔너리를 만든 다음 필요에 따라 매핑을 추가합니다. 사용자 코드가 이 딕셔너리를 직접 수정할 수는 있지만, 이는 의도된 기능은 아닌 것으로 보입니다. 이 PEP가 활성화된 후에도 어느 정도는 이를 지원할 수 있겠지만, 그 의미 체계는 예상을 벗어날 가능성이 높으며 누구도 만족시키지 못할 것입니다.

이 두 가지는 모두 정적 타입 검사기에게도 골칫거리이며, 해당 도구들에서 지원되지 않는다는 점에 유의하십시오. 두 가지 모두 최소한 지원되지 않는 것으로 선언하고, 이를 사용하면 정의되지 않은 동작이 발생한다고 선언하는 것이 합리적으로 보입니다. 컴파일 타임 검사를 통해 이들을 명시적으로 금지하는 데 약간의 노력을 기울일 가치가 있을 수도 있습니다.

마지막으로, 이 PEP가 활성화된 경우 어노테이션 값에는 if / else 삼항 연산자를 사용하지 않아야 합니다. o.__annotations__에 접근하거나 헬퍼 함수에서 inspect.VALUE를 요청할 때는 올바르게 동작하겠지만, 일부 이름이 정의되어 있는 경우 inspect.FORWARDREF에서는 불리언 표현식이 올바르게 계산되지 않을 수 있으며, inspect.SOURCE에서는 훨씬 더 부정확할 것입니다.

PEP 563 의미 체계와의 하위 호환성

PEP 563은 어노테이션의 의미 체계를 변경했습니다. 그 의미 체계가 활성화되면, 어노테이션은 모듈 수준 또는 클래스 수준 스코프에서 평가된다고 가정해야 합니다. 어노테이션은 더 이상 현재 함수나 감싸는 함수의 지역 변수를 직접 참조할 수 없습니다. 이 PEP는 그 제약을 제거하며, 어노테이션은 모든 지역 변수를 참조할 수 있습니다.

PEP 563은 문자열화된 어노테이션을 “실제” 값으로 변환하기 위해 eval(또는 eval을 대신 사용해 주는 typing.get_type_hints나 inspect.get_annotations 같은 헬퍼 함수)을 사용할 것을 요구합니다. 문자열화된 어노테이션을 활성화하고 문자열을 다시 실제 값으로 변환하기 위해 eval()을 직접 호출하는 기존 코드는 단순히 eval() 호출을 제거하면 됩니다. 헬퍼 함수를 사용하는 기존 코드는 변경 없이 계속 동작하지만, 해당 함수의 사용은 선택 사항이 될 수 있습니다.

정적 타이핑 사용자에게는 실제로 실행되는 코드 없이 비활성 타입 힌트 정의만 담은 모듈이 흔히 있습니다. 이러한 모듈은 정적 타입 검사를 실행할 때만 필요하며, 런타임에는 사용되지 않습니다. 그러나 기존 의미에서는 런타임이 어노테이션을 평가하고 계산하려면 이러한 모듈을 임포트해야 합니다. 한편, 이러한 모듈은 해결하기 어렵거나 심지어 해결이 불가능한 순환 임포트 문제를 자주 일으켰습니다. PEP 563은 사용자가 두 가지 조치로 이러한 순환 임포트 문제를 해결할 수 있게 해 주었습니다. 첫째, 사용자는 자신의 모듈에서 PEP 563을 활성화했습니다. 이는 어노테이션이 상수 문자열이 되어, 어노테이션을 계산하기 위해 실제 심볼이 정의되어 있을 필요가 없다는 뜻이었습니다. 둘째, 이를 통해 사용자는 문제가 되는 모듈을 if typing.TYPE_CHECKING 블록 안에서만 임포트할 수 있었습니다. 이렇게 하면 정적 타입 검사기는 모듈과 그 안의 타입 정의를 임포트할 수 있지만, 런타임에는 이들이 임포트되지 않습니다. 여기까지는 이 PEP가 활성화되어도 이 방식이 변경 없이 동작합니다. if typing.TYPE_CHECKING은 지원되는 동작입니다.

그러나 일부 코드베이스는 if typing.TYPE_CHECKING 기법을 사용하여 어노테이션에 쓰이는 정의를 임포트하지 않는 경우에도, 실제로 런타임에 어노테이션을 검사했습니다. 이러한 코드베이스는 어노테이션 문자열을 평가하지 않은 채, 대신 동일성 검사나 문자열에 대한 간단한 어휘 분석에 의존하여 검사했습니다.

이 PEP는 이러한 기법도 지원합니다. 하지만 사용자는 자신의 코드를 이에 맞게 이식해야 합니다. 첫째, 사용자 코드는 어노테이션에 접근하기 위해 inspect.get_annotations나 typing.get_type_hints를 사용해야 합니다. 객체에서 단순히 __annotations__ 속성을 가져올 수는 없습니다. 둘째, 그 함수를 호출할 때 format에 inspect.FORWARDREF나 inspect.SOURCE 중 하나를 지정해야 합니다. 이는 모든 심볼이 정의되어 있지 않은 경우에도 헬퍼 함수가 어노테이션 딕셔너리를 성공적으로 만들어 낼 수 있다는 뜻입니다. 문자열화된 어노테이션을 기대하는 코드는 inspect.SOURCE 형식의 어노테이션 딕셔너리에서도 수정 없이 동작할 것입니다. 하지만 inspect.FORWARDREF를 사용하면 분석이 더 쉬워질 수 있으므로, 사용자는 이것으로 전환하는 것을 고려해야 합니다.

마찬가지로, PEP 563은 어노테이션이 달린 클래스에 이전에는 불가능했던 방식으로 클래스 데코레이터를 사용할 수 있게 해 주었습니다. 일부 클래스 데코레이터(예: dataclasses)는 클래스의 어노테이션을 검사합니다. @ 데코레이터 문법을 사용하는 클래스 데코레이터는 클래스 이름이 바인딩되기 전에 실행되므로, 해결할 수 없는 순환 정의 문제를 일으킬 수 있습니다. 클래스의 속성에 클래스 자신을 참조하는 어노테이션을 달거나, 여러 클래스의 속성에 서로를 순환 참조하는 어노테이션을 달면, 어노테이션을 검사하는 데코레이터로 해당 클래스를 @ 데코레이터 문법으로 장식할 수 없습니다. PEP 563은 데코레이터가 문자열을 어휘적으로 검사하고 eval로 평가하지 않는 한(또는 추가적인 우회 방법으로 NameError를 처리하는 한) 이것이 동작하도록 해 주었습니다. 이 PEP가 활성화되면, 데코레이터는 헬퍼 함수를 사용하여 inspect.SOURCE 또는 inspect.FORWARDREF 형식으로 어노테이션 딕셔너리를 계산할 수 있습니다. 이를 통해 데코레이터는 정의되지 않은 심볼을 포함하는 어노테이션을 원하는 형식으로 분석할 수 있습니다.

PEP 563을 일찍 도입한 사용자들은 “문자열화된” 어노테이션이 자동 생성 문서에 유용하다는 것을 발견했습니다. 사용자들은 이 사용 사례를 실험했으며, Python의 pydoc은 이 기법에 어느 정도 관심을 보였습니다. 이 PEP는 이 사용 사례를 지원합니다. 문서를 생성하는 코드는 헬퍼 함수를 사용하여 inspect.SOURCE 형식으로 어노테이션에 접근하도록 수정되어야 합니다.

마지막으로, 어노테이션에서 if / else 삼항 연산자를 사용하는 것에 대한 경고는 PEP 563 사용자에게도 똑같이 적용됩니다. 이 방식은 현재 그들에게는 동작하지만, 헬퍼 함수에 일부 형식을 요청할 때 잘못된 결과를 만들어 낼 수 있습니다.

이 PEP가 수락되면, PEP 563은 폐지 예정이 되고 결국 제거될 것입니다. 현재 그 의미에 의존하고 있는 PEP 563 초기 도입자들의 이러한 전환을 돕기 위해, inspect.get_annotations와 typing.get_type_hints는 특별한 편의 기능을 구현할 것입니다.

이 PEP가 수락되더라도 Python 컴파일러는 PEP 563 의미가 활성화된 모듈에서 정의된 객체에 대해서는 어노테이션 코드 객체를 생성하지 않습니다. 따라서 일반적인 상황에서는 헬퍼 함수에 inspect.SOURCE 형식을 요청하면 빈 딕셔너리가 반환됩니다. 전환을 쉽게 하기 위한 편의 기능으로, 헬퍼 함수는 객체가 PEP 563이 활성화된 모듈에서 정의되었음을 감지하고 사용자가 inspect.SOURCE 형식을 요청한 경우 __annotations__ 딕셔너리의 현재 값을 반환합니다. 이 경우 그 값은 문자열화된 어노테이션입니다. 이를 통해 문자열화된 어노테이션을 어휘 분석하는 PEP 563 사용자는 헬퍼 함수에 inspect.SOURCE 형식을 요청하는 방식으로 곧바로 옮겨 갈 수 있으며, 이것이 PEP 563에서 벗어나는 전환을 원활하게 해 주기를 기대합니다.

기각된 아이디어

“그냥 문자열을 저장하기”

SOURCE 형식을 지원하기 위해 제안된 아이디어 하나는 Python 컴파일러가 어노테이션 값의 실제 소스 코드를 어딘가에 내보내고, 사용자가 SOURCE 형식을 요청하면 그것을 제공하는 것이었습니다.

이 아이디어는 기각되었다기보다는 “아직은 아님”으로 분류되었습니다. FORWARDREF 형식을 지원해야 한다는 것은 이미 알고 있으며, 그 기법은 단 몇 줄만으로 SOURCE 형식을 지원하도록 응용할 수 있습니다. 이 접근 방식에는 아직 답이 나오지 않은 질문이 많습니다:

  • 문자열을 어디에 저장합니까? 어노테이션이 달린 객체가 생성될 때 항상 로드합니까, 아니면 필요할 때 지연 로드합니까? 지연 로드한다면 그 지연 로드는 어떻게 동작합니까?
  • “소스 코드”에 원본의 줄바꿈과 주석이 포함됩니까? 들여쓰기와 순전히 서식을 위해 사용된 추가 공백을 포함하여 모든 공백을 보존합니까?

SOURCE 값이 원본 소스 코드를 충실하게 재현하는 정도를 높이는 일이 충분히 중요하다고 판단된다면, 향후 이 주제를 다시 검토할 수도 있습니다.

감사의 글

지속적인 피드백과 격려를 보내 주신 Carl Meyer, Barry Warsaw, Eric V. Smith, Mark Shannon, Jelle Zijlstra, Guido van Rossum에게 감사드립니다.

특히 이 제안의 가장 좋은 측면 중 일부가 된 핵심 아이디어를 내 주신 몇몇 분께 감사드립니다:

  • Carl Meyer는 FORWARDREF 형식과 SOURCE 형식을 가능하게 한 “문자열화기(stringizer)” 기법을 제안했으며, 이 덕분에 해결할 수 없어 보이는 문제들로 1년 동안 지지부진하던 이 PEP가 다시 진전할 수 있었습니다. 그는 또한 inspect.SOURCE가 문자열화된 어노테이션을 반환하도록 하는 PEP 563 사용자를 위한 편의 기능을 비롯해 그 밖에도 많은 것을 제안했습니다. Carl은 이 PEP를 논의한 비공개 이메일 스레드의 주된 대화 상대이기도 했으며, 지칠 줄 모르는 조력자이자 이성의 목소리였습니다. Carl의 기여가 없었다면 이 PEP는 거의 확실히 수락되지 못했을 것입니다.
  • Mark Shannon은 전체 어노테이션 딕셔너리를 단일 코드 객체 안에서 구축하고, 필요할 때만 함수에 바인딩하자고 제안했습니다.
  • Guido van Rossum은 __annotate__ 함수가 “기존” 의미에서의 어노테이션 이름 가시성 규칙을 그대로 따라야 한다고 제안했습니다.
  • Jelle Zijlstra는 피드백뿐만 아니라–코드까지 기여했습니다!

참고 문헌