[Accepted] PEP 649 - Deferred Evaluation Of Annotations Using Descriptors
원문 링크: PEP 649 - Deferred Evaluation Of Annotations Using Descriptors
상태: Accepted 유형: Standards Track 작성일: 11-Jan-2021
PEP 649 – Descriptors를 사용한 Annotation 지연 평가 (Deferred Evaluation Of Annotations Using Descriptors)
요약 (Abstract)
Annotation은 Python 함수, 클래스, 모듈에 대한 타입 정보 및 기타 메타데이터를 표현하는 Python 기술입니다. 하지만 Python의 초기 Annotation 의미론(semantics)은 Annotation이 주석이 달린 객체가 바인딩될 때 즉시 평가(eagerly evaluated)되어야 한다고 요구했습니다. 이는 전방 참조(forward-reference) 및 순환 참조(circular-reference) 문제로 인해 정적 타입 분석(static type analysis) 사용자에게 만성적인 문제를 야기했습니다.
Python은 이 문제를 해결하기 위해 PEP 563을 수용하여 Annotation이 Python에 의해 자동으로 문자열로 변환되는 “stringized annotations”라는 새로운 접근 방식을 도입했습니다. 이는 전방 참조 및 순환 참조 문제를 해결했으며, Annotation 메타데이터에 대한 흥미로운 새로운 사용 사례를 만들었습니다. 그러나 stringized annotations는 Annotation의 런타임 사용자에게 만성적인 문제를 야기했습니다.
이 PEP는 Annotation을 표현하고 계산하기 위한 새롭고 포괄적인 세 번째 접근 방식을 제안합니다. 이는 __annotate__
라는 새로운 객체 메서드를 통해 Annotation을 필요에 따라 지연 계산(lazily computing)하기 위한 새로운 내부 메커니즘을 추가합니다. 이 접근 방식은 Annotation 값을 대체 형식으로 강제 변환(coercing)하는 새로운 기술과 결합하여 위에서 언급한 모든 문제를 해결하고, 모든 기존 사용 사례를 지원하며, 향후 Annotation 혁신을 촉진할 것입니다.
개요 (Overview)
이 PEP는 Annotation을 지원하는 객체(함수, 클래스, 모듈)에 새로운 “dunder” 속성인 __annotate__
를 추가합니다. __annotate__
는 해당 객체의 Annotation 딕셔너리를 계산하고 반환하는 함수에 대한 참조입니다.
컴파일 시, 객체 정의에 Annotation이 포함되면 Python 컴파일러는 Annotation을 계산하는 표현식을 자체 함수로 작성합니다. 실행되면 이 함수는 Annotation 딕셔너리를 반환합니다. Python 컴파일러는 이 함수의 참조를 객체의 __annotate__
에 저장합니다.
또한, __annotations__
는 이 Annotation 함수를 한 번 호출하고 결과를 캐시하는 “데이터 디스크립터(data descriptor)”로 재정의됩니다. 이 메커니즘은 Annotation 표현식의 평가를 Annotation이 검사될 때까지 지연시켜 많은 순환 참조 문제를 해결합니다.
이 PEP는 또한 Python 표준 라이브러리의 두 함수 inspect.get_annotations
및 typing.get_type_hints
에 대한 새로운 기능을 정의합니다. 이 기능은 새로운 키워드 전용(keyword-only) 매개변수 format
을 통해 액세스됩니다. format
은 사용자가 특정 형식으로 Annotation을 요청할 수 있도록 합니다. 형식 식별자(format identifiers)는 항상 미리 정의된 정수 값입니다. 이 PEP에 의해 정의된 형식은 다음과 같습니다:
inspect.VALUE = 1
: 기본값입니다. 함수는 Annotation에 대한 일반적인 Python 값을 반환합니다. 이 형식은 Python 3.11에서 이 함수들의 반환 값과 동일합니다.inspect.FORWARDREF = 2
: 함수는 Annotation에 대한 일반적인 Python 값을 반환하려고 시도합니다. 그러나 정의되지 않은 이름이나 아직 값과 연결되지 않은 자유 변수(free variable)를 만나면, 표현식에서 해당 값을 대체하는 프록시 객체(ForwardRef
)를 동적으로 생성한 다음 평가를 계속합니다. 결과 딕셔너리에는 프록시와 실제 값의 혼합이 포함될 수 있습니다. 함수가 호출될 때 모든 실제 값이 정의된 경우,inspect.FORWARDREF
와inspect.VALUE
는 동일한 결과를 생성합니다.inspect.SOURCE = 3
: 함수는 값이 Annotation 표현식의 원래 소스 코드를 포함하는 문자열로 대체된 Annotation 딕셔너리를 생성합니다. 이 문자열은 원본 소스 코드를 보존하기보다는 다른 형식에서 역설계될 수 있으므로 대략적일 수 있지만, 차이는 미미할 것입니다.
이 PEP가 채택되면 PEP 563을 대체하며, PEP 563의 동작은 더 이상 사용되지 않고 결국 제거될 것입니다.
Annotation 의미론 비교 (Comparison Of Annotation Semantics)
다음 예제 코드를 고려해봅시다.
def foo(x: int = 3, y: MyType = None) -> float: ...
class MyType: ...
foo_y_annotation = foo.__annotations__['y']
Annotation은 함수, 클래스, 모듈의 __annotations__
속성을 통해 런타임에 사용할 수 있습니다. Annotation이 이러한 객체 중 하나에 지정되면, __annotations__
는 필드 이름을 해당 필드의 Annotation 값에 매핑하는 딕셔너리입니다.
Python의 기본 동작은 함수, 클래스 또는 모듈이 바인딩될 때 Annotation에 대한 표현식을 평가하고 Annotation 딕셔너리를 구축하는 것입니다. 위의 코드는 MyType
이 아직 정의되지 않았기 때문에 NameError
를 발생시킵니다.
PEP 563의 해결책은 컴파일 중에 표현식을 문자열로 역컴파일하고 해당 문자열을 Annotation 딕셔너리의 값으로 저장하는 것입니다. 이 코드는 성공적으로 실행되지만, foo_y_annotation
은 더 이상 MyType
에 대한 참조가 아니라 문자열 'MyType'
이 됩니다. 문자열을 실제 값 MyType
으로 바꾸려면 사용자는 eval
, inspect.get_annotations
또는 typing.get_type_hints
를 사용하여 문자열을 평가해야 합니다.
이 PEP는 세 번째 접근 방식을 제안하며, Annotation을 자체 함수에서 계산하여 평가를 지연시킵니다. 이 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']
핵심적인 변경 사항은 Annotation 딕셔너리를 구성하는 코드가 이제 annotate_foo()
라는 함수 안에 있다는 것입니다. 이 함수는 foo.__annotations__
의 값을 요청하기 전까지는 호출되지 않으며, MyType
이 정의된 후까지는 그렇게 하지 않습니다. 따라서 이 코드도 성공적으로 실행되며, MyType
이 Annotation이 정의된 후까지 정의되지 않았더라도 foo_y_annotation
은 이제 올바른 값(클래스 MyType
)을 가집니다.
2017년 11월 이 접근 방식의 잘못된 거부 (Mistaken Rejection Of This Approach In November 2017)
PEP 563에 대한 초기 논의 중에 Annotation 평가를 지연하기 위해 코드를 사용하는 아이디어가 잠시 논의되었습니다. 당시 이 기술은 “암시적 람다 표현식(implicit lambda expression)”이라고 불렸습니다. Python의 BDFL이었던 Guido van Rossum은 이러한 “암시적 람다 표현식”이 모듈 수준 스코프에서만 심볼을 해결할 수 있기 때문에 작동하지 않을 것이라고 주장하며 답변했습니다.
그러나 이 PEP에서 채택된 접근 방식은 이러한 제한 사항에 시달리지 않습니다. Annotation은 모듈 수준 정의, 클래스 수준 정의, 심지어 지역 변수(local variables) 및 자유 변수(free variables)에도 액세스할 수 있습니다.
동기 (Motivation)
Annotation의 역사 (A History Of Annotations)
Python 3.0은 PEP 3107에 정의된 “annotations”라는 새로운 구문 기능과 함께 출시되었습니다. 이는 Python 함수의 매개변수 또는 해당 함수가 반환하는 값과 연결될 Python 값을 지정할 수 있도록 했습니다. 즉, Annotation은 Python 사용자에게 함수 매개변수 또는 반환 값에 대한 풍부한 메타데이터(예: 타입 정보)를 제공하는 인터페이스를 제공했습니다. 함수의 모든 Annotation은 새로운 속성 __annotations__
에 함께 저장되었으며, 이는 매개변수 이름(또는 반환 Annotation의 경우 'return'
)을 Python 값에 매핑하는 “annotation dict”에 저장되었습니다.
실험을 장려하기 위해 Python은 의도적으로 이 메타데이터가 어떤 형태를 취해야 하는지 또는 어떤 값이 사용되어야 하는지를 정의하지 않았습니다. 사용자 코드는 이 새로운 기능을 거의 즉시 실험하기 시작했지만, 이 기능을 활용하는 인기 있는 라이브러리는 천천히 등장했습니다.
몇 년간의 진전이 거의 없자, BDFL은 PEP 484에 정의된 “타입 힌트(type hints)”라는 정적 타입 정보를 표현하는 특정 접근 방식을 선택했습니다. Python 3.5는 빠르게 매우 인기를 얻은 새로운 typing
모듈과 함께 출시되었습니다.
Python 3.6은 PEP 526에서 제안된 접근 방식을 사용하여 지역 변수, 클래스 속성 및 모듈 속성을 Annotation하는 구문을 추가했습니다. 정적 타입 분석은 계속해서 인기를 얻었습니다.
그러나 정적 타입 분석 사용자들은 불편한 문제인 전방 참조로 인해 점점 더 좌절했습니다. 기존 Python에서 클래스 C가 나중에 정의된 클래스 D에 의존하는 경우, 일반적으로 문제가 되지 않습니다. 왜냐하면 사용자 코드는 일반적으로 둘 다 정의될 때까지 기다렸다가 사용하려고 하기 때문입니다. 그러나 Annotation은 새로운 복잡성을 추가했습니다. 왜냐하면 Annotation이 달린 객체(함수, 클래스 또는 모듈)가 바인딩될 때 계산되기 때문입니다. 클래스 C의 메서드가 타입 D로 Annotation되고, 이러한 Annotation 표현식이 메서드가 바인딩될 때 계산되면 D가 아직 정의되지 않았을 수 있습니다. 그리고 D의 메서드도 타입 C로 Annotation되면 해결할 수 없는 순환 참조 문제가 발생합니다.
처음에 정적 타입 사용자들은 문제가 있는 Annotation을 문자열로 정의하여 이 문제를 해결했습니다. 이는 타입 힌트를 포함하는 문자열이 정적 타입 분석 도구에 똑같이 유용했기 때문에 작동했습니다. 그리고 정적 타입 분석 도구 사용자는 런타임에 Annotation을 거의 검사하지 않았으므로 이 표현 자체가 불편함은 아니었습니다. 그러나 타입 힌트를 수동으로 문자열화하는 것은 번거롭고 오류가 발생하기 쉬웠습니다. 또한, 코드베이스는 점점 더 많은 Annotation을 추가했고, 이는 생성 및 바인딩에 더 많은 CPU 시간을 소비했습니다.
이러한 문제를 해결하기 위해 BDFL은 PEP 563을 수용하여 Python 3.7에 “stringized annotations”라는 새로운 기능을 추가했습니다. 이는 from __future__ import annotations
를 사용하여 활성화되었습니다.
일반적으로 Annotation 표현식은 객체가 바인딩될 때 평가되었고, 그 값은 Annotation 딕셔너리에 저장되었습니다. stringized annotations가 활성화되면 이러한 의미론이 변경되었습니다. 대신 컴파일 시 컴파일러는 해당 모듈의 모든 Annotation을 소스 코드의 문자열 표현으로 변환했습니다. 따라서 사용자의 Annotation을 자동으로 문자열로 변환하여 이전에 수동으로 문자열화할 필요가 없게 했습니다. PEP 563은 런타임에 실제 값이 필요한 경우 사용자가 eval
로 이 문자열을 평가할 수 있다고 제안했습니다.
(여기서부터 이 PEP는 Annotation 표현식의 값이 객체가 바인딩될 때 계산되는 PEP 3107 및 PEP 526의 고전적인 의미론을 “stock” 의미론이라고 부르며, 새로운 PEP 563 “stringized” Annotation 의미론과 구별합니다.)
Annotation 사용 사례의 현재 상태 (The Current State Of Annotation Use Cases)
Annotation에는 많은 특정 사용 사례가 있지만, 이 PEP에 대한 논의에서 Annotation 사용자는 다음 네 가지 범주 중 하나에 속하는 경향이 있었습니다.
-
정적 타이핑 사용자 (Static typing users): 정적 타이핑 사용자는 코드에 타입 정보를 추가하기 위해 Annotation을 사용합니다. 그러나 그들은 런타임에 Annotation을 거의 검사하지 않습니다. 대신, 그들은 정적 타입 분석 도구(mypy, pytype)를 사용하여 소스 트리를 검사하고 코드가 타입을 일관되게 사용하는지 여부를 결정합니다. 이것은 오늘날 Annotation에 대한 가장 인기 있는 사용 사례일 것입니다. 이 PEP에서 정적 타이핑 사용자는 아마도
FORWARDREF
또는SOURCE
형식을 선호할 것입니다. -
런타임 Annotation 사용자 (Runtime annotation users): 런타임 Annotation 사용자는 함수 및 클래스에 대한 풍부한 메타데이터를 표현하는 수단으로 Annotation을 사용하며, 이를 런타임 동작의 입력으로 사용합니다. 특정 사용 사례에는 런타임 타입 검증(Pydantic) 및 다른 도메인에서 Python API를 노출하기 위한 glue logic(FastAPI, Typer)이 포함됩니다. Annotation은 타입 힌트일 수도 있고 아닐 수도 있습니다. 이 PEP에서 런타임 Annotation 사용자는 아마도
VALUE
형식을 선호할 것이며, 일부는FORWARDREF
형식을 사용할 수도 있습니다. -
래퍼 (Wrappers): 래퍼는 사용자 함수 또는 클래스를 래핑하고 기능을 추가하는 함수 또는 클래스입니다. 예로는
dataclass()
,functools.partial()
,attrs
,wrapt
등이 있습니다. 이 PEP에서 래퍼는 내부 로직에FORWARDREF
형식을 선호할 것입니다. -
문서화 (Documentation): PEP 563 stringized annotations는 문서를 기계적으로 구성하는 도구에 큰 도움이 되었습니다. stringized 타입 힌트는 훌륭한 문서를 만듭니다. 이 PEP에서 문서화 사용자는
SOURCE
형식을 사용할 것으로 예상됩니다.
이 PEP의 동기 (Motivation For This PEP)
Python의 원래 Annotation 의미론은 전방 참조 문제로 인해 정적 타입 분석에 사용하기 어려웠습니다. PEP 563은 전방 참조 문제를 해결했으며, 많은 정적 타입 분석 사용자들이 이를 일찍 채택했습니다. 그러나 그 비전통적인 해결책은 위에서 언급된 두 가지 사용 사례(런타임 Annotation 사용자 및 래퍼)에 새로운 문제를 야기했습니다.
첫째, stringized annotations는 지역 변수나 자유 변수를 참조하는 것을 허용하지 않았으며, 이는 Annotation을 생성하는 많은 유용하고 합리적인 접근 방식이 더 이상 실현 가능하지 않다는 것을 의미했습니다. 특히 기존 함수와 클래스를 래핑하는 데코레이터(decorator)의 경우 불편했는데, 이러한 데코레이터는 종종 클로저(closure)를 사용하기 때문입니다.
둘째, eval
이 stringized annotation에서 전역 변수를 올바르게 찾으려면 먼저 올바른 모듈에 대한 참조를 얻어야 합니다. 그러나 클래스 객체는 전역 변수에 대한 참조를 유지하지 않습니다. PEP 563은 언어 수준 기능에 대한 놀라운 요구 사항인 sys.modules
에서 이름으로 클래스의 모듈을 찾아볼 것을 제안합니다.
또한, 복잡하지만 합법적인 구성은 stringized annotation을 올바르게 평가하기 위해 eval
에 제공할 올바른 전역 및 지역 딕셔너리를 결정하기 어렵게 만들 수 있습니다. 심지어 어떤 상황에서는 단순히 불가능할 수도 있습니다.
eval()
은 느리고 항상 사용할 수 있는 것은 아닙니다. 일부 플랫폼에서는 공간상의 이유로 제거되기도 합니다. MicroPython의 eval()
은 locals
인수를 지원하지 않아 런타임에 stringized annotations를 실제 값으로 변환하는 것을 더욱 어렵게 만듭니다.
마지막으로, PEP 563은 Python 구현이 Annotation을 문자열화하도록 요구합니다. 이것은 언어 수준 기능에 대해 전례 없는, 복잡한 구현을 가진 놀라운 동작이며, 언어에 새로운 연산자가 추가될 때마다 업데이트되어야 합니다.
이러한 문제들은 Annotation 사용자가 직면한 문제를 해결하기 위한 새로운 접근 방식을 찾는 연구를 촉진했으며, 그 결과 이 PEP가 나오게 되었습니다.
구현 (Implementation)
Annotation 표현식에 대한 관찰된 의미론 (Observed semantics for annotations expressions)
Annotation을 지원하는 모든 객체 o
에 대해, Annotation 표현식에서 평가되는 모든 이름이 o
가 정의되기 전에 바인딩되고 나중에 재바인딩되지 않는 한, “stock” 의미론이 활성화될 때와 이 PEP가 활성화될 때 o.__annotations__
는 동일한 Annotation 딕셔너리를 생성할 것입니다. 특히, 이름 해결은 두 시나리오에서 동일하게 수행됩니다.
이 PEP가 활성화되면 o.__annotations__
의 값은 o.__annotations__
자체를 처음 평가할 때까지 계산되지 않습니다. Annotation 표현식의 모든 평가는 이 시점까지 지연되며, 이는 다음을 의미합니다.
- Annotation 표현식에서 참조되는 이름은 이 시점의 현재 값을 사용합니다.
- Annotation 표현식을 평가하는 동안 예외가 발생하면 해당 예외는 이 시점에 발생합니다.
o.__annotations__
가 처음으로 성공적으로 계산되면 이 값은 캐시되며, 이후의 o.__annotations__
요청에 의해 반환됩니다.
__annotate__
와 __annotations__
Python은 함수, 클래스, 모듈의 세 가지 다른 타입에 Annotation을 지원합니다. 이 PEP는 이 세 가지 타입 모두에 대한 의미론을 유사한 방식으로 수정합니다.
첫째, 이 PEP는 새로운 “dunder” 속성인 __annotate__
를 추가합니다. __annotate__
는 get, set, delete의 세 가지 작업을 모두 구현하는 “데이터 디스크립터”여야 합니다. __annotate__
속성은 항상 정의되며, None
또는 호출 가능(callable)으로만 설정할 수 있습니다. ( __annotate__
는 삭제할 수 없습니다.) 객체에 Annotation이 없으면 __annotate__
는 빈 딕셔너리를 반환하는 함수 대신 None
으로 초기화되어야 합니다.
__annotate__
데이터 디스크립터는 값을 저장하기 위한 전용 저장소를 객체 내부에 가져야 합니다. 이 저장소의 런타임 위치는 구현 세부 사항입니다. Python 코드에 표시되더라도 내부 구현 세부 사항으로 간주되어야 하며, Python 코드는 __annotate__
속성을 통해서만 상호 작용하는 것을 선호해야 합니다.
__annotate__
에 저장된 호출 가능은 format
이라는 단일 필수 위치 인수를 허용해야 하며, 이는 항상 int
(또는 int
의 서브클래스)여야 합니다. 이는 딕셔너리 (또는 딕셔너리의 서브클래스)를 반환하거나 NotImplementedError()
를 발생시켜야 합니다.
다음은 Python 언어 참조의 “Magic methods” 섹션에 나타날 __annotate__
의 공식 정의입니다.
__annotate__(format: int) -> dict
속성/매개변수 이름을 해당 Annotation 값에 매핑하는 새 딕셔너리 객체를 반환합니다.
Annotation 값이 제공되어야 하는 형식을 지정하는 format
매개변수를 취합니다. 다음 중 하나여야 합니다.
inspect.VALUE
(정수 상수1
과 동일): 값은 Annotation 표현식 평가의 결과입니다.inspect.FORWARDREF
(정수 상수2
와 동일): 값은 정의된 값에 대한 실제 Annotation 값(inspect.VALUE
형식에 따름)이고, 정의되지 않은 값에 대한ForwardRef
프록시입니다. 실제 객체는ForwardRef
프록시 객체에 노출되거나 참조를 포함할 수 있습니다.inspect.SOURCE
(정수 상수3
과 동일): 값은 소스 코드에 나타나는 Annotation의 텍스트 문자열입니다. 대략적일 수 있습니다. 공백은 정규화될 수 있고, 상수 값은 최적화될 수 있습니다. 이 문자열의 정확한 값은 향후 Python 버전에서 변경될 수 있습니다.
__annotate__
함수가 요청된 형식을 지원하지 않으면 NotImplementedError()
를 발생시켜야 합니다. __annotate__
함수는 항상 1
(inspect.VALUE
) 형식을 지원해야 합니다. format=1
로 호출될 때 NotImplementedError()
를 발생시켜서는 안 됩니다.
format=1
로 호출될 때 __annotate__
함수는 NameError
를 발생시킬 수 있습니다. 다른 형식을 요청할 때 NameError
를 발생시켜서는 안 됩니다.
객체에 Annotation이 없으면 __annotate__
는 빈 딕셔너리를 반환하는 함수 대신 None
으로 설정하는 것이 바람직합니다(삭제할 수 없음).
Python 컴파일러가 Annotation이 있는 객체를 컴파일할 때, 적절한 Annotation 함수를 동시에 컴파일합니다. 이 함수는 단일 위치 인수 inspect.VALUE
로 호출되며, 해당 객체에 정의된 Annotation 딕셔너리를 계산하고 반환합니다. Python 컴파일러와 런타임은 함수가 적절한 네임스페이스에 바인딩되도록 함께 작동합니다.
- 함수 및 클래스의 경우 전역 딕셔너리는 객체가 정의된 모듈이 됩니다. 객체가 모듈 자체인 경우 전역 딕셔너리는 자체 딕셔너리가 됩니다.
- 클래스의 메서드 및 클래스의 경우 지역 딕셔너리는 클래스 딕셔너리가 됩니다.
- Annotation이 자유 변수를 참조하는 경우 클로저(closure)는 자유 변수에 대한 셀(cell)을 포함하는 적절한 클로저 튜플이 됩니다.
둘째, 이 PEP는 기존의 __annotations__
가 get, set, delete의 세 가지 작업을 모두 구현하는 “데이터 디스크립터”여야 한다고 요구합니다. __annotations__
는 Annotation 딕셔너리에 대한 참조를 캐시하는 데 사용하는 자체 내부 저장소도 가져야 합니다.
- 클래스 및 모듈 객체는
__dict__
에__annotations__
키를 사용하여 Annotation 딕셔너리를 캐시해야 합니다. 이는 하위 호환성(backwards compatibility)을 위해 필요합니다. - 함수 객체의 경우 Annotation 딕셔너리 캐시의 저장소는 구현 세부 사항입니다. 이는 함수 객체 내부에 있으며 Python에서 보이지 않는 것이 바람직합니다.
이 PEP는 __annotations__
와 __annotate__
가 이들을 구현하는 세 가지 타입 모두에서 어떻게 상호 작용하는지에 대한 의미론을 정의합니다. 다음 예제에서 fn
은 함수를, cls
는 클래스를, mod
는 모듈을, o
는 이 세 가지 타입 중 하나를 나타냅니다.
o.__annotations__
가 평가되고,o.__annotations__
의 내부 저장소가 설정되지 않았으며,o.__annotate__
가 호출 가능으로 설정된 경우,o.__annotations__
의 getter는o.__annotate__(1)
을 호출한 다음 결과를 내부 저장소에 캐시하고 결과를 반환합니다.o.__annotations__
캐시는 이 PEP에 정의된 유일한 캐싱 메커니즘입니다. Python 컴파일러가 생성한__annotate__
함수는 계산하는 값을 명시적으로 캐시하지 않습니다.o.__annotate__
를 호출 가능으로 설정하면 캐시된 Annotation 딕셔너리가 무효화됩니다.o.__annotate__
를None
으로 설정하는 것은 캐시된 Annotation 딕셔너리에 영향을 미치지 않습니다.o.__annotate__
를 삭제하면TypeError
가 발생합니다.__annotate__
는 항상 설정되어야 합니다. 이렇게 하면 주석이 없는 서브클래스가 기본 클래스 중 하나의__annotate__
메서드를 상속하는 것을 방지합니다.o.__annotations__
를 유효한 값으로 설정하면 자동으로o.__annotate__
가None
으로 설정됩니다.cls.__annotations__
또는mod.__annotations__
를None
으로 설정하는 것은 다른 속성과 마찬가지로 작동합니다. 속성이None
으로 설정됩니다.fn.__annotations__
를None
으로 설정하면 캐시된 Annotation 딕셔너리가 무효화됩니다.fn.__annotations__
에 캐시된 Annotation 값이 없고fn.__annotate__
가None
인 경우,fn.__annotations__
데이터 디스크립터는 새 빈 딕셔너리를 생성, 캐시 및 반환합니다. (이는 PEP 3107 의미론과의 하위 호환성을 위한 것입니다.)
허용 가능한 Annotation 구문 변경 (Changes to allowable annotations syntax)
__annotate__
는 이제 __annotations__
가 나중에 참조될 때까지 Annotation 평가를 지연시킵니다. 이는 Annotation이 원래 객체가 정의된 컨텍스트가 아닌 새 함수에서 평가된다는 것을 의미하기도 합니다. stock 의미론에서는 허용되었지만 from __future__ import annotations
가 활성화될 때 허용되지 않았고, 이 PEP가 활성화될 때도 허용되지 않을 상당한 런타임 부작용이 있는 네 가지 연산자가 있습니다.
:=
yield
yield from
await
inspect.get_annotations
및 typing.get_type_hints
변경 (Changes to inspect.get_annotations and typing.get_type_hints)
이 두 함수는 객체에서 Annotation을 추출하고 반환합니다. inspect.get_annotations
는 Annotation을 변경하지 않고 반환합니다. typing.get_type_hints
는 정적 타이핑 사용자의 편의를 위해 Annotation을 반환하기 전에 일부 수정 사항을 적용합니다.
이 PEP는 이 두 함수에 새로운 키워드 전용 매개변수 format
을 추가합니다. format
은 Annotation 딕셔너리의 값이 반환되어야 하는 형식을 지정합니다. 이 두 함수의 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__
메서드를 원하는 값으로 Annotation 딕셔너리를 계산하는 함수로 교체하는 것을 고려하십시오. 그렇지 않으면 inspect.get_annotations
가 SOURCE
및 FORWARDREF
형식에 대해 오래된 결과를 생성하는 것을 방지하기 위해 객체의 __annotate__
메서드를 None
으로 덮어쓰는 것이 가장 좋습니다.
stringizer
와 fake globals
환경 (The stringizer and the fake globals environment)
원래 제안된 바와 같이, 이 PEP는 많은 런타임 Annotation 사용자 사용 사례와 많은 정적 타입 사용자 사용 사례를 지원했습니다. 그러나 이는 불충분했습니다. 이 PEP는 모든 기존 사용 사례를 만족할 때까지는 수용될 수 없었습니다. 이것은 Carl Meyer가 아래에 설명된 “stringizer”와 “fake globals” 환경을 제안할 때까지 이 PEP의 오랜 블로커가 되었습니다. 이러한 기술을 통해 이 PEP는 FORWARDREF
및 SOURCE
형식을 모두 지원하여 나머지 모든 사용 사례를 능숙하게 만족시킬 수 있습니다.
요약하자면, 이 기술은 Python 컴파일러가 생성한 __annotate__
함수를 이국적인 런타임 환경에서 실행하는 것을 포함합니다. 일반적인 전역 딕셔너리는 “fake globals” 딕셔너리라고 불리는 것으로 대체됩니다. “fake globals” 딕셔너리는 중요한 한 가지 차이점이 있는 딕셔너리입니다. 맵핑되지 않은 키를 “가져올” 때마다 해당 키에 대한 새 값을 생성, 캐시 및 반환합니다(딕셔너리의 __missing__
콜백에 따름). 이 값은 “stringizer”라고 불리는 새로운 타입의 인스턴스입니다.
“stringizer”는 매우 특이한 동작을 가진 Python 클래스입니다. 모든 stringizer는 처음에 “fake globals” 딕셔너리에서 누락된 키의 이름인 “값”으로 초기화됩니다. 그런 다음 stringizer는 연산자를 구현하는 데 사용되는 모든 Python “dunder” 메서드를 구현하며, 해당 메서드에 의해 반환되는 값은 해당 연산의 텍스트 표현인 값을 가진 새 stringizer입니다.
이러한 stringizer가 표현식에서 사용될 때, 표현식의 결과는 해당 표현식을 텍스트로 나타내는 이름을 가진 새 stringizer입니다. 예를 들어, f
라는 변수가 'f'
값으로 초기화된 stringizer에 대한 참조라고 가정해 봅시다. 다음은 f
에 대해 수행할 수 있는 몇 가지 작업과 해당 작업이 반환하는 값의 예입니다.
>>> f
Stringizer('f')
>>> f + 3
Stringizer('f + 3')
>> f["key"]
Stringizer('f["key"]')
이 모든 것을 종합하면, Python이 생성한 __annotate__
함수를 실행하되 전역 변수를 “fake globals” 딕셔너리로 대체하면, 참조하는 모든 정의되지 않은 심볼은 해당 심볼을 나타내는 stringizer 프록시 객체로 대체되고, 이러한 프록시에 대해 수행되는 모든 연산은 해당 표현식을 나타내는 프록시로 이어집니다. 이를 통해 __annotate__
는 완료되고 Annotation 딕셔너리를 반환할 수 있으며, 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__
메서드의 전역 딕셔너리의 현재 내용으로 미리 채우고, “fake globals” 딕셔너리를 객체의 __annotate__
메서드에 바인딩하고, VALUE
형식을 요청하여 호출한 다음, 결과를 반환하여 FORWARDREF
형식을 생성합니다.
이 전체 기술은 컴파일러가 생성한 __annotate__
함수가 Python 자체에 의해 제어되며, 간단하고 예측 가능하기 때문에 작동합니다. 실제로 이들은 Annotation 딕셔너리를 계산하고 반환하는 단일 return
문입니다. Annotation을 계산하는 데 필요한 대부분의 연산이 Python에서 dunder 메서드를 사용하여 구현되며, stringizer는 모든 관련 dunder 메서드를 지원하므로 이 접근 방식은 안정적이고 실용적인 해결책입니다.
그러나 모든 __annotate__
메서드에 이 기술을 시도하는 것은 합리적이지 않습니다. 이 PEP는 타사 라이브러리가 자체 __annotate__
메서드를 구현할 수 있다고 가정하며, 해당 함수는 이 “fake globals” 환경에서 실행될 때 거의 확실히 잘못 작동할 것입니다. 이러한 이유로 이 PEP는 코드 객체에 플래그(co_flags
의 사용되지 않는 비트 중 하나)를 할당하여 “이 코드 객체는 ‘fake globals’ 환경에서 실행될 수 있습니다”를 의미합니다. 이렇게 하면 “fake globals” 환경은 엄격하게 옵트인(opt-in)되며, Python 컴파일러가 생성한 __annotate__
메서드만 이를 설정할 것으로 예상됩니다.
이 기술의 약점은 객체의 dunder 메서드에 직접 매핑되지 않는 연산자를 처리하는 데 있습니다. 이들은 모두 분기 또는 반복과 같은 일종의 흐름 제어를 구현하는 연산자입니다.
- Short-circuiting
or
- Short-circuiting
and
- 삼항 연산자(the
if
/then
연산자) - Generator expressions
- List / dict / set comprehensions
- Iterable unpacking
일반적으로 이러한 기술은 Annotation에서 사용되지 않으므로 실제로는 문제가 되지 않습니다. 그러나 Python에 TypeVarTuple
이 최근 추가되면서 iterable unpacking을 사용하게 되었습니다. 관련된 dunder 메서드(__iter__
및 __next__
)는 반복 사용 사례를 구별하는 것을 허용하지 않습니다. 어떤 사용 사례가 관련되었는지 올바르게 감지하려면 단순히 “fake globals”와 “stringizer”만으로는 충분하지 않습니다. 이를 위해서는 SOURCE
및 FORWARDREF
형식을 생성하는 데 특별히 설계된 사용자 지정 바이트 코드 인터프리터가 필요합니다.
다행히 잘 작동하는 지름길이 있습니다. stringizer는 반복 dunder 메서드가 호출될 때 TypeVarTuple
에 의해 수행되는 이터레이터 언패킹(iterator unpacking)을 위해 사용된다고 단순히 가정할 것입니다. 이 동작은 하드코딩됩니다. 이는 반복을 사용하는 다른 기술은 작동하지 않지만, 실제로는 실제 사용 사례에 불편을 주지 않을 것입니다.
마지막으로, “fake globals” 환경은 일치하는 “fake locals” 딕셔너리도 구성해야 합니다. FORWARDREF
형식의 경우 관련 locals 딕셔너리로 미리 채워집니다. “fake globals” 환경은 또한 __annotate__
메서드에 의해 참조되는 자유 변수의 이름으로 미리 생성된 ForwardRef
객체의 튜플인 가짜 “클로저”를 생성해야 합니다.
자유 변수를 참조하는 __annotate__
메서드에서 생성된 ForwardRef
프록시는 해당 자유 변수의 이름과 클로저 값을 지역 딕셔너리에 매핑하여 eval
이 해당 이름에 대해 올바른 값을 사용하도록 보장합니다.
컴파일러 생성 __annotate__
함수 (Compiler-generated annotate functions)
이전 섹션에서 언급했듯이 컴파일러가 생성하는 __annotate__
함수는 간단합니다. 주로 단일 return
문으로 Annotation 딕셔너리를 계산하고 반환합니다.
그러나 inspect.get_annotations
가 FORWARDREF
또는 SOURCE
형식을 요청하는 프로토콜은 먼저 __annotate__
메서드에 이를 생성하도록 요청해야 합니다. Python 컴파일러가 생성하는 __annotate__
메서드는 이러한 형식 중 어느 것도 지원하지 않으며 NotImplementedError()
를 발생시킬 것입니다.
타사 __annotate__
함수 (Third-party annotate functions)
타사 클래스 및 함수는 자체 __annotate__
메서드를 구현해야 할 가능성이 높습니다. 그래야 이러한 객체의 다운스트림 사용자가 Annotation을 최대한 활용할 수 있기 때문입니다. 특히, 래퍼는 래핑된 객체가 생성하는 Annotation 딕셔너리를 변환해야 할 가능성이 높습니다. 즉, 딕셔너리를 어떤 식으로든 추가, 제거 또는 수정해야 합니다.
대부분의 경우 타사 코드는 기존 업스트림 객체에서 inspect.get_annotations
를 호출하여 __annotate__
메서드를 구현할 것입니다. 예를 들어, 래퍼는 래핑된 객체에 대해 요청된 형식으로 Annotation 딕셔너리를 요청한 다음, 반환된 Annotation 딕셔너리를 적절하게 수정하고 반환할 것입니다. 이를 통해 타사 코드는 “fake globals” 기술을 이해하거나 참여하지 않고도 활용할 수 있습니다.
PEP 649 이전 및 이후 버전의 Python을 모두 지원하는 타사 라이브러리는 두 가지를 모두 지원하는 방법에 대한 자체 모범 사례를 개발해야 할 것입니다. 한 가지 합리적인 접근 방식은 래퍼가 항상 __annotate__
를 지원한 다음, VALUE
형식을 요청하여 호출하고 그 결과를 래퍼 객체의 __annotations__
로 저장하는 것입니다. 이는 PEP 649 이전 Python 의미론을 지원하고 PEP 649 이후 의미론과 전방 호환될 것입니다.
의사 코드 (Pseudocode)
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 런타임에 대한 기타 수정 사항 (Other modifications to the Python runtime)
이 PEP는 정확히 어떻게 구현되어야 하는지 지시하지 않습니다. 이는 언어 구현 유지 관리자에게 맡겨져 있습니다. 그러나 이 PEP의 최상의 구현은 기존 Python 객체에 추가 정보를 추가해야 할 수 있으며, 이는 이 PEP의 수용으로 암묵적으로 용인됩니다.
예를 들어, 클래스 객체에 __globals__
속성을 추가해야 할 수도 있습니다. 그래야 해당 클래스의 __annotate__
함수가 필요할 때만 지연 바인딩될 수 있습니다. 또한, 클래스에 정의된 메서드에 정의된 __annotate__
함수는 해당 클래스에 바인딩된 이름을 올바르게 평가하기 위해 클래스의 __dict__
에 대한 참조를 유지해야 할 수도 있습니다. 이 PEP의 CPython 구현은 이 두 가지 새로운 속성을 모두 포함할 것으로 예상됩니다.
기존 Python 객체에 추가된 모든 새로운 정보는 “dunder” 속성으로 이루어져야 합니다. 물론 이는 구현 세부 사항이기 때문입니다.
대화형 REPL 셸 (Interactive REPL Shell)
이 PEP에 확립된 의미론은 Python의 대화형 REPL 셸에서 코드를 실행할 때도 적용됩니다. 단, 대화형 모듈 (__main__
) 자체의 모듈 Annotation은 예외입니다. 이 모듈은 결코 “완료”되지 않으므로 __annotate__
함수를 컴파일할 특정 시점이 없습니다.
단순화를 위해 이 경우 지연 평가는 포기됩니다. REPL 셸의 모듈 수준 Annotation은 “stock 의미론”과 동일하게 즉시 평가되고 결과가 __annotations__
딕셔너리에 직접 설정됩니다.
함수 내 지역 변수에 대한 Annotation (Annotations On Local Variables Inside Functions)
Python은 함수 내 지역 변수에 대한 Annotation 구문을 지원합니다. 그러나 이러한 Annotation은 런타임 효과가 없습니다. 컴파일 시 버려집니다. 따라서 이 PEP는 stock 의미론 및 PEP 563과 동일하게 이를 지원하기 위해 아무것도 할 필요가 없습니다.
프로토타입 (Prototype)
이 PEP의 원래 프로토타입 구현은 여기에서 찾을 수 있습니다:
https://github.com/larryhastings/co_annotations/
이 글을 쓰는 시점에는 구현이 심각하게 오래되었습니다. Python 3.10을 기반으로 하며 2021년 초 이 PEP의 첫 번째 초안 의미론을 구현합니다. 곧 업데이트될 예정입니다.
성능 비교 (Performance Comparison)
이 PEP의 성능은 일반적으로 양호합니다. 고려해야 할 네 가지 시나리오가 있습니다.
- Annotation이 정의되지 않은 경우의 런타임 비용
- Annotation이 정의되었지만 참조되지 않은 경우의 런타임 비용
- Annotation이 객체로 정의되고 참조되는 경우의 런타임 비용
- Annotation이 문자열로 정의되고 참조되는 경우의 런타임 비용
Annotation에 대한 세 가지 의미론(stock, PEP 563, 이 PEP) 모두의 컨텍스트에서 각 시나리오를 검토할 것입니다.
Annotation이 없는 경우, 세 가지 의미론 모두 동일한 런타임 비용(0)을 가집니다. Annotation 딕셔너리는 생성되지 않으며 코드가 생성되지 않습니다. 이는 런타임 프로세서 시간을 필요로 하지 않으며 메모리를 소비하지 않습니다.
Annotation이 정의되었지만 참조되지 않은 경우, 이 PEP를 사용하는 Python의 런타임 비용은 PEP 563과 거의 동일하며 stock 의미론보다 향상되었습니다. 세부 사항은 Annotation되는 객체에 따라 다릅니다.
- Stock 의미론의 경우 Annotation 딕셔너리는 항상 빌드되고 Annotation되는 객체의 속성으로 설정됩니다.
- PEP 563 의미론의 경우 함수 객체에 대해 미리 컴파일된 상수(특별히 구성된 튜플)가 함수의 속성으로 설정됩니다. 클래스 및 모듈 객체의 경우 Annotation 딕셔너리는 항상 빌드되고 클래스 또는 모듈의 속성으로 설정됩니다.
- 이 PEP의 경우 단일 객체가 Annotation되는 객체의 속성으로 설정됩니다. 대부분의 경우 이 객체는 상수(코드 객체)이지만, Annotation이 클래스 네임스페이스 또는 클로저를 필요로 하는 경우 이 객체는 바인딩 시간에 구성된 튜플이 됩니다.
Annotation이 객체로 정의되고 참조되는 경우, 이 PEP를 사용하는 코드는 PEP 563보다 훨씬 빠르고 stock 의미론과 같거나 더 빠를 것입니다. PEP 563 의미론은 Annotation 딕셔너리 내부의 모든 값에 대해 eval()
을 호출해야 하며 이는 엄청나게 느립니다. 그리고 이 PEP의 구현은 stock 의미론보다 클래스 및 모듈 Annotation에 대해 훨씬 효율적인 바이트 코드를 생성합니다. 함수 Annotation의 경우 이 PEP와 stock 의미론은 거의 동일한 속도를 가질 것입니다.
이 PEP가 PEP 563보다 눈에 띄게 느린 한 가지 경우는 Annotation이 문자열로 요청될 때입니다. “이미 문자열입니다”를 이기기는 어렵습니다. 그러나 stringized annotations는 성능이 핵심 요소가 될 가능성이 적은 온라인 문서화 사용 사례를 위한 것입니다.
메모리 사용량도 세 가지 시나리오 모두에서 세 가지 의미론 컨텍스트에서 비교 가능해야 합니다. 첫 번째 및 세 번째 시나리오에서는 모든 경우에 메모리 사용량이 거의 동일해야 합니다. 두 번째 시나리오(Annotation이 정의되었지만 참조되지 않은 경우)에서는 이 PEP의 의미론을 사용하면 함수/클래스/모듈이 하나의 사용되지 않는 코드 객체를 저장합니다(아마도 사용되지 않는 함수 객체에 바인딩됨). 다른 두 의미론의 경우 하나의 사용되지 않는 딕셔너리 또는 상수 튜플을 저장합니다.
하위 호환성 (Backwards Compatibility)
Stock 의미론과의 하위 호환성 (Backwards Compatibility With Stock Semantics)
이 PEP는 stock 의미론의 Annotation의 거의 모든 기존 동작을 보존합니다.
__annotations__
속성에 저장된 Annotation 딕셔너리의 형식은 변경되지 않습니다.- Annotation 딕셔너리에는 PEP 563과 같이 문자열이 아닌 실제 값이 포함됩니다.
- Annotation 딕셔너리는 변경 가능하며, 변경 사항은 보존됩니다.
__annotations__
속성은 명시적으로 설정할 수 있으며, 이 방식으로 설정된 모든 유효한 값은 보존됩니다.del
문을 사용하여__annotations__
속성을 삭제할 수 있습니다.
stock 의미론으로 작동하는 대부분의 코드는 이 PEP가 활성화될 때 수정 없이 계속 작동해야 합니다. 그러나 다음과 같은 예외가 있습니다.
첫째, 클래스 Annotation에 액세스하는 잘 알려진 관용구가 있는데, 이 PEP가 활성화될 때 올바르게 작동하지 않을 수 있습니다. 클래스 Annotation의 원래 구현에는 버그라고 부를 수밖에 없는 것이 있었습니다. 클래스가 자체 Annotation을 정의하지 않았지만 기본 클래스 중 하나가 Annotation을 정의한 경우, 클래스는 해당 Annotation을 “상속”했습니다. 이 동작은 바람직하지 않았으므로 사용자 코드는 cls.__annotations__
를 통해 클래스에서 직접 Annotation에 액세스하는 대신 cls.__dict__.get("__annotations__", {})
와 같이 dict
를 통해 클래스의 Annotation에 액세스하는 해결 방법을 찾았습니다. 이 관용구는 클래스가 Annotation을 __dict__
에 저장하고, 이 방식으로 액세스하면 기본 클래스에서 조회를 피했기 때문에 작동했습니다. 이 기술은 CPython의 구현 세부 사항에 의존했으므로 지원되는 동작은 아니었지만, 필요했습니다. 그러나 이 PEP가 활성화되면 클래스에 Annotation이 정의되어 있지만 아직 __annotate__
를 호출하고 결과를 캐시하지 않았을 수 있으며, 이 경우 이 접근 방식은 클래스에 Annotation이 없다고 잘못 가정하게 됩니다. 어쨌든 이 버그는 Python 3.10부터 수정되었으며, 이 관용구는 더 이상 사용해서는 안 됩니다. 또한 Python 3.10부터 Annotation 작업에 대한 모범 사례를 정의하는 Annotation HOWTO가 있습니다. 이 지침을 따르는 코드는 이 PEP가 활성화될 때도 올바르게 작동합니다. 이는 코드가 실행되는 Python 버전에 따라 클래스 객체에서 Annotation을 가져오는 다른 접근 방식을 사용할 것을 제안하기 때문입니다.
Annotation 평가를 검사될 때까지 지연시키는 것은 언어의 의미론을 변경하므로 언어 내부에서 관찰 가능합니다. 따라서 Annotation이 바인딩 시간에 평가되는지 액세스 시간에 평가되는지에 따라 다르게 동작하는 코드를 작성할 수 있습니다. 예를 들어:
mytype = str
def foo(a:mytype): pass
mytype = int
print(foo.__annotations__['a'])
이것은 stock 의미론에서는 <class 'str'>
를 출력하고 이 PEP가 활성화될 때는 <class 'int'>
를 출력합니다. 따라서 이는 하위 호환되지 않는 변경입니다. 그러나 이 예제는 좋지 않은 프로그래밍 스타일이므로 이 변경은 허용 가능한 것으로 보입니다.
클래스 및 모듈 Annotation과 함께 작동하는 두 가지 흔치 않은 상호 작용이 stock 의미론에서는 작동하지만, 이 PEP가 활성화될 때는 더 이상 작동하지 않습니다. 이 두 가지 상호 작용은 금지되어야 합니다. 좋은 소식은 둘 다 흔하지 않으며, 좋은 관행으로 간주되지 않는다는 것입니다. 사실, Python 자체의 회귀 테스트 스위트(regression test suite) 외에서는 거의 볼 수 없습니다. 이들은 다음과 같습니다.
- 어떤 종류의 흐름 제어문 내부에서 모듈 또는 클래스 속성에 Annotation을 설정하는 코드.
- 모듈 또는 클래스 스코프에서 지역
__annotations__
딕셔너리를 직접 참조하거나 수정하는 코드.
마지막으로, 이 PEP가 활성화되면 Annotation 값은 if
/ else
삼항 연산자를 사용해서는 안 됩니다. o.__annotations__
에 액세스하거나 헬퍼 함수에서 inspect.VALUE
를 요청할 때는 올바르게 작동하지만, 일부 이름이 정의되었을 때 inspect.FORWARDREF
와 함께 부울 표현식이 올바르게 계산되지 않을 수 있으며, inspect.SOURCE
와 함께는 훨씬 덜 정확할 것입니다.
PEP 563 의미론과의 하위 호환성 (Backwards Compatibility With PEP 563 Semantics)
PEP 563은 Annotation의 의미론을 변경했습니다. 의미론이 활성화되면 Annotation은 모듈 수준 또는 클래스 수준 스코프에서 평가될 것이라고 가정해야 합니다. 더 이상 현재 함수 또는 둘러싸는 함수(enclosing function)의 지역 변수를 직접 참조할 수 없습니다. 이 PEP는 해당 제한을 제거하며, Annotation은 모든 지역 변수를 참조할 수 있습니다.
PEP 563은 stringized annotations를 “실제” 값으로 변환하기 위해 eval
(또는 typing.get_type_hints
또는 inspect.get_annotations
와 같은 헬퍼 함수)을 사용하도록 요구합니다. stringized annotations를 활성화하고 문자열을 실제 값으로 다시 변환하기 위해 eval()
을 직접 호출하는 기존 코드는 단순히 eval()
호출을 제거할 수 있습니다. 헬퍼 함수를 사용하는 기존 코드는 변경 없이 계속 작동하지만, 해당 함수의 사용은 선택 사항이 될 수 있습니다.
정적 타이핑 사용자는 종종 비활성 타입 힌트 정의만 포함하는 모듈을 가지고 있지만, 실제 코드는 없습니다. 이러한 모듈은 정적 타입 검사를 실행할 때만 필요하며 런타임에는 사용되지 않습니다. 그러나 stock 의미론에서는 런타임이 Annotation을 평가하고 계산하려면 이러한 모듈을 가져와야 합니다. 한편, 이러한 모듈은 해결하기 어렵거나 심지어 불가능할 수 있는 순환 가져오기(circular import) 문제를 종종 야기했습니다. PEP 563은 사용자가 두 가지 작업을 수행하여 이러한 순환 가져오기 문제를 해결할 수 있도록 했습니다. 첫째, 모듈에서 PEP 563을 활성화했는데, 이는 Annotation이 상수 문자열이었고 Annotation을 계산하는 데 실제 심볼이 정의될 필요가 없다는 것을 의미했습니다. 둘째, 이는 사용자가 if typing.TYPE_CHECKING
블록에서 문제가 있는 모듈만 가져올 수 있도록 허용했습니다. 이를 통해 정적 타입 검사기는 모듈과 그 안에 있는 타입 정의를 가져올 수 있었지만, 런타임에는 가져오지 않았습니다. 지금까지 이 접근 방식은 이 PEP가 활성화될 때 변경 없이 작동할 것입니다. if typing.TYPE_CHECKING
은 지원되는 동작입니다.
그러나 일부 코드베이스는 if typing.TYPE_CHECKING
기술을 사용하고 Annotation에서 사용된 정의를 가져오지 않더라도 런타임에 Annotation을 실제로 검사했습니다. 이러한 코드베이스는 Annotation 문자열을 평가하지 않고 검사했으며, 대신 문자열에 대한 동일성 검사 또는 간단한 어휘 분석(lexical analysis)에 의존했습니다.
이 PEP는 이러한 기술도 지원합니다. 그러나 사용자는 코드를 포팅해야 합니다. 첫째, 사용자 코드는 inspect.get_annotations
또는 typing.get_type_hints
를 사용하여 Annotation에 액세스해야 합니다. 객체에서 단순히 __annotations__
속성을 가져올 수는 없을 것입니다. 둘째, 해당 함수를 호출할 때 format
에 inspect.FORWARDREF
또는 inspect.SOURCE
를 지정해야 합니다. 이는 모든 심볼이 정의되지 않았을 때도 헬퍼 함수가 Annotation 딕셔너리를 성공적으로 생성할 수 있다는 것을 의미합니다. stringized annotations를 기대하는 코드는 inspect.SOURCE
형식의 Annotation 딕셔너리와 함께 수정 없이 작동해야 합니다. 그러나 사용자는 분석을 더 쉽게 할 수 있으므로 inspect.FORWARDREF
로 전환하는 것을 고려해야 합니다.
마찬가지로 PEP 563은 이전에 불가능했던 방식으로 Annotation이 있는 클래스에 클래스 데코레이터를 사용할 수 있도록 허용했습니다. 일부 클래스 데코레이터(dataclasses
등)는 클래스의 Annotation을 검사합니다. @
데코레이터 구문을 사용하는 클래스 데코레이터는 클래스 이름이 바인딩되기 전에 실행되므로 해결할 수 없는 순환 정의 문제를 일으킬 수 있습니다. 클래스의 속성을 클래스 자체에 대한 참조로 Annotation하거나, 여러 클래스의 속성을 서로 순환 참조로 Annotation하면, Annotation을 검사하는 데코레이터를 사용하여 @
데코레이터 구문으로 해당 클래스를 데코레이션할 수 없습니다. PEP 563은 데코레이터가 문자열을 어휘적으로 검사하고 eval
을 사용하여 평가하지 않는 한(또는 추가 해결 방법으로 NameError
를 처리하는 한) 이것이 작동하도록 허용했습니다. 이 PEP가 활성화되면 데코레이터는 헬퍼 함수를 사용하여 inspect.SOURCE
또는 inspect.FORWARDREF
형식으로 Annotation 딕셔너리를 계산할 수 있습니다. 이를 통해 원하는 형식으로 정의되지 않은 심볼을 포함하는 Annotation을 분석할 수 있습니다.
PEP 563의 초기 채택자들은 “stringized” annotations가 자동으로 생성된 문서에 유용하다는 것을 발견했습니다. 사용자들은 이 사용 사례를 실험했으며, Python의 pydoc
은 이 기술에 관심을 표명했습니다. 이 PEP는 이 사용 사례를 지원합니다. 문서를 생성하는 코드는 헬퍼 함수를 사용하여 inspect.SOURCE
형식으로 Annotation에 액세스하도록 업데이트되어야 합니다.
마지막으로, Annotation에서 if
/ else
삼항 연산자를 사용하는 것에 대한 경고는 PEP 563 사용자에게도 동일하게 적용됩니다. 현재는 그들에게 작동하지만, 헬퍼 함수에서 일부 형식을 요청할 때 잘못된 결과를 생성할 수 있습니다.
이 PEP가 수락되면 PEP 563은 더 이상 사용되지 않고 결국 제거될 것입니다. PEP 563의 초기 채택자(이제 해당 의미론에 의존함)를 위한 이러한 전환을 용이하게 하기 위해 inspect.get_annotations
및 typing.get_type_hints
는 특별한 기능을 구현할 것입니다.
Python 컴파일러는 PEP 563 의미론이 활성화된 모듈에 정의된 객체에 대해 Annotation 코드 객체를 생성하지 않을 것입니다. 이 PEP가 수락되더라도 마찬가지입니다. 따라서 정상적인 상황에서는 헬퍼 함수에서 inspect.SOURCE
형식을 요청하면 빈 딕셔너리가 반환됩니다. 전환을 용이하게 하기 위한 기능으로, 헬퍼 함수가 객체가 PEP 563이 활성화된 모듈에 정의되었음을 감지하고 사용자가 inspect.SOURCE
형식을 요청하면, __annotations__
딕셔너리의 현재 값을 반환할 것입니다. 이 경우 stringized annotations가 될 것입니다. 이를 통해 stringized annotations를 어휘적으로 분석하는 PEP 563 사용자는 즉시 헬퍼 함수에서 inspect.SOURCE
형식을 요청하도록 변경할 수 있으며, 이는 PEP 563에서 벗어나는 전환을 원활하게 할 것입니다.
기각된 아이디어 (Rejected Ideas)
“그냥 문자열을 저장합니다 (Just store the strings)”
SOURCE
형식을 지원하기 위해 제안된 한 가지 아이디어는 Python 컴파일러가 Annotation 값에 대한 실제 소스 코드를 어딘가에 내보내고, 사용자가 SOURCE
형식을 요청할 때 이를 제공하는 것이었습니다.
이 아이디어는 “아직 아니다(not yet)”로 분류되었습니다. 우리는 이미 FORWARDREF
형식을 지원해야 한다는 것을 알고 있으며, 해당 기술은 몇 줄만으로 SOURCE
형식을 지원하도록 조정할 수 있습니다. 이 접근 방식에 대한 많은 미해결 질문이 있습니다.
- 문자열을 어디에 저장할까요?
- Annotation이 달린 객체가 생성될 때 항상 로드될까요, 아니면 필요에 따라 지연 로드될까요? 그렇다면 지연 로딩은 어떻게 작동할까요?
- “소스 코드”에 원본의 줄 바꿈 및 주석이 포함될까요?
- 들여쓰기 및 단순히 서식 지정을 위해 사용되는 추가 공백을 포함하여 모든 공백을 보존할까요?
SOURCE
값의 원본 소스 코드에 대한 충실도를 높이는 것이 충분히 중요하다고 판단되면 향후 이 주제를 다시 검토할 가능성이 있습니다.
감사 (Acknowledgements)
지속적인 피드백과 격려를 주신 Carl Meyer, Barry Warsaw, Eric V. Smith, Mark Shannon, Jelle Zijlstra, 그리고 Guido van Rossum께 감사드립니다.
이 제안의 최고의 측면 중 일부가 된 핵심 아이디어를 제공해주신 여러 개인에게 특히 감사드립니다.
- Carl Meyer:
FORWARDREF
및SOURCE
형식을 가능하게 한 “stringizer” 기술을 제안하여, 해결 불가능해 보이는 문제로 1년간 정체되어 있던 이 PEP가 진전할 수 있도록 했습니다. 또한inspect.SOURCE
가 stringized annotations를 반환하는 PEP 563 사용자를 위한 편의 기능과 더 많은 제안을 했습니다. Carl은 이 PEP를 논의하는 비공개 이메일 스레드의 주요 통신원이자 지칠 줄 모르는 정보원이었으며 이성적인 목소리였습니다. Carl의 기여가 없었다면 이 PEP는 거의 확실히 수락되지 않았을 것입니다. - Mark Shannon: 전체 Annotation 딕셔너리를 단일 코드 객체 내부에 구축하고 필요할 때만 함수에 바인딩할 것을 제안했습니다.
- Guido van Rossum:
__annotate__
함수가 “stock” 의미론의 Annotation의 이름 가시성 규칙을 복제해야 한다고 제안했습니다. - Jelle Zijlstra: 피드백뿐만 아니라 코드도 기여했습니다!
참조 (References)
https://github.com/larryhastings/co_annotations/issues
https://discuss.python.org/t/two-polls-on-how-to-revise-pep-649/23628
https://discuss.python.org/t/a-massive-pep-649-update-with-some-major-course-corrections/25672
저작권 (Copyright)
이 문서는 퍼블릭 도메인 또는 CC0-1.0-Universal 라이선스 중 더 관대한 라이선스에 따라 배포됩니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments