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

Python 개선 제안 한국어 번역

PEP 563 – 어노테이션 평가 지연

Author:
Łukasz Langa <lukasz at python.org>
Discussions-To:
Python-Dev list
Status:
Superseded
Type:
Standards Track
Topic:
Typing
Created:
08-Sep-2017
Python-Version:
3.7
Post-History:
01-Nov-2017, 21-Nov-2017
Superseded-By:
649, 749
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

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

결론

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

초록

PEP 3107은 함수 어노테이션 문법을 도입했지만, 그 의미는 의도적으로 정의하지 않은 채 남겨 두었습니다. PEP 484는 어노테이션에 타입 힌트라는 표준 의미를 부여했습니다. PEP 526은 변수 어노테이션을 정의하면서, 이를 타입 힌트 사용 사례와 명시적으로 연결했습니다.

이 PEP는 함수 어노테이션과 변수 어노테이션이 더 이상 함수 정의 시점에 평가되지 않도록 변경할 것을 제안합니다. 대신 어노테이션은 문자열 형태로 __annotations__에 보존됩니다.

이 변경은 Python 3.7의 __future__ 임포트를 시작으로 점진적으로 도입되고 있습니다.

근거와 목표

PEP 3107은 함수 정의의 각 부분에 임의의 어노테이션을 달 수 있도록 지원을 추가했습니다. 기본값과 마찬가지로 어노테이션은 함수 정의 시점에 평가됩니다. 이로 인해 타입 힌트 사용 사례에서 여러 가지 문제가 발생합니다:

  • 전방 참조: 타입 힌트에 아직 정의되지 않은 이름이 포함되어 있으면 그 정의를 문자열 리터럴로 표현해야 합니다;
  • 타입 힌트는 모듈 임포트 시점에 실행되며, 여기에는 계산 비용이 듭니다.

어노테이션의 평가를 지연하면 두 문제가 모두 해결됩니다. 참고: PEP 649는 위 문제들에 대한 대안적인 해결책을 제안하며, 이로 인해 이 PEP는 대체될 위험에 처해 있습니다.

목표가 아닌 것

PEP 484 및 PEP 526에서와 마찬가지로, Python은 앞으로도 동적 타입 언어로 남을 것이며, 저자들은 관례로서라도 타입 힌트를 의무화할 생각이 전혀 없다는 점을 강조해야 합니다.

이 PEP는 타입 어노테이션에서의 전방 참조 문제를 해결하기 위한 것입니다. 어노테이션 밖에서는 여전히 전방 참조에 문자열 리터럴을 사용해야 하는 경우가 있습니다. 이러한 경우는 이 문서의 뒷부분에 나열되어 있습니다.

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

어노테이션의 타입 외 용도

어노테이션은 여전히 타입 검사 외의 임의의 용도로 사용할 수 있지만, 이 PEP의 설계는 그 선행 PEP(PEP 484와 PEP 526)와 마찬가지로 주로 타입 힌트 사용 사례를 동기로 삼고 있다는 점을 언급해 둘 필요가 있습니다.

Python 3.8에서 PEP 484는 잠정(provisional) 상태를 벗어나게 됩니다. PEP 544, PEP 557, PEP 560 같은 Python 프로그래밍 언어의 다른 개선 사항들은 PEP 484에서 정의한 타입 어노테이션과 typing 모듈에 의존하므로 이미 이를 기반으로 구축되고 있습니다. 실제로 PEP 484가 Python 3.7에서 잠정 상태로 유지되는 이유는, 앞서 언급한 개선 사항 중 일부에 필요한 빠른 발전을 한 릴리스 주기 더 가능하게 하기 위해서입니다.

이를 고려하면, 앞서 언급한 PEP들과 호환되지 않는 어노테이션 사용은 폐지 예정으로 간주해야 합니다.

구현

이 PEP에 따라 함수와 변수 어노테이션은 더 이상 정의 시점에 평가되지 않습니다. 대신 문자열 형태가 각각의 __annotations__ 딕셔너리에 보존됩니다. 정적 타입 검사기의 동작에는 아무런 차이가 없지만, 런타임에 어노테이션을 사용하는 도구는 지연된 평가를 수행해야 합니다.

문자열 형태는 컴파일 단계에서 AST로부터 얻으므로, 소스의 정확한 서식이 그대로 보존되지 않을 수 있습니다. 참고: 어노테이션이 이미 문자열 리터럴이었더라도 여전히 문자열로 감싸집니다.

어노테이션은 리터럴 문자열로 전달될 때에도(즉, compile(literal, '', 'eval')) 문법적으로 유효한 Python 표현식이어야 합니다. 지역 이름을 사용하는 지연된 평가는 신뢰할 수 없으므로, 어노테이션에는 모듈 스코프에 있는 이름만 사용할 수 있습니다(typing.get_type_hints()가 해석하는 클래스 수준 이름만이 유일한 예외입니다).

PEP 526에 따라 지역 변수 어노테이션은 함수의 클로저 밖에서 접근할 수 없으므로 전혀 평가되지 않는다는 점에 유의하십시오.

Python 3.7에서 향후 동작 활성화하기

위에서 설명한 기능은 Python 3.7부터 다음과 같은 특수 임포트를 사용하여 활성화할 수 있습니다:

from __future__ import annotations

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

런타임에 타입 힌트 해석하기

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

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

다른 목적으로 어노테이션을 사용하는 코드의 경우, 일반적인 eval(ann, globals, locals) 호출만으로 어노테이션을 충분히 해석할 수 있습니다.

두 경우 모두 전역 및 지역 네임스페이스가 지연된 평가에 어떤 영향을 미치는지 고려하는 것이 중요합니다. 어노테이션은 더 이상 정의 시점에 평가되지 않으며, 더 중요하게는 그것이 정의된 같은 스코프에서 평가되지 않습니다. 따라서 일반적으로 어노테이션에서 지역 상태를 사용하는 것은 더 이상 불가능합니다. 전역 네임스페이스의 경우, 어노테이션이 정의된 모듈이 지연된 평가를 위한 올바른 컨텍스트입니다.

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

eval()을 실행할 때, 전역 네임스페이스의 값은 다음과 같은 방법으로 수집할 수 있습니다:

  • 함수 객체는 __globals__라는 속성에 각자의 전역 네임스페이스에 대한 참조를 보관합니다;
  • 클래스는 자신이 정의된 모듈의 이름을 보관하며, 이를 사용하여 해당 전역 네임스페이스를 가져올 수 있습니다:
    cls_globals = vars(sys.modules[SomeClass.__module__])
    

    모든 __annotations__를 평가하려면 베이스 클래스에 대해서도 이 작업을 반복해야 한다는 점에 유의하십시오.

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

함수의 경우 호출 시점의 스택 프레임이 이미 존재하지 않을 가능성이 매우 높으므로 localns의 값을 신뢰성 있게 가져올 수 없습니다.

클래스의 경우, localns는 해당 클래스와 그 베이스 클래스들의 vars를 (메서드 결정 순서에 따라) 연결하여 구성할 수 있습니다. 슬롯은 클래스가 정의된 후에야 채워질 수 있으므로, 이 목적을 위해 슬롯을 참조할 필요는 없습니다.

런타임 어노테이션 해석과 클래스 데코레이터

현재 클래스의 어노테이션을 해석해야 하는 메타클래스와 클래스 데코레이터는 현재 클래스의 이름을 사용하는 어노테이션에서 실패합니다. 예시:

def class_decorator(cls):
    annotations = get_type_hints(cls)  # raises NameError on 'C'
    print(f'Annotations for {cls}: {annotations}')
    return cls

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

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

런타임 어노테이션 해석과 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
    ...

이 접근 방식은 순환 임포트를 처리할 때도 유용합니다.

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

하위 호환성

이것은 하위 호환되지 않는 변경입니다. 임의의 객체가 어노테이션에 직접 존재한다는 것에 의존하는 애플리케이션은 typing.get_type_hints()나 eval()을 사용하지 않으면 동작하지 않게 됩니다.

함수 정의 시점의 지역 변수에 의존하는 어노테이션은 나중에 해석할 수 없습니다. 예시:

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은 이러한 어노테이션을 금지하려고 하지 않습니다.

중첩 클래스와 그 각각의 상태를 사용하는 어노테이션은 여전히 유효합니다. 지역 이름이나 완전한 정규화된 이름을 사용할 수 있습니다. 예시:

class C:
    field = 'c_field'
    def method(self) -> C.field:  # this is OK
        ...

    def method(self) -> field:  # this is OK
        ...

    def method(self) -> C.D:  # this is OK
        ...

    def method(self) -> D:  # this is OK
        ...

    class D:
        field2 = 'd_field'
        def method(self) -> C.D.field2:  # this is OK
            ...

        def method(self) -> D.field2:  # this FAILS, class D is local to C
            ...                        # and is therefore only available
                                       # as C.D. This was already true
                                       # before the PEP.

        def method(self) -> field2:  # this is OK
            ...

        def method(self) -> field:  # this FAILS, field is local to C and
                                    # is therefore not visible to D unless
                                    # accessed as C.field. This was already
                                    # true before the PEP.

문법적으로 유효한 표현식이 아닌 어노테이션이 있으면 컴파일 시점에 SyntaxError가 발생합니다. 그러나 그 시점에는 이름이 해석되지 않으므로, 사용된 이름이 올바른지 여부는 검증하지 않습니다.

폐지 정책

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

참고: 이것이 결국 기본 동작이 될지는 PEP 649에 대한 결정이 내려지지 않아 현재로서는 불분명합니다. 어떤 경우든, 어노테이션의 즉시 평가에 의존하는 어노테이션 사용은 두 제안 모두와 호환되지 않으며 더 이상 지원되지 않습니다.

전방 참조

모듈에서 이름이 정의되기 전에 의도적으로 그 이름을 사용하는 것을 전방 참조라고 합니다. 이 절에서는 if TYPE_CHECKING: 블록 안에서 임포트되거나 정의된 모든 이름도 전방 참조라고 부르겠습니다.

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

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

구체적인 경우에 따라, 위에 나열된 경우 중 일부는 해당 사용 부분을 if TYPE_CHECKING: 블록 안에 두어 우회할 수 있습니다. 이 방법은 런타임에 사용할 수 있어야 하는 코드에는 통하지 않으며, 특히 베이스 클래스와 캐스팅이 그렇습니다. 네임드 튜플의 경우, Python 3.6에서 도입된 새로운 클래스 정의 문법을 사용하면 문제가 해결됩니다.

일반적으로 모든 전방 참조 문제를 해결하려면 Python에서 모듈 인스턴스화를 수행하는 방식을 현재의 단일 패스 하향식 모델에서 다른 방식으로 바꿔야 합니다. 이는 언어의 큰 변경이 될 것이며 이 PEP의 범위를 벗어납니다.

기각된 아이디어

어노테이션을 정의할 때 함수 지역 상태를 사용할 수 있는 기능 유지

평가를 지연하면 어노테이션이 생성된 프레임에 대한 참조를 유지해야 합니다. 예를 들어 모든 어노테이션을 문자열 대신 람다로 저장하는 방식으로 이를 구현할 수 있습니다.

프레임이 자신의 모든 객체를 살아 있는 상태로 유지하므로, 어노테이션이 많은 코드에서는 감당할 수 없을 만큼 비용이 커집니다. 여기에는 다시는 접근되지 않을 객체가 대부분을 차지합니다.

클래스 수준 스코프를 다룰 수 있으려면, 람다 방식은 인터프리터에 새로운 종류의 셀을 필요로 합니다. 이는 __annotations__에 나타날 수 있는 타입의 수를 늘릴 뿐만 아니라, 문자열만큼 인트로스펙션하기도 쉽지 않습니다.

중첩 클래스의 경우, 정의 시점의 실질적인 “globals”와 “locals”를 얻는 기능은 typing.get_type_hints()가 제공한다는 점에 유의하십시오.

함수가 지역 변수를 사용해야 하는 어노테이션을 가진 클래스나 함수를 생성하는 경우, 컴파일러에 의존하지 않고 생성된 해당 객체의 __annotations__ 딕셔너리를 직접 채울 수 있습니다.

클래스에서도 지역 상태 사용 금지

이 PEP는 원래 클래스를 포함하여 어노테이션 내의 이름을 모델 수준 스코프의 이름으로만 제한하도록 제안했습니다. 작성자는 이렇게 하면 지역 이름과 모듈 수준 이름이 충돌하는 경우를 포함하여 이름 해석이 모호하지 않게 된다고 주장했습니다.

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

이 아이디어를 거부한 이유는 Python에서 스코프가 동작하는 방식에 대한 직관에 어긋나고, 기존 타입 어노테이션을 상당수 깨뜨려 전환을 번거롭게 만들기 때문이었습니다. 마지막으로, 클래스 데코레이터가 타입 어노테이션을 평가할 수 있으려면 지역 스코프에 접근할 수 있어야 합니다. 이는 클래스 데코레이터가 외부 스코프에서 클래스가 이름을 받기 전에 적용되기 때문입니다.

대신 문자열 리터럴 형식을 위한 새 딕셔너리 도입

Yury Selivanov는 다음과 같은 아이디어를 공유했습니다.

  1. 함수에 새로운 특수 속성 __annotations_text__를 추가합니다.
  2. __annotations__를 지연 동적 매핑으로 만들어, __annotations_text__의 해당 키에 있는 표현식을 필요한 시점에 즉시(just-in-time) 평가합니다.

이 아이디어는 하위 호환성 문제를 해결하여 새로운 __future__ 임포트가 필요 없도록 하기 위한 것입니다. 안타깝게도 이것만으로는 충분하지 않습니다. 지연된 평가는 어노테이션이 접근할 수 있는 상태를 바꿉니다. 지연된 평가는 전방 참조 문제를 해결하지만, 동시에 함수 수준의 지역 변수에 더 이상 접근할 수 없게 만듭니다. 이것만으로도 하위 호환성을 깨뜨리는 원인이 되며, 폐지 예고 기간을 두어야 할 충분한 근거가 됩니다.

__future__ 임포트는 새로운 기능을 사용하기로 선택했음을 나타내는 분명하고 명시적인 표시입니다. 또한 외부 도구가 이전 방식을 사용하는 Python 파일과 새로운 방식을 사용하는 Python 파일의 차이를 아주 쉽게 알아볼 수 있게 해 줍니다. 전자의 경우 해당 도구는 지역 상태에 접근할 수 있다는 것을 인식하고, 후자의 경우 전방 참조가 허용된다는 것을 인식합니다.

마지막으로, 나중에 get_type_hints()를 사용한다면 __annotations__에서의 필요 시점 평가는 불필요한 단계입니다.

-O로 어노테이션 제거하기

이 방법이 이 PEP의 목적에 부합하지 않는 이유는 두 가지입니다.

첫째, 이 방법은 런타임 비용만 해결할 뿐 전방 참조는 해결하지 못하므로, 전방 참조는 여전히 소스 코드에서 안전하게 사용할 수 없습니다. 라이브러리 유지 관리자는 전방 참조를 결코 사용할 수 없게 됩니다. 전방 참조를 사용하면 라이브러리 사용자에게 이 새로운 가상의 -O 스위치 사용을 강제하게 되기 때문입니다.

둘째, 이 방법은 빈대 잡으려다 초가삼간 태우는 격입니다. 이제 런타임에 어노테이션을 전혀 사용할 수 없게 됩니다. PEP 557은 런타임에 타입 어노테이션을 평가하는 것이 유용한 최근 개발 사례 중 하나입니다.

그렇기는 하지만, 어노테이션을 제거하는 세분화된 -O 옵션은 개념적으로 기존의 -O 동작(독스트링과 assert 문 제거)과 호환되므로 향후에 도입될 가능성이 있습니다. 이 PEP는 그 아이디어를 무효화하지 않습니다.

어노테이션의 문자열 리터럴을 __annotations__에 그대로 전달하기

이 PEP는 원래 문자열 리터럴의 내용을 __annotations__의 해당 키 아래에 직접 저장할 것을 제안했습니다. 이는 런타임 타입 검사기 지원을 단순화하기 위한 것이었습니다.

Mark Shannon은 문자열이 타입 어노테이션의 일부에 불과한 경우를 처리하지 못하므로 이 아이디어에 결함이 있다고 지적했습니다.

이 방식의 비일관성은 처음부터 분명했지만, 어차피 문자열이 이중으로 감싸지는 경우를 완전히 막지도 못한다는 점을 고려하면 그럴 만한 가치가 없습니다.

future 임포트의 이름을 더 장황하게 만들기

다음 임포트를 요구하는 대신:

from __future__ import annotations

이 PEP는 기능을 더 명시적인 이름으로 부를 수도 있었습니다. 예를 들면 string_annotations, stringify_annotations, annotation_strings, annotations_as_strings, lazy_annotations, static_annotations 등입니다.

이러한 이름들의 문제는 매우 장황하다는 것입니다. lazy_annotations를 제외한 나머지는 모두 Python에서 가장 긴 future 기능 이름이 됩니다. 이 이름들은 입력하기에 길고, 한 단어로 된 형태보다 기억하기 어렵습니다.

지나치게 일반적으로 들리지만 실제로는 그 기능이 무엇을 하는지 사용자에게 분명했던 future 임포트 이름의 선례가 있습니다:

from __future__ import division

이전 논의

PEP 484에서

전방 참조 문제는 PEP 484의 초안이 처음 작성될 때 논의되었으며, 그 결과 문서에 다음과 같은 문장이 포함되었습니다:

__future__ 임포트로 특정 모듈의 모든 어노테이션을 문자열 리터럴로 바꿀 수 있게 하는 절충안이 가능합니다. 예를 들면 다음과 같습니다:
from __future__ import annotations

class ImSet:
    def add(self, a: ImSet) -> List[ImSet]: ...

assert ImSet.add.__annotations__ == {
    'a': 'ImSet', 'return': 'List[ImSet]'
}

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

python/typing#400

이 문제는 typing 모듈의 GitHub 프로젝트에서 Issue 400을 통해 상세히 논의되었습니다. 그곳의 문제 제기에는 제네릭 타입을 쓰려면 typing에서 임포트해야 한다는 점에 대한 비판이 포함되어 있습니다. 이는 초보자에게 혼란을 주기 쉽습니다:

왜 이것은 되는데:
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]()

타입 지정의 사용성은 흥미로운 문제이지만, 이 PEP의 범위를 벗어납니다. 구체적으로, PEP 484에서 표준화된 타입 지정 문법을 확장하려면 각각 별도의 PEP와 승인이 필요합니다.

Issue 400은 결국 이 PEP에서 명시하는 것과 똑같이, 어노테이션의 평가를 연기하고 어노테이션을 __annotations__에 문자열로 보관할 것을 제안합니다. 이 아이디어는 좋은 반응을 얻었습니다. Ivan Levkivskyi는 __future__ 임포트를 사용하는 방안을 지지했으며, compile.c에서 AST를 역파싱할 것을 제안했습니다. Jukka Lehtosalo는 타입이 어노테이션 밖에서 사용되는 전방 참조 사례가 일부 있으며, 이러한 경우에는 평가 연기가 도움이 되지 않는다고 지적했습니다. 이러한 경우에는 여전히 문자열 리터럴 표기법을 사용해야 합니다. 이러한 경우는 이 PEP의 “전방 참조” 절에서 간략히 다룹니다.

이 이슈에서 가장 큰 논쟁거리는 어노테이션 표현식을 다시 문자열 형태로 역토큰화하는 것은 Python 프로그래밍 언어에 전례가 없으며 편법적인 우회책처럼 느껴진다는 Guido van Rossum의 우려였습니다. 그는 다음과 같이 말했습니다:

먼저 떠오르는 것은 이것이 언어에 대한 매우 임의적인 변경이라는 점입니다. 표현식의 지연 실행을 나타내는 더 간결한 방법(lambda:보다 적은 문법을 사용하는 방법)이 있다면 유용할 수도 있습니다. 하지만 이 특정한 사용 사례에 대해 이미 아주 최소한의 문법만 요구하는 해결책이 있는데, 왜 타입 어노테이션이라는 사용 사례가 (더 일반적인 해결책을 제안하는 대신) 그곳에서 먼저 언어를 변경할 만큼 그토록 중요하다는 것입니까?

결국 Ethan Smith와 schollii는 PyCon US에서 수집한 피드백을 보면 전방 참조의 현 상태를 바로잡을 필요가 있다는 의견을 밝혔습니다. Guido van Rossum은 __future__ 아이디어로 돌아갈 것을 제안하면서, 남용을 막으려면 어노테이션이 문법적으로 유효하면서도 런타임에 올바르게 평가되도록 유지하는 것이 중요하다고 지적했습니다.

python-ideas에서의 첫 번째 초안 논의

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

이 PEP는 꽤 호의적인 반응을 얻었습니다(적극 찬성 4명, 우려와 함께 찬성 2명, 반대 2명). 앞선 스레드에서 가장 크게 우려를 표한 것은 Steven D’Aprano의 리뷰였습니다. 그는 이 PEP의 문제 정의만으로는 하위 호환성을 깨뜨리는 것이 정당화되지 않는다고 말했습니다. 이 답변에서 Steven은 함수나 클래스의 지역 상태에 의존하는 어노테이션을 Python이 더 이상 평가하지 못하게 된다는 점을 주로 우려하는 것으로 보였습니다.

몇몇 사람은 타입 지정이 아닌 목적으로 어노테이션을 사용하는 라이브러리가 있다는 우려를 표했습니다. 그러나 언급된 라이브러리 중 이 PEP 때문에 동작하지 않게 되는 라이브러리는 하나도 없습니다. 다만 이들 라이브러리는 globals와 locals를 올바르게 설정하고 어노테이션에 eval()을 호출해야 한다는 새로운 요구 사항에 맞게 수정되어야 합니다.

globals와 locals가 올바르게 설정되어야 한다는 이 세부 사항은 여러 의견 제시자가 짚었습니다. Alyssa (Nick) Coghlan은 어노테이션을 문자열 대신 람다로 바꾸는 방식을 벤치마크했지만, 안타깝게도 이 방식은 런타임에 현재 방식보다 훨씬 느린 것으로 드러났습니다.

후자의 스레드는 Jim J. Jewett이 시작했으며, 그는 어노테이션을 제대로 평가할 수 있는 능력이 중요한 요구 사항이고 그 점에서 하위 호환성이 가치 있다고 강조했습니다. 어느 정도 논의가 진행된 후 그는 어노테이션 안의 부작용이 코드 스멜이라는 점을 인정했습니다. 또한 평가를 수행하거나 수행하지 않도록 모드를 두어 지원하는 방식은 지저분한 해결책이라는 점도 인정했습니다. 그의 가장 큰 우려는 여전히 전역 및 지역 스코프에 대한 평가 제약에서 비롯되는 기능 손실이었습니다.

Alyssa Coghlan은 평가 헬퍼를 영리하게 구현하면 이 PEP의 평가 제약 중 일부를 없앨 수 있다고 지적했습니다. 그러한 헬퍼는 클래스 데코레이터 형태에서도 자기 참조 클래스 문제를 해결할 수 있습니다. 그녀는 PEP가 이 헬퍼 함수를 표준 라이브러리에서 제공해야 한다고 제안했습니다.

python-dev에서의 두 번째 초안 논의

논의는 주로 공지 스레드에서 이루어졌으며, 이어서 Mark Shannon의 게시물 아래에서 짧은 논의가 있었습니다.

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

주요 논의 주제는 Alyssa Coghlan의 제안이었습니다. 어노테이션을 “썽크 형태”, 다시 말해 클래스 수준 스코프에 접근할 수 있는(그리고 호출 시점에 스코프를 사용자 정의할 수 있게 하는) 특수한 람다로 저장하자는 제안이었습니다. 그는 이를 위한 설계안(간접 속성 셀)을 제시했습니다. 이는 나중에 Lisp의 “특수 형식(special forms)”과 같은 것으로 여겨졌습니다. Guido van Rossum은 이런 종류의 기능을 12주 안에(즉, Python 3.7 베타 동결 전까지) 안전하게 구현할 수는 없다는 우려를 표했습니다.

얼마 후, 문자열 형식 지지자와 썽크(thunk) 형식 지지자 사이의 쟁점이 실제로는 어노테이션을 일반적인 문법 요소로 볼 것인지, 아니면 타입 검사 사용 사례에 묶인 것으로 볼 것인지에 관한 것이라는 점이 분명해졌습니다.

결국 Guido van Rossum은 썽크 아이디어를 거부한다고 선언했습니다. 이 아이디어를 구현하려면 인터프리터에 새로운 구성 요소가 필요하다는 점이 근거였습니다. 이 구성 요소는 어노테이션에 노출되어, __annotations__에 저장될 수 있는 값의 종류(임의의 객체, 문자열, 그리고 이제 썽크까지)를 늘리게 됩니다. 게다가 썽크는 문자열만큼 인트로스펙션하기 쉽지 않습니다. 무엇보다 Guido van Rossum은 어노테이션의 용도를 (선택적인 런타임 구성 요소를 갖춘) 정적 타입 지정으로 점차 제한하는 데 관심이 있다고 명시적으로 밝혔습니다.

Alyssa Coghlan도 PEP 563에 설득되었고, 곧바로 __future__ 임포트의 이름을 두고 으레 거치는 사소한 논쟁(bike shedding)을 시작했습니다. 많은 토론 참여자는 annotations가 기능 이름으로는 지나치게 포괄적인 이름으로 보인다는 데 동의했습니다. Guido van Rossum은 잠시 이를 string_annotations라고 부르기로 했지만, 이내 생각을 바꾸었습니다. division이 포괄적이면서도 의미가 명확한 이름의 선례라는 것이 그 이유였습니다.

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

정적 타이핑의 런타임 비용을 둘러싼 곁가지 논의도 시작되었으며, typing 모듈의 임포트 시간(의존성을 제외하면 re와 비슷하고, 의존성까지 포함하면 re보다 세 배 무겁습니다) 같은 주제가 다루어졌습니다.

감사의 글

이 문서는 Guido van Rossum, Jukka Lehtosalo, Ivan Levkivskyi의 소중한 의견과 격려, 조언이 없었다면 완성될 수 없었습니다.

구현은 Serhiy Storchaka가 철저히 검토하였으며, 그는 버그, 낮은 가독성, 성능 문제를 비롯한 온갖 종류의 문제를 찾아냈습니다.