PEP 593 – 유연한 함수 및 변수 어노테이션
- Author:
- Till Varoquaux <till.varoquaux at gmail.com>, Konstantin Kashin <kkashin at fb.com>
- Sponsor:
- Ivan Levkivskyi <levkivskyi at gmail.com>
- Discussions-To:
- Typing-SIG list
- Status:
- Final
- Type:
- Standards Track
- Topic:
- Typing
- Created:
- 26-Apr-2019
- Python-Version:
- 3.9
- Post-History:
- 20-May-2019
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 PEP 484의 타입 어노테이션을 임의의 메타데이터로 확장하는 메커니즘을 도입합니다.
동기
PEP 484는 PEP 3107에서 도입된 어노테이션에 표준 의미론을 제공합니다. PEP 484는 규범적인 문서이지만, 어노테이션을 소비하는 대부분의 도구에는 사실상의 표준입니다. 타입 어노테이션이 널리 쓰이는 여러 정적 검사 코드베이스에서는 타입 어노테이션이 다른 모든 형태의 어노테이션을 사실상 밀어냈습니다. PEP 3107에 기술된 어노테이션 사용 사례 중 일부(데이터베이스 매핑, 외국어 브리지)는 타입 어노테이션이 널리 퍼진 현 상황에서는 현실적이지 않습니다. 게다가 타입 어노테이션이 표준화되면서 특정 타입 검사기만 지원하는 고급 기능은 사용할 수 없게 되었습니다.
근거
이 PEP는 기존 타입을 컨텍스트별 메타데이터로 장식할 수 있도록 typing 모듈에 Annotated 타입을 추가합니다. 구체적으로, 타입 T에는 타입 힌트 Annotated[T, x]를 통해 메타데이터 x를 어노테이션으로 붙일 수 있습니다. 이 메타데이터는 정적 분석에도, 런타임에도 사용할 수 있습니다. 라이브러리(또는 도구)가 타입 힌트 Annotated[T, x]를 만났는데 메타데이터 x에 대한 특별한 로직이 없다면, 이를 무시하고 해당 타입을 단순히 T로 취급해야 합니다. 현재 typing 모듈에 있는 no_type_check 기능은 함수나 클래스의 어노테이션 타입 검사를 완전히 비활성화하지만, Annotated 타입은 T에 대한 정적 타입 검사(예: x를 안전하게 무시할 수 있는 mypy나 Pyre를 통한 검사)와 특정 애플리케이션 안에서 x에 대한 런타임 접근을 모두 허용합니다. 이 타입을 도입하면 Python 커뮤니티 전반이 관심을 갖는 다양한 사용 사례를 해결할 수 있습니다.
이 제안은 원래 typing GitHub에서 issue 600으로 제기되었고, 이후 Python ideas에서 논의되었습니다.
동기가 된 예시
어노테이션의 런타임 사용과 정적 사용 결합
런타임에 타입 어노테이션을 활용하는 라이브러리가 늘어나는 추세입니다(예: dataclasses). 외부 데이터로 타입 어노테이션을 확장할 수 있다면 이러한 라이브러리에 큰 도움이 될 것입니다.
다음은 가상의 모듈이 어노테이션을 활용하여 C 구조체를 읽는 방법을 보여 주는 예시입니다:
UnsignedShort = Annotated[int, struct2.ctype('H')]
SignedChar = Annotated[int, struct2.ctype('b')]
class Student(struct2.Packed):
# mypy typechecks 'name' field as 'str'
name: Annotated[str, struct2.ctype("<10s")]
serialnum: UnsignedShort
school: SignedChar
# 'unpack' only uses the metadata within the type annotations
Student.unpack(record)
# Student(name=b'raymond ', serialnum=4658, school=264)
새로운 타이핑 구성 요소 개발의 장벽 낮추기
일반적으로 새 타입을 추가할 때 개발자는 그 타입을 typing 모듈에 업스트림하고 mypy, PyCharm, Pyre, pytype 등을 수정해야 합니다… 이는 이러한 타입을 사용하는 오픈 소스 코드를 작업할 때 특히 중요합니다. 추가 로직 없이는 그 코드를 다른 개발자의 도구로 곧바로 옮겨 쓸 수 없기 때문입니다. 결과적으로 코드베이스에서 새로운 타입을 개발하고 시험해 보는 데 높은 비용이 듭니다. 이상적으로는 작성자가 우아한 기능 저하(graceful degradation)를 허용하는 방식(예: 클라이언트에 사용자 정의 mypy 플러그인이 없는 경우)으로 새 타입을 도입할 수 있어야 합니다. 그러면 개발 장벽이 낮아지고 어느 정도의 하위 호환성이 보장됩니다.
예를 들어, 작성자가 Python에 태그된 유니언에 대한 지원을 추가하고 싶다고 가정해 봅시다. 이를 달성하는 한 가지 방법은 단 하나의 필드만 설정할 수 있도록 Python에서 TypedDict를 어노테이트하는 것입니다:
Currency = Annotated[
TypedDict('Currency', {'dollars': float, 'pounds': float}, total=False),
TaggedUnion,
]
다소 번거로운 문법이지만, 이 방식을 쓰면 이 개념 증명을 반복적으로 개선할 수 있고, 아직 이 기능을 지원하지 않는 타입 검사기(또는 기타 도구)를 쓰는 사람들도 태그된 유니언이 있는 코드베이스에서 작업할 수 있습니다. 작성자는 태그된 유니언을 typing, mypy 등에 업스트림하기 전에 이 제안을 쉽게 테스트하고 문제점을 다듬을 수 있습니다. 게다가 TaggedUnion 어노테이션 파싱을 지원하지 않는 도구도 여전히 Currency를 TypedDict로 취급할 수 있으며, 이는 (약간 덜 엄격하기는 하지만) 여전히 가까운 근사입니다.
명세
문법
Annotated는 타입 하나와, 어노테이션을 나타내는 임의의 Python 값 목록으로 매개변수화됩니다. 문법의 구체적인 세부 사항은 다음과 같습니다:
Annotated의 첫 번째 인자는 유효한 타입이어야 합니다- 여러 개의 타입 어노테이션을 지원합니다 (
Annotated는 가변 인자를 지원합니다):Annotated[int, ValueRange(3, 10), ctype("char")]
Annotated는 최소 두 개의 인자로 호출해야 합니다 (Annotated[int]는 유효하지 않습니다)- 어노테이션의 순서는 보존되며 동등성 검사에 영향을 줍니다:
Annotated[int, ValueRange(3, 10), ctype("char")] != Annotated[ int, ctype("char"), ValueRange(3, 10) ]
- 중첩된
Annotated타입은 평탄화되며, 메타데이터는 가장 안쪽 어노테이션부터 순서대로 배치됩니다:Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[ int, ValueRange(3, 10), ctype("char") ]
- 중복된 어노테이션은 제거되지 않습니다:
Annotated[int, ValueRange(3, 10)] != Annotated[ int, ValueRange(3, 10), ValueRange(3, 10) ]
Annotated는 중첩된 별칭 및 제네릭 별칭과 함께 사용할 수 있습니다:Typevar T = ... Vec = Annotated[List[Tuple[T, T]], MaxLen(10)] V = Vec[int] V == Annotated[List[Tuple[int, int]], MaxLen(10)]
어노테이션 소비하기
결국 어노테이션을 어떻게 해석할지(해석한다면)는 Annotated 타입을 만나는 도구나 라이브러리가 책임집니다. Annotated 타입을 만난 도구나 라이브러리는 어노테이션을 훑어보며 관심 대상인지 판단할 수 있습니다 (예: isinstance()를 사용하여).
알 수 없는 어노테이션: 도구나 라이브러리가 어노테이션을 지원하지 않거나 알 수 없는 어노테이션을 만나면, 그것을 그냥 무시하고 어노테이션이 붙은 타입을 기저 타입으로 취급해야 합니다. 예를 들어, 이름에 대한 어노테이션 중 struct2.ctype의 인스턴스가 아닌 어노테이션을 만나면 (예: Annotated[str, 'foo', struct2.ctype("<10s")]), unpack 메서드는 그것을 무시해야 합니다.
어노테이션의 네임스페이스: 어노테이션에 사용되는 클래스가 네임스페이스 역할을 하므로 어노테이션에는 네임스페이스가 필요하지 않습니다.
여러 어노테이션: 클라이언트가 한 타입에 여러 어노테이션을 붙이도록 허용할지, 그리고 그 어노테이션들을 어떻게 병합할지는 어노테이션을 소비하는 도구가 결정할 일입니다.
Annotated 타입은 어떤 노드에든 같은 (또는 서로 다른) 타입의 어노테이션을 여러 개 붙일 수 있게 해 주므로, 그 어노테이션을 소비하는 도구나 라이브러리가 발생할 수 있는 중복을 처리할 책임을 집니다. 예를 들어, 값 범위 분석을 하고 있다면 다음을 허용할 수 있습니다:
T1 = Annotated[int, ValueRange(-10, 5)]
T2 = Annotated[T1, ValueRange(-20, 3)]
중첩된 어노테이션을 평탄화하면 이는 다음과 같이 변환됩니다:
T2 = Annotated[int, ValueRange(-10, 5), ValueRange(-20, 3)]
get_type_hints()와의 상호 작용
typing.get_type_hints()는 하위 호환성을 유지하기 위해 기본값이 False인 새 인자 include_extras를 받게 됩니다. include_extras가 False이면 추가 어노테이션은 반환값에서 제거됩니다. 그렇지 않으면 어노테이션은 변경되지 않은 채로 반환됩니다:
@struct2.packed
class Student(NamedTuple):
name: Annotated[str, struct.ctype("<10s")]
get_type_hints(Student) == {'name': str}
get_type_hints(Student, include_extras=False) == {'name': str}
get_type_hints(Student, include_extras=True) == {
'name': Annotated[str, struct.ctype("<10s")]
}
별칭과 장황함에 대한 우려
모든 곳에 typing.Annotated를 쓰는 것은 상당히 장황할 수 있습니다. 다행히 어노테이션에 별칭을 붙일 수 있으므로, 실제로는 클라이언트가 상용구 코드를 많이 작성해야 할 것으로 예상하지 않습니다:
T = TypeVar('T')
Const = Annotated[T, my_annotations.CONST]
class C:
def const_method(self: Const[List[int]]) -> int:
...
기각된 아이디어
제안된 아이디어 중 일부는 Annotated가 다른 타이핑 어노테이션과 깔끔하게 통합되지 못하게 만들기 때문에 이 PEP에서 거부되었습니다:
Annotated는 데코레이트된 타입을 추론할 수 없습니다.Annotated[..., Immutable]를 사용해 값의 타입은 여전히 추론하면서 그 값을 불변으로 표시하는 것을 상상해 볼 수 있습니다. 타이핑은 추론된 타입을 그 밖의 어떤 곳에서도 사용하는 것을 지원하지 않으므로, 이를 특수한 경우로 추가하지 않는 것이 가장 좋습니다.Annotated[Type, Ann1, Ann2, ...]대신(Type, Ann1, Ann2, ...)를 사용하는 것. 이렇게 하면 어노테이션이 중첩된 위치에 나타날 때 혼란을 일으키고 (Callable[[A, B], C]가Callable[[(A, B)], C]와 너무 비슷합니다), 생성자가 그대로 통과되도록 하는 것이 불가능해집니다 (C = Annotation[T, Ann]일 때T(5) == C(5)).
설계를 단순하게 유지하기 위해 이 기능은 제외되었습니다:
Annotated는 단일 인자로 호출할 수 없습니다. Annotated가 단일 인자로 호출될 때 기저 값을 반환하도록 지원할 수도 있었습니다(예:Annotated[int] == int). 그러나 이는 명세를 복잡하게 만들 뿐 이점은 거의 없습니다.
Copyright
This document has been placed in the public domain.