[Superseded] PEP 563 - Postponed Evaluation of Annotations

원문 링크: PEP 563 - Postponed Evaluation of Annotations

상태: Superseded 유형: Standards Track 작성일: 08-Sep-2017

PEP 563 – 어노테이션 지연 평가 (Postponed Evaluation of Annotations)

상태: 이 PEP에서 제안된 기능은 기본 동작이 되지 못했으며, PEP 649 및 PEP 749에서 제안된 어노테이션의 지연된 평가로 대체되었습니다.

개요 (Abstract)

PEP 3107은 함수 어노테이션을 위한 구문을 도입했지만, 그 의미는 의도적으로 정의되지 않은 채로 남겨두었습니다. PEP 484는 어노테이션에 표준적인 의미인 “타입 힌트 (type hints)”를 부여했습니다. PEP 526은 변수 어노테이션을 정의하며, 이를 타입 힌트 사용 사례와 명시적으로 연결했습니다.

PEP 563은 함수 및 변수 어노테이션이 더 이상 함수 정의 시점에 평가되지 않도록 변경할 것을 제안합니다. 대신, 어노테이션은 __annotations__에 문자열 형태로 보존됩니다. 이 변경 사항은 Python 3.7부터 __future__ import를 통해 점진적으로 도입되었습니다.

배경 및 목표 (Rationale and Goals)

PEP 3107은 함수 정의의 일부에 임의의 어노테이션을 추가하는 기능을 지원했습니다. 기본값(default values)과 마찬가지로 어노테이션은 함수 정의 시점에 평가됩니다. 이는 타입 힌트 사용 사례에서 여러 문제를 야기했습니다:

  • 전방 참조 (forward references): 타입 힌트에 아직 정의되지 않은 이름이 포함될 경우, 해당 정의는 문자열 리터럴로 표현되어야 합니다.
  • 런타임 비용: 타입 힌트는 모듈 임포트(module import) 시점에 실행되므로, 계산 비용이 발생합니다.

어노테이션 평가를 지연하면 이 두 가지 문제가 모두 해결됩니다.

참고: PEP 649는 위 문제에 대한 대체 솔루션을 제안하여, 이 PEP가 대체될 위험에 처하게 되었습니다.

비목표 (Non-goals)

PEP 484 및 PEP 526과 마찬가지로, Python은 동적 타입 언어로 유지될 것이며, 타입 힌트를 관례로든 강제로든 필수화할 의도는 없음을 강조합니다. 이 PEP는 타입 어노테이션의 전방 참조 문제를 해결하기 위한 것입니다. 어노테이션 외부에 전방 참조가 여전히 문자열 리터럴 사용을 요구하는 경우가 존재하며, 이는 이 문서의 뒷부분에 나열되어 있습니다.

강제 평가가 없는 어노테이션은 타입 힌트의 구문을 개선할 기회를 제공합니다. 이 아이디어는 별도의 PEP가 필요하며 이 문서에서는 더 이상 논의되지 않습니다.

비-타이핑 목적의 어노테이션 사용 (Non-typing usage of annotations)

어노테이션은 타입 검사 외에도 임의의 용도로 계속 사용 가능하지만, 이 PEP의 설계와 그 선구자들(PEP 484 및 PEP 526)은 주로 타입 힌트 사용 사례에 의해 동기 부여되었다는 점을 언급할 가치가 있습니다. Python 3.8에서 PEP 484는 임시(provisional) 상태에서 벗어나게 됩니다. PEP 544, PEP 557, PEP 560과 같은 다른 Python 언어 개선 사항들은 이미 PEP 484에 정의된 타입 어노테이션 및 typing 모듈에 의존하고 있습니다.

이를 염두에 두고, 위 PEP들과 호환되지 않는 어노테이션 사용은 폐기(deprecated)될 것으로 간주해야 합니다.

구현 (Implementation)

이 PEP를 통해 함수 및 변수 어노테이션은 더 이상 정의 시점에 평가되지 않습니다. 대신, 문자열 형태가 해당 __annotations__ 딕셔너리에 보존됩니다. 정적 타입 검사기(static type checkers)는 동작에 차이를 느끼지 않겠지만, 런타임에 어노테이션을 사용하는 도구는 지연 평가를 수행해야 합니다.

문자열 형태는 컴파일 단계에서 AST(Abstract Syntax Tree)로부터 얻어지며, 이는 문자열 형태가 원본 소스 코드의 정확한 포맷팅을 보존하지 않을 수 있음을 의미합니다. 어노테이션이 이미 문자열 리터럴이었더라도 여전히 문자열로 래핑됩니다.

어노테이션은 구문적으로 유효한 Python 표현식이어야 하며, 리터럴 문자열로 전달될 때도 마찬가지입니다 (예: compile(literal, '', 'eval')). 어노테이션은 모듈 스코프(module scope)에 있는 이름만 사용할 수 있습니다. 이는 로컬 이름을 사용한 지연 평가가 신뢰할 수 없기 때문입니다 (클래스 수준 이름은 typing.get_type_hints()에 의해 해결되는 유일한 예외). PEP 526에 따라 로컬 변수 어노테이션은 함수의 클로저(closure) 외부에서 접근할 수 없으므로 전혀 평가되지 않습니다.

Python 3.7에서 미래 동작 활성화 (Enabling the future behavior in Python 3.7)

위에 설명된 기능은 Python 3.7부터 다음 특별 import를 사용하여 활성화할 수 있습니다:

from __future__ import annotations

이 기능의 참조 구현은 GitHub에서 사용할 수 있습니다.

런타임에 타입 힌트 해결 (Resolving Type Hints at Runtime)

런타임에 어노테이션을 문자열 형태에서 포함된 표현식의 결과로 해결하려면, 사용자 코드가 해당 문자열을 평가해야 합니다.

타입 힌트를 사용하는 코드의 경우, typing.get_type_hints(obj, globalns=None, localns=None) 함수는 문자열 형태에서 표현식을 올바르게 평가합니다. 현재 __annotations__를 사용하는 모든 유효한 코드는 타입 어노테이션이 문자열 리터럴로 표현될 수 있으므로 이미 이렇게 해야 합니다.

다른 목적으로 어노테이션을 사용하는 코드의 경우, 일반적인 eval(ann, globals, locals) 호출만으로 어노테이션을 해결하기에 충분합니다.

두 경우 모두 globalslocals가 지연 평가에 어떻게 영향을 미치는지 고려하는 것이 중요합니다. 어노테이션은 더 이상 정의 시점에, 그리고 더 중요하게는 정의된 것과 동일한 스코프에서 평가되지 않습니다. 결과적으로, 어노테이션에 로컬 상태를 사용하는 것은 일반적으로 더 이상 불가능합니다. globals의 경우, 어노테이션이 정의된 모듈이 지연 평가를 위한 올바른 컨텍스트입니다.

get_type_hints() 함수는 함수 및 클래스에 대한 globalns의 올바른 값을 자동으로 해결합니다. 또한 클래스에 대한 올바른 localns를 자동으로 제공합니다.

eval()을 실행할 때, globals의 값은 다음 방식으로 수집될 수 있습니다:

  • 함수 객체는 __globals__라는 속성에 해당 globals에 대한 참조를 가지고 있습니다.
  • 클래스는 정의된 모듈의 이름을 가지고 있으며, 이를 사용하여 해당 globals를 검색할 수 있습니다.
    cls_globals = vars(sys.modules[SomeClass.__module__])
    

    이것은 모든 __annotations__를 평가하기 위해 기본 클래스(base classes)에 대해서도 반복되어야 합니다.

  • 모듈은 자신의 __dict__를 사용해야 합니다.

localns의 값은 함수에 대해 신뢰할 수 있게 검색할 수 없습니다. 이는 호출 시점의 스택 프레임이 더 이상 존재하지 않을 가능성이 높기 때문입니다.

클래스의 경우, localns는 주어진 클래스와 그 기본 클래스(메서드 결정 순서로)의 vars를 연결하여 구성될 수 있습니다. 슬롯(slots)은 클래스가 정의된 후에만 채워질 수 있으므로, 이 목적으로 슬롯을 고려할 필요는 없습니다.

런타임 어노테이션 해결과 클래스 데코레이터 (Runtime annotation resolution and class decorators)

현재 클래스에 대한 어노테이션을 해결해야 하는 메타클래스(metaclasses) 및 클래스 데코레이터(class decorators)는 현재 클래스의 이름을 사용하는 어노테이션에 대해 실패합니다. 예시:

def class_decorator(cls):
    annotations = get_type_hints(cls) # 'C'에서 NameError 발생
    print(f'Annotations for {cls}: {annotations}')
    return cls

@class_decorator
class C:
    singleton: 'C' = None

이는 이 PEP 이전에도 마찬가지였습니다. 클래스 데코레이터는 현재 정의 스코프에서 클래스에 이름이 할당되기 전에 클래스에 작용합니다.

런타임 어노테이션 해결과 TYPE_CHECKING (Runtime annotation resolution and TYPE_CHECKING)

때로는 타입 검사기가 봐야 하지만 실행되어서는 안 되는 코드가 있습니다. 이러한 상황을 위해 typing 모듈은 타입 검사 중에는 True로 간주되지만 런타임에는 False인 상수 TYPE_CHECKING을 정의합니다. 예시:

import typing

if typing.TYPE_CHECKING:
    import expensive_mod

def a_func(arg: expensive_mod.SomeClass) -> None:
    a_var: expensive_mod.SomeClass = arg
    ...

이 접근 방식은 임포트 순환(import cycles)을 처리할 때도 유용합니다.

typing.get_type_hints()를 사용하여 a_func의 어노테이션을 런타임에 해결하려고 하면 expensive_mod 이름이 정의되지 않았기 때문에 실패합니다 (런타임에 TYPE_CHECKING 변수가 False이기 때문입니다). 이는 이 PEP 이전에도 마찬가지였습니다.

하위 호환성 (Backwards Compatibility)

이것은 하위 호환성을 깨는 변경 사항입니다. typing.get_type_hints() 또는 eval()을 사용하지 않고 어노테이션에 임의의 객체가 직접 존재한다고 가정하는 애플리케이션은 깨지게 됩니다.

함수 정의 시점의 locals에 의존하는 어노테이션은 나중에 해결할 수 없게 됩니다. 예시:

def generate():
    A = Optional[int]
    class C:
        field: A = 1
        def method(self, arg: A) -> None: ...
    return C

X = generate()

나중에 get_type_hints(X)를 사용하여 X의 어노테이션을 해결하려고 하면 A와 그를 둘러싼 스코프가 더 이상 존재하지 않기 때문에 실패합니다. Python은 이러한 어노테이션을 금지하려는 시도를 하지 않습니다. 이는 어노테이션의 주된 사용 사례인 정적 분석에서 여전히 성공적으로 분석될 수 있기 때문입니다.

중첩된 클래스(nested classes) 및 해당 상태를 사용하는 어노테이션은 여전히 유효합니다. 이들은 로컬 이름 또는 정규화된 이름(fully qualified name)을 사용할 수 있습니다. 예시:

class C:
    field = 'c_field'
    def method(self) -> C.field: # OK
        ...
    def method(self) -> field: # OK
        ...
    def method(self) -> C.D: # OK
        ...
    def method(self) -> D: # OK
        ...
    class D:
        field2 = 'd_field'
        def method(self) -> C.D.field2: # OK
            ...
        def method(self) -> D.field2: # 실패, 클래스 D는 C의 로컬이므로
                                   # C.D로만 접근 가능. PEP 이전에도 마찬가지.
            ...
        def method(self) -> field2: # OK
            ...
        def method(self) -> field: # 실패, field는 C의 로컬이므로
                                   # C.field로 접근하지 않으면 D에서 보이지 않음.
                                   # PEP 이전에도 마찬가지.
            ...

구문적으로 유효하지 않은 표현식인 어노테이션이 있는 경우, 컴파일 시점에 SyntaxError가 발생합니다. 그러나 이름이 해당 시점에 해결되지 않으므로, 사용된 이름이 올바른지 여부를 검증하려는 시도는 이루어지지 않습니다.

폐기 정책 (Deprecation policy)

Python 3.7부터는 설명된 기능을 사용하려면 __future__ import가 필요합니다. 경고는 발생하지 않습니다.

참고: 이 기능이 결국 기본 동작이 될지 여부는 PEP 649에 대한 결정이 보류 중이므로 현재 불분명합니다. 어떠한 경우든, 즉시 평가(eager evaluation)에 의존하는 어노테이션의 사용은 두 제안 모두와 호환되지 않으며 더 이상 지원되지 않습니다.

전방 참조 (Forward References)

모듈에서 이름이 정의되기 전에 의도적으로 이름을 사용하는 것을 전방 참조라고 합니다. 이 섹션의 목적을 위해 if TYPE_CHECKING: 블록 내에서 임포트되거나 정의된 모든 이름도 전방 참조라고 부를 것입니다.

이 PEP는 타입 어노테이션의 전방 참조 문제를 해결합니다. 이 경우 문자열 리터럴의 사용은 더 이상 필수가 아닙니다. 그러나 typing 모듈에는 언어의 다른 구문 구조를 사용하는 API가 있으며, 이들은 여전히 문자열 리터럴로 전방 참조를 우회해야 합니다. 목록은 다음과 같습니다:

  • 타입 정의:
    T = TypeVar('T', bound='<type>')
    UserId = NewType('UserId', '<type>')
    Employee = NamedTuple('Employee', [('name', '<type>'), ('id', '<type>')])
    
  • 별칭 (aliases):
    Alias = Optional['<type>']
    AnotherAlias = Union['<type>', '<type>']
    YetAnotherAlias = '<type>'
    
  • 캐스팅 (casting):
    cast('<type>', value)
    
  • 기본 클래스 (base classes):
    class C(Tuple['<type>', '<type>']): ...
    

    특정 경우에 따라 위 목록의 일부 사례는 if TYPE_CHECKING: 블록에 사용을 배치하여 해결할 수 있습니다. 이는 런타임에 사용할 수 있어야 하는 코드, 특히 기본 클래스 및 캐스팅에는 작동하지 않습니다. NamedTuple의 경우, Python 3.6에서 도입된 새 클래스 정의 구문을 사용하면 문제가 해결됩니다.

일반적으로 모든 전방 참조에 대한 문제를 해결하려면 현재의 단일 패스 하향식(single-pass top-down) 모델에서 Python의 모듈 인스턴스화 방식 변경이 필요합니다. 이는 언어에 대한 주요 변경 사항이므로 이 PEP의 범위 밖입니다.

기각된 아이디어 (Rejected Ideas)

어노테이션 정의 시 함수 로컬 상태 사용 기능 유지 (Keeping the ability to use function local state when defining annotations)

지연 평가를 통해, 이는 어노테이션이 생성된 프레임에 대한 참조를 유지해야 합니다. 예를 들어, 모든 어노테이션을 문자열 대신 람다(lambdas)로 저장함으로써 달성할 수 있습니다. 이는 고도로 어노테이션된 코드의 경우 프레임이 모든 객체를 활성 상태로 유지하므로 엄청나게 비쌀 것입니다.

클래스 수준 스코프를 처리하려면 람다 접근 방식은 인터프리터에 새로운 종류의 셀(cell)을 요구할 것입니다. 이는 __annotations__에 나타날 수 있는 타입의 수를 증식시킬 뿐만 아니라, 문자열만큼 내부를 들여다볼 수 없을 것입니다.

중첩 클래스의 경우, 정의 시점에 효과적인 “globals” 및 “locals”를 가져오는 기능은 typing.get_type_hints()에 의해 제공됩니다.

어노테이션이 로컬 변수를 사용해야 하는 클래스 또는 함수를 생성하는 함수는 컴파일러에 의존하지 않고 생성된 객체의 __annotations__ 딕셔너리를 직접 채울 수 있습니다.

클래스에도 로컬 상태 사용 금지 (Disallowing local state usage for classes, too)

이 PEP는 원래 어노테이션 내의 이름을 모듈 수준 스코프의 이름으로만 제한할 것을 제안했으며, 클래스의 경우에도 마찬가지였습니다. 저자는 이것이 로컬 이름과 모듈 수준 이름 간의 충돌을 포함하여 이름 해결을 모호하지 않게 만든다고 주장했습니다.

이 아이디어는 결국 클래스의 경우 기각되었습니다. 대신 typing.get_type_hints()가 클래스 수준 어노테이션이 필요한 경우 로컬 네임스페이스를 올바르게 채우도록 수정되었습니다.

이 아이디어를 기각한 이유는 Python에서 스코프(scoping)가 작동하는 방식의 직관에 어긋나고, 기존 타입 어노테이션을 충분히 깨뜨려 전환을 번거롭게 만들 것이기 때문입니다. 마지막으로, 클래스 데코레이터가 타입 어노테이션을 평가할 수 있으려면 로컬 스코프 접근이 필요합니다. 이는 클래스 데코레이터가 외부 스코프에서 클래스가 이름을 받기 전에 적용되기 때문입니다.

문자열 리터럴 형태를 위한 새 딕셔너리 도입 (Introducing a new dictionary for the string literal form instead)

Yury Selivanov는 다음 아이디어를 공유했습니다: 함수에 __annotations_text__라는 새로운 특별 속성을 추가합니다. __annotations__를 지연 동적 매핑으로 만들어, __annotations_text__의 해당 키에서 표현식을 Just-In-Time으로 평가합니다.

이 아이디어는 하위 호환성 문제를 해결하고 새 __future__ import의 필요성을 제거하기 위한 것이었습니다. 그러나 지연 평가는 어노테이션이 접근할 수 있는 상태를 변경합니다. 지연 평가가 전방 참조 문제를 해결하지만, 함수 수준의 locals에 더 이상 접근할 수 없게 만듭니다. 이것만으로도 하위 호환성 위반의 원인이 되며, 이는 폐기 기간을 정당화합니다.

__future__ import는 새로운 기능을 선택한다는 명확하고 명시적인 지표입니다. 또한 외부 도구가 이전 방식 또는 새 방식을 사용하는 Python 파일 간의 차이를 쉽게 인식하게 합니다.

마지막으로, get_type_hints()가 나중에 사용되는 경우 __annotations__에서의 Just-In-Time 평가는 불필요한 단계입니다.

-O 옵션으로 어노테이션 제거 (Dropping annotations with -O)

이 PEP의 목적을 위해 이것이 만족스럽지 않은 두 가지 이유가 있습니다.

  1. 이것은 런타임 비용만 다루며, 전방 참조는 다루지 않습니다. 라이브러리 관리자는 전방 참조를 사용할 수 없을 것입니다. 이는 라이브러리 사용자가 새로운 가상의 -O 스위치를 사용하도록 강요하기 때문입니다.
  2. 이것은 “목욕물과 함께 아기를 버리는” 격입니다. 이제 런타임 어노테이션 사용은 전혀 수행할 수 없게 됩니다. PEP 557은 런타임에 타입 어노테이션을 평가하는 것이 유용한 최근 개발의 한 예입니다.

그럼에도 불구하고, 어노테이션을 제거하는 세분화된 -O 옵션은 기존 -O 동작(docstring 및 assert 문 제거)과 개념적으로 호환되므로 미래에 가능할 수 있습니다. 이 PEP는 이 아이디어를 무효화하지 않습니다.

어노테이션의 문자열 리터럴을 __annotations__에 그대로 전달 (Passing string literals in annotations verbatim to annotations)

이 PEP는 원래 문자열 리터럴의 내용을 __annotations__의 해당 키 아래에 직접 저장할 것을 제안했습니다. 이는 런타임 타입 검사기(runtime type checkers)를 위한 지원을 단순화하기 위한 것이었습니다. Mark Shannon은 이 아이디어가 문자열이 타입 어노테이션의 일부일 때의 상황을 처리하지 못하므로 결함이 있다고 지적했습니다. 이 아이디어의 불일치성은 항상 명백했지만, 어쨌든 문자열의 이중 래핑(double-wrapping) 사례를 완전히 방지하지 못했기 때문에 가치가 없었습니다.

__future__ import의 이름을 더 길게 만들기 (Making the name of the future import more verbose)

다음 import를 요구하는 대신:

from __future__ import annotations

이 PEP는 기능을 더 명시적으로 호출할 수 있습니다. 예를 들어, string_annotations, stringify_annotations, annotation_strings, annotations_as_strings, lazy_annotations, static_annotations 등이 있습니다.

이러한 이름의 문제는 매우 장황하다는 것입니다. lazy_annotations를 제외한 각 이름은 Python에서 가장 긴 미래 기능 이름이 될 것입니다. 타이핑하기 길고 단어 하나로 된 형태보다 기억하기 어렵습니다.

지나치게 일반적으로 들리지만 실제로는 사용자에게 무엇을 하는지 명백했던 __future__ import 이름의 선례가 있습니다:

from __future__ import division

이전 논의 (Prior discussion)

PEP 484에서 (In PEP 484)

전방 참조 문제는 PEP 484가 처음 초안될 때 논의되었으며, 문서에 다음과 같은 진술을 이끌어냈습니다:

__future__ import를 통해 주어진 모듈의 모든 어노테이션을 문자열 리터럴로 바꿀 수 있는 절충안이 가능하며, 다음과 같습니다:

from __future__ import annotations
class ImSet:
    def add(self, a: ImSet) -> List[ImSet]: ...
assert ImSet.add.__annotations__ == {
    'a': 'ImSet',
    'return': 'List[ImSet]'
}

이러한 __future__ import 문은 별도의 PEP에서 제안될 수 있습니다.

python/typing#400

이 문제는 typing 모듈의 GitHub 프로젝트, Issue 400에서 심층적으로 논의되었습니다. 거기서의 문제 설명은 typing에서 제네릭 타입(generic types)을 임포트해야 하는 것에 대한 비판을 포함합니다. 이는 초보자에게 혼란을 주는 경향이 있습니다:

# 왜 이렇게 해야 하는가:
from typing import List, Set
def dir(o: object = ...) -> List[str]: ...
def add_friends(friends: Set[Friend]) -> None: ...

# 이렇게는 안 되는가:
def dir(o: object = ...) -> list[str]: ...
def add_friends(friends: set[Friend]) -> None ...

# 왜 이렇게 해야 하는가:
up_to_ten = list(range(10))
friends = set()

# 이렇게는 안 되는가:
from typing import List, Set
up_to_ten = List[int](range(10))
friends = Set[Friend]()

typing의 유용성은 흥미로운 문제이지만, 이 PEP의 범위 밖입니다. 특히 PEP 484에서 표준화된 typing 구문의 확장은 각각의 PEP와 승인을 요구할 것입니다.

Issue 400은 궁극적으로 어노테이션의 평가를 지연하고 __annotations__에 문자열로 유지할 것을 제안하며, 이는 이 PEP가 명시하는 바와 같습니다. 이 아이디어는 잘 받아들여졌습니다. Ivan Levkivskyi는 __future__ import 사용을 지지했고, compile.c에서 AST를 역파싱(unparsing)할 것을 제안했습니다. Jukka Lehtosalo는 타입이 어노테이션 외부에서 사용되는 전방 참조의 일부 경우가 있으며, 지연 평가는 도움이 되지 않을 것이라고 지적했습니다. 이러한 경우에는 문자열 리터럴 표기법을 계속 사용해야 할 것입니다. 이러한 사례들은 이 PEP의 “Forward References” 섹션에서 간략하게 논의됩니다.

이 문제에 대한 가장 큰 논란은 Guido van Rossum의 우려였습니다. 그는 어노테이션 표현식을 다시 문자열 형태로 토큰화하지 않는 것은 Python 프로그래밍 언어에 전례가 없으며 해킹적인 우회책처럼 느껴진다고 말했습니다. 그는 다음과 같이 말했습니다:

“하나 생각나는 것은 언어에 대한 매우 무작위적인 변경이라는 것입니다. 표현식의 지연 실행을 나타내는 더 간결한 방법이 있으면 유용할 수 있습니다 ( lambda:보다 적은 구문을 사용하여). 그러나 이 특정 사용 사례에 대한 해결책이 이미 존재하고 매우 최소한의 구문을 요구하는데, 타입 어노테이션의 사용 사례가 언어를 먼저 변경해야 할 만큼 그렇게 중요한 이유는 무엇일까요 (더 일반적인 해결책을 제안하기보다는)?”

결국 Ethan Smith와 schollii는 PyCon US에서 수집된 피드백이 전방 참조 상태를 수정해야 함을 시사한다고 말했습니다. Guido van Rossum은 __future__ 아이디어로 돌아갈 것을 제안하며, 남용을 방지하기 위해 어노테이션이 구문적으로 유효하고 런타임에 올바르게 평가되는 것이 중요하다고 지적했습니다.

python-ideas에서의 첫 번째 초안 논의 (First draft discussion on python-ideas)

논의는 주로 두 스레드, 즉 원래 발표와 PEP 563 and expensive backwards compatibility라는 후속 스레드에서 이루어졌습니다.

PEP는 상당히 긍정적인 피드백을 받았습니다 (4명이 강력하게 찬성, 2명이 우려와 함께 찬성, 2명이 반대). 이전 스레드에서 가장 큰 우려의 목소리는 Steven D’Aprano의 검토였습니다. 그는 PEP의 문제 정의가 하위 호환성을 깨뜨리는 것을 정당화하지 못한다고 주장했습니다. 이 답변에서 Steven은 주로 Python이 더 이상 로컬 함수/클래스 상태에 의존하는 어노테이션의 평가를 지원하지 않는 것에 대해 우려하는 것처럼 보였습니다.

몇몇 사람들은 어노테이션을 비-타이핑 목적으로 사용하는 라이브러리가 있다는 우려를 표명했습니다. 그러나 언급된 라이브러리 중 어느 것도 이 PEP에 의해 무효화되지 않을 것입니다. 그들은 어노테이션에 대해 올바른 globalslocals를 설정하여 eval()을 호출하는 새로운 요구 사항에 적응해야 합니다.

globalslocals가 올바르게 설정되어야 한다는 이 세부 사항은 많은 논평자들에 의해 언급되었습니다. Alyssa (Nick) Coghlan은 어노테이션을 문자열 대신 람다로 바꾸는 벤치마크를 수행했지만, 안타깝게도 이는 현재 상황보다 런타임에 훨씬 느리다는 것이 입증되었습니다.

후자의 스레드는 Jim J. Jewett에 의해 시작되었는데, 그는 어노테이션을 제대로 평가하는 능력이 중요한 요구 사항이며, 이와 관련하여 하위 호환성이 가치 있다고 강조했습니다. 그는 논의 후 어노테이션의 부작용(side effects)이 코드 스멜(code smell)이며, 평가를 수행할지 말지를 선택하는 모달 지원은 지저분한 해결책이라고 인정했습니다. 그의 가장 큰 우려는 전역 및 로컬 스코프의 평가 제한으로 인해 발생하는 기능 손실이었습니다.

Alyssa Coghlan은 PEP의 일부 평가 제한이 영리한 평가 도우미(evaluation helper) 구현으로 해제될 수 있으며, 이는 클래스 데코레이터 형태에서도 자기 참조 클래스(self-referencing classes)를 해결할 수 있다고 지적했습니다. 그녀는 PEP가 표준 라이브러리에 이 도우미 함수를 제공해야 한다고 제안했습니다.

python-dev에서의 두 번째 초안 논의 (Second draft discussion on python-dev)

논의는 주로 발표 스레드에서 이루어졌으며, Mark Shannon의 게시물 아래에서 간략한 논의가 이어졌습니다.

Steven D’Aprano는 PEP가 제안한 변경 후 어노테이션에 오타가 허용되는 것이 허용되는지 우려했습니다. Brett Cannon은 타입 검사기 및 다른 정적 분석기(린터나 프로그래밍 텍스트 편집기 등)가 이러한 유형의 오류를 잡아낼 것이라고 응답했습니다. Jukka Lehtosalo는 이 상황이 함수 본문의 이름이 함수가 호출될 때까지 해결되지 않는 방식과 유사하다고 덧붙였습니다.

논의의 주요 주제는 Alyssa Coghlan의 어노테이션을 “thunk form”으로 저장하자는 제안이었습니다. 즉, 클래스 수준 스코프에 접근할 수 있고 (호출 시 스코프 사용자 정의를 허용하는) 특수화된 람다 형태였습니다. 그는 이에 대한 가능한 설계를 제시했습니다 (간접 속성 셀). 이는 나중에 Lisp의 “특수 형태”와 동등한 것으로 간주되었습니다. Guido van Rossum은 이러한 종류의 기능이 12주 안에 (즉, Python 3.7 베타 동결 전에) 안전하게 구현될 수 있을지 우려를 표했습니다.

얼마 후, 문자열 형태 지지자와 thunk 형태 지지자 간의 분열점은 실제로 어노테이션이 일반적인 구문 요소로 인식되어야 하는지 아니면 타입 검사 사용 사례에 묶인 것으로 인식되어야 하는지에 관한 것임이 분명해졌습니다.

마침내 Guido van Rossum은 thunk 아이디어가 인터프리터에 새로운 빌딩 블록을 요구할 것이라는 사실에 근거하여 이를 거부한다고 선언했습니다. 이 블록은 어노테이션에 노출되어 __annotations__에 저장될 수 있는 값의 유형(임의 객체, 문자열, 이제는 thunk)을 증식시킬 것입니다. 또한 thunk는 문자열만큼 내부를 들여다볼 수 없습니다. 가장 중요하게, Guido van Rossum은 어노테이션의 사용을 점진적으로 정적 타이핑(선택적 런타임 구성 요소 포함)으로 제한하는 데 명시적인 관심을 표명했습니다.

Alyssa Coghlan도 PEP 563에 설득되었고, 즉시 __future__ import 이름에 대한 의무적인 “bike shedding” 세션을 시작했습니다. 많은 토론자들은 annotations가 기능 이름으로는 지나치게 광범위한 이름처럼 보인다는 데 동의했습니다. Guido van Rossum은 잠시 string_annotations라고 부르기로 결정했지만, division이 명확한 의미를 가진 광범위한 이름의 선례라고 주장하며 마음을 바꿨습니다.

Mark Shannon이 논의에서 제안한 PEP에 대한 최종 개선 사항은 문자열 리터럴을 __annotations__에 그대로 전달하려는 유혹을 거부하는 것이었습니다.

정적 타이핑의 런타임 패널티에 대한 부차적인 논의 스레드가 시작되었으며, typing 모듈의 임포트 시간(종속성 없이 re와 비슷하고, 종속성을 포함하면 re보다 세 배 무겁다)과 같은 주제가 다루어졌습니다.

감사 (Acknowledgements)

이 문서는 Guido van Rossum, Jukka Lehtosalo, Ivan Levkivskyi의 귀중한 의견, 격려 및 조언 없이는 완성될 수 없었습니다. 구현은 Serhiy Storchaka가 철저히 검토하여 버그, 낮은 가독성, 성능 문제 등 모든 종류의 문제를 발견했습니다.

이 문서는 공개 도메인에 배포되었습니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments