PEP 484 – 타입 힌트
- Author:
- Guido van Rossum <guido at python.org>, Jukka Lehtosalo <jukka.lehtosalo at iki.fi>, Łukasz Langa <lukasz at python.org>
- BDFL-Delegate:
- Mark Shannon
- Discussions-To:
- Python-Dev list
- Status:
- Final
- Type:
- Standards Track
- Topic:
- Typing
- Created:
- 29-Sep-2014
- Python-Version:
- 3.5
- Post-History:
- 16-Jan-2015, 20-Mar-2015, 17-Apr-2015, 20-May-2015, 22-May-2015
- Resolution:
- Python-Dev message
Table of Contents
- 초록
- 근거와 목표
- 어노테이션의 의미
- 타입 정의 구문
- 허용되는 타입 힌트
- None 사용
- 타입 별칭
- 호출 가능 객체
- 제네릭
- 사용자 정의 제네릭 타입
- 타입 변수의 범위 지정 규칙
- 제너릭 클래스의 인스턴스화와 타입 소거
- 기본 클래스로서의 임의의 제너릭 타입
- 추상 제네릭 타입
- 상한이 있는 타입 변수
- 공변성과 반공변성
- 수치 탑
- 전방 참조
- 유니온 타입
- 유니언에서 싱글턴 타입 지원
Any타입NoReturn타입- 클래스 객체의 타입
- 인스턴스 메서드와 클래스 메서드에 어노테이션 지정하기
- 버전 및 플랫폼 검사
- 런타임 검사 또는 타입 검사?
- 임의의 인자 목록과 기본 인자 값
- 위치 전용 인자
- 제너레이터 함수와 코루틴에 어노테이션 지정하기
- 함수 어노테이션의 다른 용도와의 호환성
- 타입 주석입니다.
- 캐스트입니다.
- NewType 도우미 함수
- 스텁 파일
- 예외
typing모듈- Python 2.7 및 양쪽 버전을 아우르는 코드에 권장되는 구문입니다.
- 거부된 대안
- PEP 개발 프로세스
- 감사의 말
- Copyright
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
PEP 3107은 함수 어노테이션을 위한 구문을 도입했지만, 의미론은 의도적으로 정의하지 않은 채 남겨 두었습니다. 이제 정적 타입 분석을 위한 제3자 사용 사례가 충분히 축적되었으므로, 커뮤니티는 표준 라이브러리 내의 표준 용어와 기본 도구로부터 혜택을 얻을 수 있습니다.
이 PEP는 이러한 표준 정의와 도구를 제공하는 잠정적 모듈과, 어노테이션을 사용할 수 없는 상황을 위한 몇 가지 관례를 도입합니다.
이 PEP는 어노테이션의 다른 사용을 여전히 명시적으로 방지하지 않으며, 어노테이션이 이 사양을 준수하는 경우에도 특정한 어노테이션 처리를 요구하거나 금지하지 않는다는 점에 유의하십시오. 이는 PEP 333이 웹 프레임워크를 위해 했던 것처럼 더 나은 조정을 가능하게 할 뿐입니다.
예를 들어, 다음은 인자와 반환 타입이 어노테이션에 선언된 간단한 함수입니다.:
def greeting(name: str) -> str:
return 'Hello ' + name
이러한 어노테이션은 일반적인 __annotations__ 속성을 통해 런타임에 사용할 수 있지만, 런타임에는 타입 검사가 수행되지 않습니다. 대신 이 제안은 사용자가 자발적으로 소스 코드에 실행할 수 있는 별도의 오프라인 타입 검사기가 존재한다고 가정합니다. 본질적으로 이러한 타입 검사기는 매우 강력한 린터로 작동합니다. (물론 개별 사용자가 계약에 의한 설계 적용이나 JIT 최적화를 위해 런타임에 유사한 검사기를 사용하는 것도 가능하겠지만, 이러한 도구는 아직 그만큼 성숙하지 않았습니다.)
이 제안은 mypy에서 강한 영감을 받았습니다. 예를 들어, “정수의 시퀀스”라는 타입은 Sequence[int]로 작성할 수 있습니다. 대괄호는 언어에 새로운 구문을 추가할 필요가 없다는 의미입니다. 여기의 예제는 순수 Python 모듈 typing에서 가져온 사용자 정의 타입 Sequence를 사용합니다. Sequence[int] 표기는 메타클래스에 __getitem__()을 구현함으로써 런타임에 작동하지만, 그 의미는 주로 오프라인 타입 검사기를 위한 것입니다.
타입 시스템은 유니언, 제네릭 타입 및 모든 타입과 일관되는(즉, 모든 타입에 할당할 수 있고 모든 타입으로부터 할당받을 수 있는) Any라는 특수 타입을 지원합니다. 이 후자의 기능은 점진적 타이핑이라는 개념에서 가져온 것입니다. 점진적 타이핑과 전체 타입 시스템은 PEP 483에 설명되어 있습니다.
우리가 차용했거나 우리의 접근 방식과 비교 및 대조할 수 있는 다른 접근 방식은 PEP 482에 설명되어 있습니다.
근거와 목표
PEP 3107은 함수 정의의 일부에 임의의 어노테이션을 사용할 수 있도록 지원을 추가했습니다. 당시에는 어노테이션에 어떤 의미도 부여되지 않았지만, 어노테이션을 타입 힌트에 사용하려는 암묵적 목표가 항상 있었으며, 이는 해당 PEP에서 첫 번째 가능한 사용 사례로 열거되어 있습니다.
이 PEP는 타입 어노테이션을 위한 표준 구문을 제공하여 Python 코드를 더 쉬운 정적 분석과 리팩터링, 잠재적인 런타임 타입 검사, 그리고 (아마도 일부 상황에서는) 타입 정보를 활용한 코드 생성에 개방하는 것을 목표로 합니다.
이러한 목표 중 정적 분석이 가장 중요합니다. 여기에는 mypy와 같은 오프라인 타입 검사기에 대한 지원과, IDE가 코드 완성과 리팩터링에 사용할 수 있는 표준 표기법 제공이 포함됩니다.
목표가 아닌 것
제안된 typing 모듈에는 런타임 타입 검사를 위한 일부 구성 요소, 특히 get_type_hints() 함수가 포함되지만, 특정 런타임 타입 검사 기능을 구현하려면 제3자 패키지를 개발해야 합니다. 예를 들어 데코레이터나 메타클래스를 사용할 수 있습니다. 성능 최적화를 위해 타입 힌트를 사용하는 것은 독자의 과제로 남겨 둡니다.
또한 Python은 동적 타입 언어로 남으며, 작성자들은 관례에 의해서라도 타입 힌트를 의무화할 의도가 전혀 없다는 점을 강조해야 합니다.
어노테이션의 의미
어노테이션이 없는 모든 함수는 모든 타입 검사기에서 가능한 가장 일반적인 타입을 가진 것으로 처리하거나 무시해야 합니다. @no_type_check 데코레이터가 적용된 함수는 어노테이션이 없는 것으로 처리해야 합니다.
검사 대상 함수에는 모든 인자와 반환 타입에 대한 어노테이션을 지정하는 것이 권장되지만 필수는 아닙니다. 검사 대상 함수에서 인자와 반환 타입의 기본 어노테이션은 Any입니다. 예외는 인스턴스 메서드와 클래스 메서드의 첫 번째 인자입니다. 첫 번째 인자에 어노테이션이 없으면, 인스턴스 메서드에서는 포함하는 클래스의 타입으로, 클래스 메서드에서는 포함하는 클래스 객체에 해당하는 타입 객체 타입으로 간주합니다. 예를 들어 클래스 A에서 인스턴스 메서드의 첫 번째 인자는 암시적 타입 A를 가집니다. 클래스 메서드에서는 사용 가능한 타입 표기만으로 첫 번째 인자의 정확한 타입을 나타낼 수 없습니다.
(__init__의 반환 타입에는 -> None을 어노테이션으로 지정해야 합니다. 그 이유는 미묘합니다. __init__이 -> None 반환 어노테이션을 기본값으로 가정한다면, 인자가 없고 어노테이션도 없는 __init__ 메서드도 여전히 타입 검사를 수행해야 한다는 의미입니까? 이를 모호하게 남겨 두거나 예외에 대한 예외를 도입하는 대신, __init__에는 반환 어노테이션이 있어야 한다고만 말합니다. 따라서 기본 동작은 다른 메서드와 동일합니다.)
타입 검사기는 검사 대상 함수의 본문이 지정된 어노테이션과 일관되는지 검사해야 합니다. 어노테이션은 다른 검사 대상 함수에 나타나는 호출의 정확성을 검사하는 데에도 사용할 수 있습니다.
타입 검사기는 필요한 만큼 많은 정보를 추론하려고 해야 합니다. 최소 요구 사항은 내장 데코레이터 @property, @staticmethod 및 @classmethod를 처리하는 것입니다.
타입 정의 구문
이 구문은 아래 절에서 설명하는 여러 확장 기능을 포함한 PEP 3107 스타일 어노테이션을 활용합니다. 기본 형식에서는 함수 어노테이션 슬롯을 클래스로 채워 타입 힌트를 사용합니다.:
def greeting(name: str) -> str:
return 'Hello ' + name
이는 name 인자의 예상 타입이 str임을 나타냅니다. 마찬가지로 예상 반환 타입은 str입니다.
특정 인자 타입의 서브타입인 타입을 가진 표현식도 해당 인자에 허용됩니다.
허용되는 타입 힌트
타입 힌트에는 내장 클래스(표준 라이브러리 또는 서드 파티 확장 모듈에 정의된 클래스 포함), 추상 베이스 클래스, types 모듈에서 사용할 수 있는 타입, 사용자 정의 클래스(표준 라이브러리 또는 서드 파티 모듈에 정의된 클래스 포함)가 포함될 수 있습니다.
일반적으로 어노테이션이 타입 힌트에 가장 적합한 형식이지만, 특수 주석으로 표현하거나 별도로 배포되는 스텁 파일에 표현하는 것이 더 적절한 경우도 있습니다. (예제는 아래를 참조하십시오.)
어노테이션은 함수가 정의되는 시점에 예외를 발생시키지 않고 평가되는 유효한 표현식이어야 합니다(단, 아래의 전방 참조는 제외합니다).
어노테이션은 단순하거나 정적으로 유지해야 하며, 그렇지 않으면 정적 분석 도구가 해당 값을 해석하지 못할 수 있습니다. 예를 들어 동적으로 계산되는 타입은 이해되지 않을 가능성이 큽니다. (이는 의도적으로 다소 모호한 요구 사항이며, 논의 결과에 따라 향후 이 PEP 버전에 구체적인 포함 및 제외 항목이 추가될 수 있습니다.)
위 항목에 더해, 아래에 정의된 다음 특수 구성도 사용할 수 있습니다. None, Any, Union, Tuple, Callable, typing에서 내보내는 모든 ABC와 구체 클래스의 대체물(예: Sequence 및 Dict), 타입 변수, 타입 별칭입니다.
다음 절에서 설명하는 기능을 지원하기 위해 새로 도입된 모든 이름(예: Any 및 Union)은 typing 모듈에서 사용할 수 있습니다.
None 사용
타입 힌트에서 사용될 때 None은 type(None)과 동등한 것으로 간주됩니다.
타입 별칭
타입 별칭은 간단한 변수 할당으로 정의합니다.:
Url = str
def retry(url: Url, retry_count: int) -> None: ...
별칭 이름은 사용자 정의 타입을 나타내므로 대문자로 표기할 것을 권장합니다. 사용자 정의 클래스와 마찬가지로 일반적으로 그렇게 표기하기 때문입니다.
타입 별칭은 어노테이션의 타입 힌트만큼 복잡할 수 있습니다. 타입 힌트로 허용되는 것은 무엇이든 타입 별칭에서도 허용됩니다.:
from typing import TypeVar, Iterable, Tuple
T = TypeVar('T', int, float, complex)
Vector = Iterable[Tuple[T, T]]
def inproduct(v: Vector[T]) -> T:
return sum(x*y for x, y in v)
def dilate(v: Vector[T], scale: T) -> Vector[T]:
return ((x * scale, y * scale) for x, y in v)
vec = [] # type: Vector[float]
이는 다음과 같습니다.:
from typing import TypeVar, Iterable, Tuple
T = TypeVar('T', int, float, complex)
def inproduct(v: Iterable[Tuple[T, T]]) -> T:
return sum(x*y for x, y in v)
def dilate(v: Iterable[Tuple[T, T]], scale: T) -> Iterable[Tuple[T, T]]:
return ((x * scale, y * scale) for x, y in v)
vec = [] # type: Iterable[Tuple[float, float]]
호출 가능 객체
특정 시그니처의 콜백 함수를 예상하는 프레임워크에는 Callable[[Arg1Type, Arg2Type], ReturnType]를 사용하여 타입 힌트를 지정할 수 있습니다. 예:
from typing import Callable
def feeder(get_next_item: Callable[[], str]) -> None:
# Body
def async_query(on_success: Callable[[int], None],
on_error: Callable[[int, Exception], None]) -> None:
# Body
인자 목록 대신 리터럴 생략 부호(점 세 개)를 사용하면 호출 시그니처를 지정하지 않고 호출 가능 객체의 반환 타입을 선언할 수 있습니다.:
def partial(func: Callable[..., str], *args) -> Callable[..., str]:
# Body
생략 부호 주위에는 대괄호가 없다는 점에 유의하십시오. 이 경우 콜백의 인자에는 어떠한 제약도 없으며 키워드 인자도 허용됩니다.
키워드 인자를 사용하는 콜백은 일반적인 사용 사례로 간주되지 않으므로 현재 Callable를 사용하여 키워드 인자를 지정할 수는 없습니다. 마찬가지로 특정 타입의 가변 개수 인자를 사용하는 콜백 시그니처를 지정하는 기능도 지원되지 않습니다.
typing.Callable은 collections.abc.Callable을 대체하는 역할도 하므로, isinstance(x, typing.Callable)은 isinstance(x, collections.abc.Callable)에 처리를 위임하여 구현됩니다. 그러나 isinstance(x, typing.Callable[...])은 지원되지 않습니다.
제네릭
컨테이너에 저장된 객체의 타입 정보는 일반적인 방식으로 정적으로 추론할 수 없으므로, 추상 베이스 클래스는 컨테이너 요소에 대해 예상되는 타입을 나타내는 첨자 표기를 지원하도록 확장되었습니다. 예:
from typing import Mapping, Set
def notify_by_email(employees: Set[Employee], overrides: Mapping[str, str]) -> None: ...
제네릭은 typing에서 제공하는 TypeVar라는 새로운 팩토리를 사용하여 매개변수화할 수 있습니다. 예:
from typing import Sequence, TypeVar
T = TypeVar('T') # Declare type variable
def first(l: Sequence[T]) -> T: # Generic function
return l[0]
이 경우 반환되는 값은 컬렉션이 보유한 요소와 일관되어야 한다는 것이 계약입니다.
TypeVar()표현식은 항상 변수에 직접 할당해야 합니다. 더 큰 표현식의 일부로 사용해서는 안 됩니다. TypeVar()에 전달하는 인자는 할당 대상 변수 이름과 동일한 문자열이어야 합니다. 타입 변수는 재정의해서는 안 됩니다.
TypeVar는 매개변수화된 타입을 가능한 타입의 고정된 집합으로 제한하는 기능을 지원합니다(주의: 해당 타입은 타입 변수로 매개변수화할 수 없습니다). 예를 들어 str 과 bytes 만을 범위로 갖는 타입 변수를 정의할 수 있습니다. 기본적으로 타입 변수의 범위는 가능한 모든 타입입니다. 타입 변수를 제한하는 예:
from typing import TypeVar, Text
AnyStr = TypeVar('AnyStr', Text, bytes)
def concat(x: AnyStr, y: AnyStr) -> AnyStr:
return x + y
concat 함수는 str 인자 두 개 또는 bytes 인자 두 개로 호출할 수 있지만, str 과 bytes 인자를 혼합하여 호출할 수는 없습니다.
제한 조건이 있다면 최소 두 개 이상이어야 하며, 하나의 제한 조건만 지정하는 것은 허용되지 않습니다.
타입 변수로 제한된 타입의 서브타입은 해당 타입 변수의 문맥에서 각각 명시적으로 나열된 기본 타입으로 처리해야 합니다. 다음 예를 살펴보십시오.:
class MyStr(str): ...
x = concat(MyStr('apple'), MyStr('pie'))
호출은 유효하지만 타입 변수 AnyStr는 MyStr가 아니라 str로 설정됩니다. 실제로 x에 할당된 반환 값의 추론된 타입도 str입니다.
또한 Any는 모든 타입 변수에 유효한 값입니다. 다음을 고려하십시오.:
def count_truthy(elements: List[Any]) -> int:
return sum(1 for elem in elements if elem)
이는 제네릭 표기를 생략하고 단지 elements: List라고 말하는 것과 같습니다.
사용자 정의 제네릭 타입
Generic기본 클래스를 포함하여 사용자 정의 클래스를 제네릭으로 정의할 수 있습니다. 예::
from typing import TypeVar, Generic
from logging import Logger
T = TypeVar('T')
class LoggedVar(Generic[T]):
def __init__(self, value: T, name: str, logger: Logger) -> None:
self.name = name
self.logger = logger
self.value = value
def set(self, new: T) -> None:
self.log('Set ' + repr(self.value))
self.value = new
def get(self) -> T:
self.log('Get ' + repr(self.value))
return self.value
def log(self, message: str) -> None:
self.logger.info('{}: {}'.format(self.name, message))
Generic[T]를 기본 클래스로 사용하면 LoggedVar클래스가 단일 타입 매개변수 T를 받도록 정의합니다. 또한 이를 통해 클래스 본문 내에서 T를 타입으로 사용할 수 있습니다.
Generic기본 클래스는 __getitem__을 정의하는 메타클래스를 사용하므로 LoggedVar[t]를 타입으로 사용할 수 있습니다.:
from typing import Iterable
def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
for var in vars:
var.set(0)
제네릭 타입에는 임의의 수의 타입 변수가 있을 수 있으며, 타입 변수에는 제약을 지정할 수 있습니다. 이는 유효합니다.:
from typing import TypeVar, Generic
...
T = TypeVar('T')
S = TypeVar('S')
class Pair(Generic[T, S]):
...
Generic에 전달하는 각 타입 변수 인자는 서로 달라야 합니다. 따라서 이는 유효하지 않습니다.:
from typing import TypeVar, Generic
...
T = TypeVar('T')
class Pair(Generic[T, T]): # INVALID
...
다른 제네릭 클래스를 서브클래싱하고 해당 매개변수의 타입 변수를 지정하는 단순한 경우에는 Generic[T]기본 클래스가 중복됩니다.:
from typing import TypeVar, Iterator
T = TypeVar('T')
class MyIter(Iterator[T]):
...
해당 클래스 정의는 다음과 동등합니다.:
class MyIter(Iterator[T], Generic[T]):
...
Generic을 사용하여 다중 상속을 사용할 수 있습니다.:
from typing import TypeVar, Generic, Sized, Iterable, Container, Tuple
T = TypeVar('T')
class LinkedList(Sized, Generic[T]):
...
K = TypeVar('K')
V = TypeVar('V')
class MyMapping(Iterable[Tuple[K, V]],
Container[Tuple[K, V]],
Generic[K, V]):
...
타입 매개변수를 지정하지 않고 제네릭 클래스를 서브클래싱하면 각 위치에 Any를 사용한다고 간주합니다. 다음 예에서 MyIterable은 제네릭이 아니지만 암시적으로 Iterable[Any]를 상속합니다.:
from typing import Iterable
class MyIterable(Iterable): # Same as Iterable[Any]
...
제네릭 메타클래스는 지원되지 않습니다.
타입 변수의 범위 지정 규칙
타입 변수는 일반적인 이름 확인 규칙을 따릅니다. 그러나 정적 타입 검사 컨텍스트에서는 다음과 같은 몇 가지 특수한 경우가 있습니다.
- 제네릭 함수에서 사용되는 타입 변수는 동일한 코드 블록에서 서로 다른 타입을 나타내는 것으로 추론될 수 있습니다. 예::
from typing import TypeVar, Generic T = TypeVar('T') def fun_1(x: T) -> T: ... # T here def fun_2(x: T) -> T: ... # and here could be different fun_1(1) # This is OK, T is inferred to be int fun_2('a') # This is also OK, now T is str
- 제네릭 클래스의 메서드에서 사용되는 타입 변수가 이 클래스를 매개변수화하는 변수 중 하나와 일치하면 항상 해당 변수에 바인딩됩니다. 예::
from typing import TypeVar, Generic T = TypeVar('T') class MyClass(Generic[T]): def meth_1(self, x: T) -> T: ... # T here def meth_2(self, x: T) -> T: ... # and here are always the same a = MyClass() # type: MyClass[int] a.meth_1(1) # OK a.meth_2('a') # This is an error!
- 클래스를 매개변수화하는 변수 중 어느 것과도 일치하지 않는 메서드의 타입 변수는 해당 변수에 대한 제네릭 함수로 만듭니다.:
T = TypeVar('T') S = TypeVar('S') class Foo(Generic[T]): def method(self, x: T, y: S) -> S: ... x = Foo() # type: Foo[int] y = x.method(0, "abc") # inferred type of y is str
- 바인딩되지 않은 타입 변수는 제네릭 함수의 본문이나 메서드 정의를 제외한 클래스 본문에 나타나서는 안 됩니다.:
T = TypeVar('T') S = TypeVar('S') def a_fun(x: T) -> None: # this is OK y = [] # type: List[T] # but below is an error! y = [] # type: List[S] class Bar(Generic[T]): # this is also an error an_attr = [] # type: List[S] def do_something(x: S) -> S: # this is OK though ...
- 제네릭 함수 내부에 나타나는 제네릭 클래스 정의에서는 해당 제네릭 함수를 매개변수화하는 타입 변수를 사용해서는 안 됩니다.:
from typing import List def a_fun(x: T) -> None: # This is OK a_list = [] # type: List[T] ... # This is however illegal class MyGeneric(Generic[T]): ...
- 다른 제너릭 클래스 안에 중첩된 제너릭 클래스는 동일한 타입 변수를 사용할 수 없습니다. 바깥 클래스의 타입 변수 범위는 안쪽 클래스까지 미치지 않습니다.:
T = TypeVar('T') S = TypeVar('S') class Outer(Generic[T]): class Bad(Iterable[T]): # Error ... class AlsoBad: x = None # type: List[T] # Also an error class Inner(Iterable[S]): # OK ... attr = None # type: Inner[T] # Also OK
제너릭 클래스의 인스턴스화와 타입 소거
사용자 정의 제너릭 클래스는 인스턴스화할 수 있습니다. Generic[T]를 상속하는 Node 클래스를 작성한다고 가정합니다.:
from typing import TypeVar, Generic
T = TypeVar('T')
class Node(Generic[T]):
...
Node 인스턴스를 만들려면 일반 클래스와 마찬가지로 Node()를 호출합니다. 런타임에 인스턴스의 타입(클래스)은 Node가 됩니다. 그러나 타입 검사기에게는 어떤 타입입니까? 답은 호출에 사용할 수 있는 정보의 양에 따라 달라집니다. 생성자(__init__ 또는 __new__)가 시그니처에서 T를 사용하고 이에 대응하는 인자 값이 전달되면, 해당 인자(들)의 타입이 대체됩니다. 그렇지 않으면 Any로 간주합니다. 예를 들어 다음과 같습니다.:
from typing import TypeVar, Generic
T = TypeVar('T')
class Node(Generic[T]):
x = None # type: T # Instance attribute (see below)
def __init__(self, label: T = None) -> None:
...
x = Node('') # Inferred type is Node[str]
y = Node(0) # Inferred type is Node[int]
z = Node() # Inferred type is Node[Any]
추론된 타입이 [Any]를 사용하지만 의도한 타입이 더 구체적인 경우, 타입 주석(아래 참조)을 사용하여 변수의 타입을 강제할 수 있습니다. 예를 들어 다음과 같습니다.:
# (continued from previous example)
a = Node() # type: Node[int]
b = Node() # type: Node[str]
또는 특정한 구체적 타입을 지정하여 인스턴스화할 수도 있습니다. 예를 들어 다음과 같습니다.:
# (continued from previous example)
p = Node[int]()
q = Node[str]()
r = Node[int]('') # Error
s = Node[str](0) # Error
p와 q의 런타임 타입(클래스)은 여전히 Node에 불과합니다 – Node[int]와 Node[str]은 구별 가능한 클래스 객체이지만, 이를 인스턴스화하여 생성한 객체의 런타임 클래스에는 그 구분이 기록되지 않습니다. 이 동작을 “타입 소거”라고 하며, 제너릭을 사용하는 언어(예: Java, TypeScript)에서는 일반적입니다.
제너릭 클래스를 사용하여 속성에 접근하면(매개변수화 여부와 관계없이) 타입 검사에 실패합니다. 클래스 정의 본문 외부에서는 클래스 속성을 할당할 수 없으며, 같은 이름의 인스턴스 속성이 없는 클래스 인스턴스를 통해서만 조회할 수 있습니다.:
# (continued from previous example)
Node[int].x = 1 # Error
Node[int].x # Error
Node.x = 1 # Error
Node.x # Error
type(p).x # Error
p.x # Ok (evaluates to None)
Node[int]().x # Ok (evaluates to None)
p.x = 1 # Ok, but assigning to instance attribute
Mapping이나 Sequence와 같은 추상 컬렉션의 제너릭 버전과 내장 클래스의 제너릭 버전 – List, Dict, Set, FrozenSet – 은 인스턴스화할 수 없습니다. 그러나 이러한 클래스의 구체적인 사용자 정의 서브클래스와 구체적인 컬렉션의 제너릭 버전은 인스턴스화할 수 있습니다.:
data = DefaultDict[int, bytes]()
정적 타입과 런타임 클래스를 혼동하지 않도록 주의하십시오. 이 경우에도 타입은 소거되며, 위 표현식은 단지 다음의 축약형입니다.:
data = collections.defaultdict() # type: DefaultDict[int, bytes]
첨자화된 클래스(예: Node[int])를 표현식에서 직접 사용하는 것은 권장하지 않으며 – 대신 타입 별칭(예: IntNode = Node[int])을 사용하는 것이 좋습니다. (첫째, 첨자화된 클래스(예: Node[int])를 생성하는 데에는 런타임 비용이 듭니다. 둘째, 타입 별칭을 사용하면 더 읽기 쉽습니다.)
기본 클래스로서의 임의의 제너릭 타입
Generic[T]는 기본 클래스로만 유효하며 – 적절한 타입은 아닙니다. 그러나 위 예의 LinkedList[T]와 같은 사용자 정의 제너릭 타입과 List[T]및 Iterable[T]와 같은 내장 제너릭 타입 및 ABC는 타입과 기본 클래스 양쪽 모두로 유효합니다. 예를 들어, 타입 인자를 특수화하는 Dict의 서브클래스를 정의할 수 있습니다.:
from typing import Dict, List, Optional
class Node:
...
class SymbolTable(Dict[str, List[Node]]):
def push(self, name: str, node: Node) -> None:
self.setdefault(name, []).append(node)
def pop(self, name: str) -> Node:
return self[name].pop()
def lookup(self, name: str) -> Optional[Node]:
nodes = self.get(name)
if nodes:
return nodes[-1]
return None
SymbolTable은 dict의 서브클래스이며 Dict[str, List[Node]]의 서브타입입니다.
제너릭 기본 클래스가 타입 변수인 타입 인자를 가지면, 이렇게 정의된 클래스도 제너릭이 됩니다. 예를 들어, 이터러블이면서 컨테이너인 제너릭 LinkedList 클래스를 정의할 수 있습니다.:
from typing import TypeVar, Iterable, Container
T = TypeVar('T')
class LinkedList(Iterable[T], Container[T]):
...
이제 LinkedList[int]은 유효한 타입입니다. Generic[...]내부에서 동일한 타입 변수 T를 여러 번 사용하지 않는 한, 기본 클래스 목록에서 T를 여러 번 사용할 수 있다는 점에 유의하십시오.
다음 예제도 고려하십시오.:
from typing import TypeVar, Mapping
T = TypeVar('T')
class MyDict(Mapping[str, T]):
...
이 경우 MyDict에는 단일 매개변수 T가 있습니다.
추상 제네릭 타입
Generic에 사용되는 메타클래스는 abc.ABCMeta의 하위 클래스입니다. 추상 메서드나 프로퍼티를 포함하면 제네릭 클래스를 ABC로 만들 수 있으며, 제네릭 클래스는 메타클래스 충돌 없이 ABC를 기본 클래스로 가질 수도 있습니다.
상한이 있는 타입 변수
타입 변수는 bound=<type> 을 사용하여 상한을 지정할 수 있습니다(참고: <type> 자체는 타입 변수로 매개변수화할 수 없습니다). 이는 타입 변수를 대신하여 명시적 또는 암시적으로 대입되는 실제 타입이 경계 타입의 서브타입이어야 한다는 의미입니다. 예시:
from typing import TypeVar, Sized
ST = TypeVar('ST', bound=Sized)
def longer(x: ST, y: ST) -> ST:
if len(x) > len(y):
return x
else:
return y
longer([1], [1, 2]) # ok, return type List[int]
longer({1}, {1, 2}) # ok, return type Set[int]
longer([1], {1, 2}) # ok, return type Collection[int]
상한은 타입 제약 조건과 결합할 수 없습니다(앞의 예제에서 사용한 AnyStr를 참조하십시오). 타입 제약 조건은 추론된 타입이 제약 조건 타입 중 _exactly_ 하나가 되도록 하는 반면, 상한은 실제 타입이 경계 타입의 서브타입이기만을 요구합니다.
공변성과 반공변성
Manager를 서브클래스로 갖는 Employee클래스를 생각해 보십시오. 이제 인자에 List[Employee]가 어노테이션으로 지정된 함수가 있다고 가정해 보십시오. 이 함수를 List[Manager]타입의 변수를 인자로 사용하여 호출해도 되겠습니까? 많은 사람은 결과를 고려하지도 않고 “물론 그렇습니다”라고 대답할 것입니다. 그러나 함수에 대해 더 많은 정보를 알지 못하는 한, 타입 검사기는 이러한 호출을 거부해야 합니다. 함수가 목록에 Employee인스턴스를 추가할 수 있으며, 이는 호출자에 있는 변수의 타입을 위반하게 되기 때문입니다.
이러한 인자는 반공변적으로 동작하는 것으로 밝혀졌지만, 직관적인 답변(함수가 해당 인자를 변경하지 않는 경우에는 올바릅니다!)을 따르려면 인자가 공변적으로 동작해야 합니다. 이러한 개념에 대한 자세한 소개는 Wikipedia와 PEP 483에서 확인할 수 있으며, 여기서는 타입 검사기의 동작을 제어하는 방법만 살펴봅니다.
기본적으로 제네릭 타입은 모든 타입 변수에 대해 불변으로 간주됩니다. 이는 List[Employee]와 같은 타입이 어노테이션으로 지정된 변수의 값이 타입 어노테이션과 정확히 일치해야 한다는 의미입니다. 즉, 타입 매개변수(이 예제에서는 Employee)의 서브클래스나 슈퍼클래스는 허용되지 않습니다.
공변 또는 반공변 타입 검사가 허용되는 컨테이너 타입의 선언을 용이하게 하기 위해, 타입 변수는 covariant=True 또는 contravariant=True 키워드 인자를 허용합니다. 이 중 최대 하나만 전달할 수 있습니다. 이러한 변수를 사용하여 정의된 제네릭 타입은 해당 변수에 대해 공변 또는 반공변으로 간주됩니다. 관례상 covariant=True 로 정의된 타입 변수에는 _co 로 끝나는 이름을 사용하고, contravariant=True 로 정의된 타입 변수에는 _contra 로 끝나는 이름을 사용하는 것이 권장됩니다.
일반적인 예로 불변(또는 읽기 전용) 컨테이너 클래스를 정의하는 경우가 있습니다.:
from typing import TypeVar, Generic, Iterable, Iterator
T_co = TypeVar('T_co', covariant=True)
class ImmutableList(Generic[T_co]):
def __init__(self, items: Iterable[T_co]) -> None: ...
def __iter__(self) -> Iterator[T_co]: ...
...
class Employee: ...
class Manager(Employee): ...
def dump_employees(emps: ImmutableList[Employee]) -> None:
for emp in emps:
...
mgrs = ImmutableList([Manager()]) # type: ImmutableList[Manager]
dump_employees(mgrs) # OK
typing의 읽기 전용 컬렉션 클래스는 모두 타입 변수에 대해 공변으로 선언됩니다(예: Mapping 및 Sequence). 변경 가능한 컬렉션 클래스(예: MutableMapping 및 MutableSequence)는 불변으로 선언됩니다. 반공변 타입의 한 가지 예는 Generator타입이며, 이 타입은 send()인자 타입에 대해 반공변입니다(아래 참조).
참고: 공변성이나 반공변성은 타입 변수의 속성이 아니라, 이 변수를 사용하여 정의된 제네릭 클래스의 속성입니다. 분산은 제네릭 타입에만 적용되며, 제네릭 함수에는 이러한 속성이 없습니다. 후자는 covariant 또는 contravariant 키워드 인자 없이 타입 변수만 사용하여 정의해야 합니다. 예를 들어 다음 예제는 괜찮습니다.:
from typing import TypeVar
class Employee: ...
class Manager(Employee): ...
E = TypeVar('E', bound=Employee)
def dump_employee(e: E) -> None: ...
dump_employee(Manager()) # OK
반면 다음 예제는 금지됩니다.:
B_co = TypeVar('B_co', covariant=True)
def bad_func(x: B_co) -> B_co: # Flagged as error by a type checker
...
수치 탑
PEP 3141은 Python의 수치 탑을 정의하며, 표준 라이브러리 모듈 numbers는 해당 추상 베이스 클래스(ABCs)(Number, Complex, Real, Rational 및 Integral)를 구현합니다. 이러한 ABCs에는 몇 가지 문제가 있지만, 내장 구체 수치 클래스인 complex, float 및 int는 어디에서나 사용됩니다(특히 뒤의 두 가지가 그렇습니다 :-).
사용자가 import numbers를 작성한 다음 numbers.Float등을 사용하도록 요구하는 대신, 이 PEP에서는 거의 그만큼 효과적인 간단한 단축 방법을 제안합니다. 인자가 float타입이라고 어노테이션되면 int타입의 인자를 허용하며, 마찬가지로 인자가 complex타입이라고 어노테이션되면 float또는 int타입의 인자를 허용합니다. 이는 해당 ABCs를 구현하는 클래스나 fractions.Fraction 클래스를 처리하지 않지만, 그러한 사용 사례는 극히 드물다고 생각합니다.
전방 참조
타입 힌트에 아직 정의되지 않은 이름이 포함된 경우, 나중에 해석되도록 해당 정의를 문자열 리터럴로 표현할 수 있습니다.
이러한 상황은 컨테이너 클래스를 정의할 때 흔히 발생하며, 정의 중인 클래스가 일부 메서드의 시그니처에 등장합니다. 예를 들어 다음 코드(간단한 이진 트리 구현의 시작 부분)는 작동하지 않습니다.:
class Tree:
def __init__(self, left: Tree, right: Tree):
self.left = left
self.right = right
이를 해결하기 위해 다음과 같이 작성합니다.:
class Tree:
def __init__(self, left: 'Tree', right: 'Tree'):
self.left = left
self.right = right
문자열 리터럴에는 유효한 Python 표현식이 포함되어야 하며(즉, compile(lit, '', 'eval')이 유효한 코드 객체여야 함), 모듈이 완전히 로드된 후 오류 없이 평가되어야 합니다. 이를 평가하는 로컬 및 전역 네임스페이스는 동일한 함수의 기본 인자가 평가되는 네임스페이스와 같아야 합니다.
또한 해당 표현식은 유효한 타입 힌트로 구문 분석될 수 있어야 합니다. 즉, 위의 Acceptable type hints 섹션의 규칙에 의해 제한됩니다.
문자열 리터럴을 타입 힌트의 일부로 사용하는 것도 허용됩니다. 예를 들면 다음과 같습니다.:
class Tree:
...
def leaves(self) -> List['Tree']:
...
전방 참조의 일반적인 사용 사례는 예를 들어 Django 모델이 시그니처에 필요한 경우입니다. 일반적으로 각 모델은 별도의 파일에 있으며, 다른 모델과 관련된 타입의 인자를 받는 메서드를 가집니다. Python에서 순환 임포트가 작동하는 방식 때문에 필요한 모든 모델을 직접 임포트할 수 없는 경우가 많습니다.:
# File models/a.py
from models.b import B
class A(Model):
def foo(self, b: B): ...
# File models/b.py
from models.a import A
class B(Model):
def bar(self, a: A): ...
# File main.py
from models.a import A
from models.b import B
main을 먼저 임포트한다고 가정하면, models/a.py에서 임포트되는 models/b.py의 from models.a import A줄에서 ImportError가 발생합니다. 이는 a가 클래스 A를 정의하기 전에 models/a.py를 임포트하기 때문입니다. 해결책은 모듈 전용 임포트로 전환하고 모델을 _module_._class_ 이름으로 참조하는 것입니다.:
# File models/a.py
from models import b
class A(Model):
def foo(self, b: 'b.B'): ...
# File models/b.py
from models import a
class B(Model):
def bar(self, a: 'a.A'): ...
# File main.py
from models.a import A
from models.b import B
유니온 타입
하나의 인자에 대해 예상되는 타입을 작고 제한된 집합으로 허용하는 경우가 흔하므로, Union이라는 새로운 특수 팩토리가 제공됩니다. 예제:
from typing import Union
def handle_employees(e: Union[Employee, Sequence[Employee]]) -> None:
if isinstance(e, Employee):
e = [e]
...
Union[T1, T2, ...]로 정의된 타입은 모든 T1, T2 등의 타입의 상위 타입이므로, 이러한 타입 중 하나의 멤버인 값은 Union[T1, T2, ...]로 어노테이션된 인자에 허용됩니다.
유니온 타입의 일반적인 사례 중 하나는 선택적 타입입니다. 기본적으로 모든 타입에서 None은 유효하지 않은 값이며, 함수 정의에서 None의 기본값이 제공된 경우는 예외입니다. 예제:
def handle_employee(e: Union[Employee, None]) -> None: ...
Union[T1, None]의 축약형으로 Optional[T1]을 작성할 수 있습니다. 예를 들어 위의 내용은 다음과 동등합니다.:
from typing import Optional
def handle_employee(e: Optional[Employee]) -> None: ...
이 PEP의 과거 버전에서는 기본값이 None일 때 이 코드에서처럼 타입 검사기가 선택적 타입이라고 가정하도록 허용했습니다.:
def handle_employee(e: Employee = None): ...
이는 다음과 동등한 것으로 처리되었을 것입니다.:
def handle_employee(e: Optional[Employee] = None) -> None: ...
이는 더 이상 권장되는 동작이 아닙니다. 타입 검사기는 선택적 타입을 명시적으로 지정하도록 요구하는 방향으로 나아가야 합니다.
유니언에서 싱글턴 타입 지원
싱글턴 인스턴스는 특정 특수 조건을 표시하는 데 자주 사용되며, 특히 변수가 None도 유효한 값으로 가질 수 있는 상황에서 사용됩니다. 예:
_empty = object()
def func(x=_empty):
if x is _empty: # default argument value
return 0
elif x is None: # argument was provided and it's None
return 1
else:
return x * 2
이러한 상황에서 정확한 타이핑을 가능하게 하려면 사용자는 표준 라이브러리에서 제공하는 enum.Enum 클래스와 함께 Union 타입을 사용하여 타입 오류를 정적으로 검출할 수 있도록 해야 합니다.:
from typing import Union
from enum import Enum
class Empty(Enum):
token = 0
_empty = Empty.token
def func(x: Union[int, None, Empty] = _empty) -> int:
boom = x * 42 # This fails type check
if x is _empty:
return 0
elif x is None:
return 1
else: # At this point typechecker knows that x can only have type int
return x * 2
Enum의 서브클래스는 더 이상 서브클래스로 만들 수 없으므로, 위 예제의 모든 분기에서 변수 x의 타입을 정적으로 추론할 수 있습니다. 싱글턴 객체가 둘 이상 필요한 경우에도 동일한 접근 방식을 적용할 수 있습니다. 값이 둘 이상인 열거형을 사용하면 됩니다.:
class Reason(Enum):
timeout = 1
error = 2
def process(response: Union[str, Reason] = '') -> str:
if response is Reason.timeout:
return 'TIMEOUT'
elif response is Reason.error:
return 'ERROR'
else:
# response can be only str, all other possible values exhausted
return 'PROCESSED: ' + response
Any 타입
특별한 종류의 타입으로 Any가 있습니다. 모든 타입은 Any와 일관됩니다. 이는 모든 값과 모든 메서드를 가진 타입으로 간주할 수 있습니다. Any와 내장 타입 object는 완전히 다르다는 점에 유의하십시오.
값의 타입이 object인 경우 타입 검사기는 해당 값에 대한 거의 모든 연산을 거부하며, 이를 더 특수화된 타입의 변수에 할당하거나 반환값으로 사용하는 것은 타입 오류입니다. 반면 값의 타입이 Any인 경우 타입 검사기는 해당 값에 대한 모든 연산을 허용하며, Any타입의 값은 더 제한적인 타입의 변수에 할당하거나 반환값으로 사용할 수 있습니다.
어노테이션이 없는 함수 매개변수에는 Any가 어노테이션된 것으로 가정합니다. 타입 매개변수를 지정하지 않고 제네릭 타입을 사용하면 해당 매개변수는 Any로 가정됩니다.:
from typing import Mapping
def use_map(m: Mapping) -> None: # Same as Mapping[Any, Any]
...
이 규칙은 Tuple에도 적용됩니다. 어노테이션 문맥에서는 Tuple[Any, ...]과 동등하고, 다시 tuple과 동등합니다. 또한 어노테이션에서 단독으로 사용된 Callable은 Callable[..., Any]와 동등하며, 다시 collections.abc.Callable과 동등합니다.:
from typing import Tuple, List, Callable
def check_args(args: Tuple) -> bool:
...
check_args(()) # OK
check_args((42, 'abc')) # Also OK
check_args(3.14) # Flagged as error by a type checker
# A list of arbitrary callables is accepted by this function
def apply_callbacks(cbs: List[Callable]) -> None:
...
NoReturn 타입
typing 모듈은 정상적으로 반환하지 않는 함수에 어노테이션을 지정하기 위한 특수 타입 NoReturn을 제공합니다. 예를 들어, 무조건 예외를 발생시키는 함수가 있습니다.:
from typing import NoReturn
def stop() -> NoReturn:
raise RuntimeError('no way')
NoReturn 어노테이션은 sys.exit와 같은 함수에 사용됩니다. 정적 타입 검사기는 NoReturn을 반환한다고 어노테이션된 함수가 암묵적으로든 명시적으로든 실제로 절대 반환하지 않도록 보장합니다.:
import sys
from typing import NoReturn
def f(x: int) -> NoReturn: # Error, f(0) implicitly returns None
if x != 0:
sys.exit(1)
또한 타입 검사기는 이러한 함수 호출 이후의 코드가 도달 불가능하다는 것을 인식하고 그에 따라 동작합니다.:
# continue from first example
def g(x: int) -> int:
if x > 0:
return x
stop()
return 'whatever works' # Error might be not reported by some checkers
# that ignore errors in unreachable blocks
NoReturn 타입은 함수의 반환 어노테이션으로만 유효하며, 다른 위치에 나타나면 오류로 간주됩니다.:
from typing import List, NoReturn
# All of the following are errors
def bad1(x: NoReturn) -> int:
...
bad2 = None # type: NoReturn
def bad3() -> List[NoReturn]:
...
클래스 객체의 타입
때로는 클래스 객체, 특히 주어진 클래스를 상속하는 클래스 객체를 다루고 싶을 수 있습니다. 클래스인 C를 사용하여 Type[C]로 표기할 수 있습니다. 명확히 하자면, C(어노테이션으로 사용되는 경우)는 C의 인스턴스를 가리키는 반면, Type[C]는 C의 서브클래스를 가리킵니다. (이는 object와 type 사이의 구분과 유사합니다.)
예를 들어, 다음과 같은 클래스들이 있다고 가정해 보겠습니다.:
class User: ... # Abstract base for User classes
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...
클래스 객체를 전달하면 이러한 클래스 중 하나의 인스턴스를 생성하는 함수가 있다고 가정해 보겠습니다.:
def new_user(user_class):
user = user_class()
# (Here we could write the user object to a database)
return user
Type[]를 사용하지 않으면 new_user()에 타입 어노테이션을 지정하는 최선의 방법은 다음과 같습니다.:
def new_user(user_class: type) -> User:
...
그러나 Type[]와 상한이 있는 타입 변수를 사용하면 훨씬 더 잘 처리할 수 있습니다.:
U = TypeVar('U', bound=User)
def new_user(user_class: Type[U]) -> U:
...
이제 User의 특정 서브클래스를 사용하여 new_user()를 호출하면 타입 검사기는 결과의 올바른 타입을 추론합니다.:
joe = new_user(BasicUser) # Inferred type is BasicUser
Type[C]에 해당하는 값은 특수 형식이 아니라 C의 서브타입인 실제 클래스 객체여야 합니다. 즉, 위 예제에서 예를 들어 new_user(Union[BasicUser, ProUser])를 호출하는 것은 타입 검사기에 의해 거부됩니다(유니온은 인스턴스화할 수 없으므로 런타임에도 실패합니다).
Type[]의 매개변수로 클래스의 유니온을 사용하는 것은 합법적이며, 예를 들면 다음과 같습니다.:
def new_non_team_user(user_class: Type[Union[BasicUser, ProUser]]):
user = new_user(user_class)
...
그러나 런타임에 전달되는 실제 인자는 여전히 구체적인 클래스 객체여야 합니다. 예를 들어 위 예제에서는 다음과 같습니다.:
new_non_team_user(ProUser) # OK
new_non_team_user(TeamUser) # Disallowed by type checker
Type[Any]도 지원됩니다(그 의미는 아래를 참조하십시오).
T가 타입 변수인 Type[T]는 클래스 메서드의 첫 번째 인자에 어노테이션을 지정할 때 허용됩니다(관련 절을 참조하십시오).
Tuple이나 Callable같은 기타 특수 구성체는 Type의 인자로 허용되지 않습니다.
이 기능에는 몇 가지 우려 사항이 있습니다. 예를 들어 new_user()가 user_class()를 호출하면, 이는 User의 모든 서브클래스가 생성자 시그니처에서 이를 지원해야 함을 의미합니다. 그러나 이는 Type[]에만 해당하는 것은 아닙니다. 클래스 메서드에도 유사한 우려 사항이 있습니다. 타입 검사기는 이러한 가정의 위반을 표시해야 하지만, 기본적으로 지정된 베이스 클래스(위 예제에서는 User)의 생성자 시그니처와 일치하는 생성자 호출은 허용해야 합니다. 복잡하거나 확장 가능한 클래스 계층 구조를 포함하는 프로그램에서는 팩토리 클래스 메서드를 사용하는 방식으로 이 문제를 처리할 수도 있습니다. 이 PEP의 향후 개정판에서는 이러한 우려 사항을 처리하는 더 나은 방법이 도입될 수 있습니다.
Type에 매개변수를 지정할 때는 정확히 하나의 매개변수가 필요합니다. 대괄호가 없는 일반 Type은 Type[Any]와 동등하며, 이는 다시 type(파이썬 메타클래스 계층 구조의 루트)과 동등합니다. 이러한 동등성 때문에 Class나 SubType같은 대안이 아니라 Type이라는 이름을 사용합니다. 이러한 대안은 이 기능을 논의하는 동안 제안된 것입니다. 이는 예를 들어 List와 list의 관계와 유사합니다.
Type[Any] (또는 Type 또는 type)의 동작과 관련하여, 이 타입을 가진 변수의 특성에 액세스하면 type에 정의된 특성과 메서드만 제공됩니다(예를 들어 __repr__()와 __mro__). 이러한 변수는 임의의 인자로 호출할 수 있으며, 반환 타입은 Any입니다.
Type은 그 매개변수에 대해 공변입니다. Type[Derived]가 Type[Base]의 서브타입이기 때문입니다.:
def new_pro_user(pro_user_class: Type[ProUser]):
user = new_user(pro_user_class) # OK
...
인스턴스 메서드와 클래스 메서드에 어노테이션 지정하기
대부분의 경우 클래스 메서드와 인스턴스 메서드의 첫 번째 인자에는 어노테이션을 지정할 필요가 없습니다. 인스턴스 메서드에서는 포함하는 클래스의 타입으로, 클래스 메서드에서는 포함하는 클래스 객체에 대응하는 타입 객체 타입으로 간주됩니다. 또한 인스턴스 메서드의 첫 번째 인자에는 타입 변수를 어노테이션으로 지정할 수 있습니다. 이 경우 반환 타입에서 동일한 타입 변수를 사용할 수 있으므로 해당 메서드가 제너릭 함수가 됩니다. 예를 들면 다음과 같습니다.:
T = TypeVar('T', bound='Copyable')
class Copyable:
def copy(self: T) -> T:
# return a copy of self
class C(Copyable): ...
c = C()
c2 = c.copy() # type here should be C
첫 번째 인자의 어노테이션에 Type[]를 사용하는 클래스 메서드에도 동일하게 적용됩니다.:
T = TypeVar('T', bound='C')
class C:
@classmethod
def factory(cls: Type[T]) -> T:
# make a new instance of cls
class D(C): ...
d = D.factory() # type here should be D
일부 타입 검사기는 이 사용에 제한을 적용할 수 있습니다. 예를 들어 사용되는 타입 변수에 적절한 상한을 요구할 수 있습니다(예제를 참조하십시오).
버전 및 플랫폼 검사
타입 검사기는 간단한 버전 및 플랫폼 검사를 이해할 수 있어야 합니다. 예를 들면 다음과 같습니다.:
import sys
if sys.version_info[0] >= 3:
# Python 3 specific definitions
else:
# Python 2 specific definitions
if sys.platform == 'win32':
# Windows specific definitions
else:
# Posix specific definitions
검사기가 "".join(reversed(sys.platform)) == "xunil"와 같은 난독화를 이해할 것이라고 기대하지 마십시오.
런타임 검사 또는 타입 검사?
때로는 타입 검사기(또는 다른 정적 분석 도구)가 확인해야 하지만 실행해서는 안 되는 코드가 있습니다. 이러한 상황을 위해 typing 모듈은 상수 TYPE_CHECKING을 정의하며, 이 상수는 타입 검사 중(또는 다른 정적 분석 중)에는 True로 간주되지만 런타임에는 False로 간주됩니다. 예를 들어:
import typing
if typing.TYPE_CHECKING:
import expensive_mod
def a_func(arg: 'expensive_mod.SomeClass') -> None:
a_var = arg # type: expensive_mod.SomeClass
...
(타입 어노테이션은 따옴표로 묶어 “전방 참조”로 만들어야 expensive_mod 참조를 인터프리터 런타임에서 숨길 수 있다는 점에 유의하십시오. # type 주석에서는 따옴표가 필요하지 않습니다.)
이 방법은 임포트 순환을 처리할 때도 유용할 수 있습니다.
임의의 인자 목록과 기본 인자 값
임의의 인자 목록에도 타입 어노테이션을 지정할 수 있으므로, 다음 정의는:
def foo(*args: str, **kwds: int): ...
허용되며, 예를 들어 다음의 모든 항목이 유효한 인자 타입을 사용하는 함수 호출을 나타낸다는 의미입니다.:
foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)
함수 foo의 본문에서 변수 args의 타입은 Tuple[str, ...]로 추론되고 변수 kwds의 타입은 Dict[str, int]로 추론됩니다.
스텁에서는 실제 기본값을 지정하지 않고 인자에 기본값이 있다고 선언하는 것이 유용할 수 있습니다. 예를 들어:
def foo(x: AnyStr, y: AnyStr = ...) -> AnyStr: ...
기본값은 어떤 형태여야 합니까? "", b"" 또는 None 중 어떤 선택지도 타입 제약 조건을 충족하지 못합니다.
이러한 경우 기본값을 리터럴 줄임표로 지정할 수 있으며, 즉 위의 예제가 실제로 작성할 내용입니다.
위치 전용 인자
일부 함수는 인자를 위치 인자로만 받도록 설계되었으며, 호출자가 해당 인자를 키워드로 제공하기 위해 인자 이름을 사용하지 않을 것이라고 가정합니다. 이름이 __로 시작하는 모든 인자는 위치 전용으로 간주되지만, 이름이 __로 끝나는 경우는 예외입니다.:
def quux(__x: int, __y__: int = 0) -> None: ...
quux(3, __y__=1) # This call is fine.
quux(__x=3) # This call is an error.
제너레이터 함수와 코루틴에 어노테이션 지정하기
제너레이터 함수의 반환 타입은 typing.py 모듈에서 제공하는 제네릭 타입 Generator[yield_type, send_type, return_type]로 어노테이션을 지정할 수 있습니다.:
def echo_round() -> Generator[int, float, str]:
res = yield
while res:
res = yield round(res)
return 'OK'
관련 PEP 492에서 도입된 코루틴은 일반 함수와 동일한 구문으로 어노테이션됩니다. 그러나 반환 타입 어노테이션은 코루틴 타입이 아니라 await표현식의 타입에 해당합니다.:
async def spam(ignored: int) -> str:
return 'spam'
async def foo() -> None:
bar = await spam(42) # type: str
typing.py 모듈은 send() 및 throw() 메서드도 지원하는 어웨이터블을 지정하기 위해 ABC collections.abc.Coroutine의 제네릭 버전을 제공합니다. 타입 변수의 분산과 순서는 Generator의 경우와 동일하며, 예를 들어 Coroutine[T_co, T_contra, V_co]입니다.:
from typing import List, Coroutine
c = None # type: Coroutine[List[str], str, int]
...
x = c.send('hi') # type: List[str]
async def bar() -> None:
x = await c # type: int
또한 이 모듈은 더 정확한 타입을 지정할 수 없는 상황을 위해 제네릭 ABC Awaitable, AsyncIterable 및 AsyncIterator를 제공합니다.:
def op() -> typing.Awaitable[str]:
if cond:
return spam(42)
else:
return asyncio.Future(...)
함수 어노테이션의 다른 용도와의 호환성
함수 어노테이션에는 타입 힌팅과 호환되지 않는 기존 또는 잠재적 사용 사례가 여러 가지 있습니다. 이러한 사용 사례는 정적 타입 검사기를 혼란스럽게 할 수 있습니다. 그러나 타입 힌팅 어노테이션은 런타임 동작이 없으므로(어노테이션 표현식을 평가하고 함수 객체의 __annotations__ 속성에 어노테이션을 저장하는 것 제외), 이로 인해 프로그램이 잘못되는 것은 아니며 타입 검사기가 불필요한 경고나 오류를 발생시킬 수 있을 뿐입니다.
타입 힌팅의 적용 대상에서 제외해야 하는 프로그램 일부를 표시하려면 다음 중 하나 이상을 사용할 수 있습니다.
# type: ignore주석입니다;- 클래스 또는 함수에 대한
@no_type_check데코레이터입니다; @no_type_check_decorator로 표시된 사용자 지정 클래스 또는 함수 데코레이터입니다.
자세한 내용은 이후 절을 참조하십시오.
오프라인 타입 검사와 최대한 호환되도록 하려면, 어노테이션에 의존하는 인터페이스를 변경하여 다른 메커니즘(예를 들어 데코레이터)으로 전환하는 것이 나중에는 좋은 방법일 수 있습니다. 그러나 Python 3.5에서는 이렇게 해야 할 압박이 없습니다. 아래의 Rejected Alternatives 절에 있는 더 긴 논의도 참조하십시오.
타입 주석입니다.
변수를 특정 타입으로 명시적으로 표시하기 위한 일급 구문 지원은 이 PEP에 추가되지 않습니다. 복잡한 경우의 타입 추론을 돕기 위해 다음 형식의 주석을 사용할 수 있습니다.:
x = [] # type: List[Employee]
x, y, z = [], [], [] # type: List[int], List[int], List[str]
x, y, z = [], [], [] # type: (List[int], List[int], List[str])
a, b, *c = range(5) # type: float, float, List[float]
x = [1, 2] # type: List[int]
타입 주석은 변수 정의를 포함하는 명령문의 마지막 줄에 작성해야 합니다. with 문과 for 문에도 콜론 바로 뒤에 작성할 수 있습니다.
with 문과 for 문의 타입 주석 예제입니다.:
with frobnicate() as foo: # type: int
# Here foo is an int
...
for x, y in points: # type: float, float
# Here x and y are floats
...
스텁에서는 초기값을 지정하지 않고 변수의 존재를 선언하는 것이 유용할 수 있습니다. 이는 PEP 526 변수 어노테이션 구문을 사용하여 수행할 수 있습니다.:
from typing import IO
stream: IO[str]
위 구문은 모든 Python 버전의 스텁에서 허용됩니다. 그러나 Python 3.5 이하 버전의 비스텁 코드에는 특별한 경우가 있습니다.:
from typing import IO
stream = None # type: IO[str]
타입 검사기는 이에 대해 불평하지 않아야 하며(값 None이 주어진 타입과 일치하지 않더라도), 추론된 타입을 Optional[...]로 변경해서도 안 됩니다(None을 기본값으로 사용하는 어노테이션이 지정된 인자에 이 규칙이 적용되더라도). 여기서는 다른 코드가 변수에 적절한 타입의 값이 할당되도록 보장하며, 모든 사용처에서 해당 변수가 주어진 타입을 가진다고 가정할 수 있다는 전제입니다.
# type: ignore 주석은 오류가 가리키는 줄에 작성해야 합니다.:
import http.client
errors = {
'not_found': http.client.NOT_FOUND # type: ignore
}
파일의 맨 위에서 독스트링, 임포트 또는 기타 실행 가능한 코드보다 앞선 줄에 # type: ignore 주석만 단독으로 작성하면 파일의 모든 오류가 무시됩니다. 빈 줄과 셰뱅 줄 및 코딩 쿠키와 같은 다른 주석은 # type: ignore 주석보다 앞에 올 수 있습니다.
경우에 따라 타입 주석과 같은 줄에 린팅 도구 또는 다른 주석이 필요할 수 있습니다. 이러한 경우 타입 주석은 다른 주석 및 린팅 표식보다 앞에 와야 합니다.
# type: ignore # <comment or other marker>
타입 힌팅이 전반적으로 유용한 것으로 입증되면, 이후 Python 버전에서 변수 타입 지정을 위한 구문이 제공될 수 있습니다. (UPDATE: 이 구문은 PEP 526을 통해 Python 3.6에 추가되었습니다.)
캐스트입니다.
때때로 타입 검사기에는 다른 종류의 힌트가 필요할 수 있습니다. 프로그래머는 타입 검사기가 추론할 수 있는 것보다 표현식의 타입이 더 제한적이라는 사실을 알고 있을 수 있습니다. 예를 들어 다음과 같습니다.:
from typing import List, cast
def find_first_str(a: List[object]) -> str:
index = next(i for i, x in enumerate(a) if isinstance(x, str))
# We only get here if there's at least one string in a
return cast(str, a[index])
일부 타입 검사기는 a[index]의 타입이 str이라는 것을 추론하지 못하고 object 또는 Any만 추론할 수 있지만, 우리는 코드가 해당 지점까지 실행되었다면 이것이 문자열이어야 한다는 것을 알고 있습니다. cast(t, x) 호출은 x의 타입이 t라는 점을 확신한다고 타입 검사기에 알립니다. 런타임에 캐스트는 항상 표현식을 변경하지 않고 그대로 반환합니다. 즉, 타입을 검사하지 않으며 값을 변환하거나 강제 변환하지도 않습니다.
캐스트는 타입 주석과 다릅니다(이전 절을 참조하십시오). 타입 주석을 사용할 때에도 타입 검사기는 추론된 타입이 명시된 타입과 일치하는지 확인해야 합니다. 캐스트를 사용할 때 타입 검사기는 프로그래머를 무조건 믿어야 합니다. 또한 캐스트는 표현식에서 사용할 수 있지만, 타입 주석은 할당에만 적용됩니다.
NewType 도우미 함수
프로그래머가 간단한 클래스를 만들어 논리적 오류를 방지하려는 경우도 있습니다. 예를 들어:
class UserId(int):
pass
def get_by_user_id(user_id: UserId):
...
그러나 이 접근 방식은 런타임 오버헤드를 발생시킵니다. 이를 방지하기 위해 typing.py는 런타임 오버헤드가 거의 없이 간단한 고유 타입을 생성하는 도우미 함수 NewType를 제공합니다. 정적 타입 검사기에서 Derived = NewType('Derived', Base)는 대략 다음 정의와 동등합니다.:
class Derived(Base):
def __init__(self, _x: Base) -> None:
...
반면 런타임에는 NewType('Derived', Base)가 인수를 그대로 반환하는 더미 함수를 반환합니다. 타입 검사기는 UserId가 필요한 곳에서 int로부터의 명시적 캐스트를 요구하지만, int가 필요한 곳에서 UserId로부터의 암시적 캐스트는 허용합니다. 예제:
UserId = NewType('UserId', int)
def name_by_id(user_id: UserId) -> str:
...
UserId('user') # Fails type check
name_by_id(42) # Fails type check
name_by_id(UserId(42)) # OK
num = UserId(5) + 1 # type: int
NewType는 새 고유 타입의 이름과 베이스 클래스라는 정확히 두 개의 인수를 받습니다. 후자는 적절한 클래스여야 합니다(즉, Union등과 같은 타입 생성자가 아니어야 합니다). 또는 NewType를 호출하여 생성된 또 다른 고유 타입이어야 합니다. NewType가 반환하는 함수는 인수를 하나만 받습니다. 이는 베이스 클래스의 인스턴스를 받는 생성자 하나만 지원하는 것과 같습니다(위 참조). 예제:
class PacketId:
def __init__(self, major: int, minor: int) -> None:
self._major = major
self._minor = minor
TcpPacketId = NewType('TcpPacketId', PacketId)
packet = PacketId(100, 100)
tcp_packet = TcpPacketId(packet) # OK
tcp_packet = TcpPacketId(127, 0) # Fails in type checker and at runtime
함수 객체는 이러한 연산을 지원하지 않으므로 NewType('Derived', Base)에서는 isinstance와 issubclass는 물론 서브클래싱도 실패합니다.
스텁 파일
스텁 파일은 런타임이 아니라 타입 검사기에서만 사용하기 위한 타입 힌트를 포함하는 파일입니다. 스텁 파일에는 여러 사용 사례가 있습니다.
- 확장 모듈
- 아직 타입 힌트를 추가하지 않은 작성자의 서드파티 모듈
- 아직 타입 힌트가 작성되지 않은 표준 라이브러리 모듈
- Python 2와 3에서 호환되어야 하는 모듈
- 다른 목적으로 어노테이션을 사용하는 모듈
스텁 파일은 일반 Python 모듈과 동일한 구문을 사용합니다. typing모듈에는 스텁 파일에서 다른 기능이 하나 있습니다. 바로 아래에서 설명하는 @overload 데코레이터입니다.
타입 검사기는 스텁 파일에서 함수 시그니처만 검사해야 합니다. 스텁 파일의 함수 본문은 단일 줄임표(...)로만 작성하는 것이 좋습니다.
타입 검사기는 스텁 파일에 대해 구성 가능한 검색 경로를 가져야 합니다. 스텁 파일을 찾으면 타입 검사기는 이에 대응하는 “실제” 모듈을 읽지 않아야 합니다.
스텁 파일은 문법적으로 유효한 Python 모듈이지만, 해당 실제 모듈과 같은 디렉터리에서 스텁 파일을 유지 관리할 수 있도록 .pyi확장자를 사용합니다. 이는 스텁 파일에서 런타임 동작을 기대해서는 안 된다는 개념도 강화합니다.
스텁 파일에 대한 추가 참고 사항:
- 스텁으로 가져온 모듈과 변수는 가져오기에
import ... as ...형식 또는 이에 상응하는from ... import ... as ...형식을 사용하지 않는 한 스텁에서 내보낸 것으로 간주되지 않습니다. (업데이트: 명확히 하자면,X as X형식을 사용해 가져온 이름만 내보내려는 것이며, 즉as앞뒤의 이름이 같아야 합니다.) - 그러나 이전 항목의 예외로,
from ... import *을 사용해 스텁으로 가져온 모든 객체는 내보낸 것으로 간주됩니다. (이를 통해 Python 버전에 따라 달라질 수 있는 지정된 모듈의 모든 객체를 더 쉽게 다시 내보낼 수 있습니다.) - normal Python files에서와 마찬가지로, 하위 모듈은 가져올 때 자동으로 상위 모듈의 내보낸 속성이 됩니다. 예를 들어
spam패키지가 다음 디렉터리 구조를 갖는 경우:spam/ __init__.pyi ham.pyi
__init__.pyi에from . import ham또는from .ham import Ham과 같은 줄이 포함되어 있다면,ham은spam의 내보낸 속성입니다. - 스텁 파일은 불완전할 수 있습니다. 타입 검사기가 이를 인식하도록 하려면 파일에 다음 코드를 포함할 수 있습니다.:
def __getattr__(name) -> Any: ...
따라서 스텁에 정의되지 않은 모든 식별자는
Any타입으로 간주됩니다.
함수/메서드 오버로딩
@overload 데코레이터를 사용하면 여러 가지 서로 다른 인자 타입 조합을 지원하는 함수와 메서드를 설명할 수 있습니다. 이 패턴은 내장 모듈과 타입에서 자주 사용됩니다. 예를 들어 bytes 타입의 __getitem__() 메서드는 다음과 같이 설명할 수 있습니다.:
from typing import overload
class bytes:
...
@overload
def __getitem__(self, i: int) -> int: ...
@overload
def __getitem__(self, s: slice) -> bytes: ...
이 설명은 유니언을 사용해서는 불가능한 방식으로 더 정확합니다. (유니언으로는 인자 타입과 반환 타입 사이의 관계를 표현할 수 없습니다.):
from typing import Union
class bytes:
...
def __getitem__(self, a: Union[int, slice]) -> Union[int, bytes]: ...
@overload 데코레이터가 유용한 또 다른 예는 내장 map() 함수의 타입으로, 호출 가능 객체의 타입에 따라 서로 다른 수의 인자를 받습니다.:
from typing import Callable, Iterable, Iterator, Tuple, TypeVar, overload
T1 = TypeVar('T1')
T2 = TypeVar('T2')
S = TypeVar('S')
@overload
def map(func: Callable[[T1], S], iter1: Iterable[T1]) -> Iterator[S]: ...
@overload
def map(func: Callable[[T1, T2], S],
iter1: Iterable[T1], iter2: Iterable[T2]) -> Iterator[S]: ...
# ... and we could add more items to support more than two iterables
map(None, ...)을 지원하도록 항목을 쉽게 추가할 수도 있다는 점에 유의하십시오.:
@overload
def map(func: None, iter1: Iterable[T1]) -> Iterable[T1]: ...
@overload
def map(func: None,
iter1: Iterable[T1],
iter2: Iterable[T2]) -> Iterable[Tuple[T1, T2]]: ...
위와 같이 @overload 데코레이터를 사용하는 방식은 스텁 파일에 적합합니다. 일반 모듈에서는 일련의 @overload-데코레이터가 적용된 정의 뒤에 정확히 하나의 non-@overload-데코레이터가 적용된 정의가 와야 합니다. (동일한 함수/메서드에 대한 정의여야 합니다.) @overload-데코레이터가 적용된 정의는 타입 검사기만을 위한 것입니다. 이러한 정의는 non-@overload-데코레이터가 적용된 정의로 덮어써지기 때문이며, 후자는 런타임에 사용되지만 타입 검사기에서는 무시되어야 합니다. 런타임에 @overload-데코레이터가 적용된 함수를 직접 호출하면 NotImplementedError가 발생합니다. 다음은 유니언이나 타입 변수를 사용해 쉽게 표현할 수 없는 스텁이 아닌 오버로드의 예입니다.:
@overload
def utf8(value: None) -> None:
pass
@overload
def utf8(value: bytes) -> bytes:
pass
@overload
def utf8(value: unicode) -> bytes:
pass
def utf8(value):
<actual implementation>
참고: 이 구문을 사용해 다중 디스패치 구현을 제공할 수는 있지만, 이를 구현하려면 sys._getframe()을 사용해야 하며 이는 바람직하지 않은 방식으로 여겨집니다. 또한 효율적인 다중 디스패치 메커니즘을 설계하고 구현하기는 어렵기 때문에, 이전 시도는 functools.singledispatch()를 사용하는 방식으로 대체되고 포기되었습니다. (PEP 443의 특히 “Alternative approaches” 섹션을 참조하십시오.) 향후 만족스러운 다중 디스패치 설계를 고안할 수도 있지만, 그러한 설계가 스텁 파일의 타입 힌트에 정의된 오버로딩 구문에 의해 제약받기를 원하지는 않습니다. 두 기능이 서로 독립적으로 발전할 가능성도 있습니다. (타입 검사기에서의 오버로딩은 런타임의 다중 디스패치와 사용 사례 및 요구 사항이 다르기 때문입니다. 예를 들어 후자는 제네릭 타입을 지원하지 않을 가능성이 큽니다.)
@overload 데코레이터를 사용하는 대신 제약이 있는 TypeVar 타입을 사용할 수 있는 경우가 많습니다. 예를 들어 이 스텁 파일의 concat1 및 concat2 정의는 동등합니다.:
from typing import TypeVar, Text
AnyStr = TypeVar('AnyStr', Text, bytes)
def concat1(x: AnyStr, y: AnyStr) -> AnyStr: ...
@overload
def concat2(x: str, y: str) -> str: ...
@overload
def concat2(x: bytes, y: bytes) -> bytes: ...
그러나 위의 map 또는 bytes.__getitem__ 같은 일부 함수는 타입 변수를 사용해 정확하게 표현할 수 없습니다. 하지만 @overload와 달리 타입 변수는 스텁 파일 외부에서도 사용할 수 있습니다. 특수한 스텁 전용 지위 때문에, 타입 변수가 충분하지 않은 경우에만 @overload를 사용하도록 권장합니다.
AnyStr와 같은 타입 변수와 @overload를 사용하는 것의 또 다른 중요한 차이점은 전자는 제네릭 클래스 타입 매개변수의 제약 조건을 정의하는 데에도 사용할 수 있다는 점입니다. 예를 들어, 제네릭 클래스 typing.IO의 타입 매개변수에는 제약이 적용되며 (IO[str], IO[bytes] 및 IO[Any]만 유효합니다):
class IO(Generic[AnyStr]): ...
스터브 파일 저장 및 배포
스터브 파일을 저장하고 배포하는 가장 쉬운 방법은 같은 디렉터리에서 Python 모듈과 나란히 배치하는 것입니다. 이렇게 하면 프로그래머와 도구가 모두 쉽게 찾을 수 있습니다. 그러나 패키지 유지 관리자는 패키지에 타입 힌트를 추가하지 않아도 되므로, PyPI에서 pip로 설치할 수 있는 서드파티 스터브도 지원합니다. 이 경우 이름 지정, 버전 관리, 설치 경로라는 세 가지 문제를 고려해야 합니다.
이 PEP에서는 서드파티 스터브 파일 패키지에 사용해야 하는 명명 체계에 대한 권고를 제시하지 않습니다. 예를 들어 Django 패키지와 마찬가지로, 패키지의 인기도를 기반으로 발견 가능성이 결정되기를 기대합니다.
서드파티 스터브는 호환되는 소스 패키지의 가장 낮은 버전을 사용하여 버전을 지정해야 합니다. 예를 들어, FooPackage에는 1.0, 1.1, 1.2, 1.3, 2.0, 2.1, 2.2 버전이 있습니다. 1.1, 2.0 및 2.2 버전에서 API 변경 사항이 있습니다. 스터브 파일 패키지 유지 관리자는 모든 버전에 대한 스터브를 자유롭게 릴리스할 수 있지만, 최종 사용자가 모든 버전에 대해 타입 검사를 수행할 수 있도록 하려면 최소한 1.0, 1.1, 2.0 및 2.2가 필요합니다. 이는 사용자가 스터브의 가장 가까운 낮거나 같은 버전이 호환된다는 것을 알고 있기 때문입니다. 제시된 예에서 FooPackage 1.3의 경우 사용자는 스터브 버전 1.1을 선택합니다.
사용자가 사용 가능한 “최신” 소스 패키지를 사용하기로 결정한 경우, 스터브 파일도 자주 업데이트된다면 “최신” 스터브 파일을 사용해도 일반적으로 작동합니다.
서드파티 스터브 패키지는 스터브를 저장할 위치를 자유롭게 선택할 수 있습니다. 타입 검사기는 PYTHONPATH를 사용하여 해당 스터브를 검색해야 합니다. 항상 검사되는 기본 대체 디렉터리로 shared/typehints/pythonX.Y/가 있습니다 (설치된 버전뿐만 아니라 타입 검사기가 결정한 어떤 PythonX.Y에 대한 디렉터리입니다). 환경마다 특정 Python 버전에 대해 설치할 수 있는 패키지는 하나뿐이므로, 해당 디렉터리 아래에서는 추가적인 버전 관리가 수행되지 않습니다 (site-packages에서 pip로 수행하는 일반 디렉터리 설치와 같습니다). 스터브 파일 패키지 작성자는 setup.py에 다음과 같은 코드 조각을 사용할 수 있습니다.:
...
data_files=[
(
'shared/typehints/python{}.{}'.format(*sys.version_info[:2]),
pathlib.Path(SRC_PATH).glob('**/*.pyi'),
),
],
...
(업데이트: 2018년 6월부터 서드파티 패키지의 타입 힌트를 배포하는 권장 방식이 변경되었습니다. 다음 절에서 설명하는 typeshed와 더불어 이제 타입 힌트를 배포하기 위한 표준인 PEP 561이 마련되었습니다. 이를 통해 스터브가 포함된 별도 설치 가능 패키지, 패키지의 실행 코드와 동일한 배포물에 포함된 스터브 파일, 인라인 타입 힌트를 지원하며, 후자의 두 옵션은 패키지에 py.typed이라는 파일을 포함하여 활성화합니다.)
Typeshed 저장소
유용한 스터브를 수집하는 공유 저장소가 있습니다. 여기 수집된 스터브에 관한 정책은 별도로 결정되며 저장소 문서에 보고됩니다. 패키지 소유자가 해당 스터브를 제외해 달라고 명시적으로 요청한 경우에는 그 패키지의 스터브가 여기에 포함되지 않는다는 점에 유의하십시오.
예외
명시적으로 발생시킨 예외를 나열하는 구문은 제안하지 않습니다. 현재 이 기능의 유일하게 알려진 사용 사례는 문서화이며, 이 경우에는 해당 정보를 독스트링에 넣는 것이 좋습니다.
typing 모듈
정적 타입 검사의 사용 범위를 Python 3.5 및 이전 버전으로 확장하려면 통일된 네임스페이스가 필요합니다. 이를 위해 표준 라이브러리에 typing이라는 새 모듈을 도입합니다.
타입을 구성하기 위한 기본 구성 요소(예: Any), 내장 컬렉션의 제네릭 변형을 나타내는 타입(예: List), 제네릭 컬렉션 ABC를 나타내는 타입(예: Sequence) 및 소수의 편의 정의를 정의합니다.
Any, Union과 같은 특수 타입 구성 요소와 TypeVar를 사용하여 정의된 타입 변수는 타입 어노테이션 컨텍스트에서만 지원되며, Generic은 베이스 클래스로만 사용할 수 있다는 점에 유의하십시오. 이러한 모든 구성 요소(매개변수화되지 않은 제네릭은 제외)는 isinstance 또는 issubclass에 나타나면 TypeError를 발생시킵니다.
기본 구성 요소:
- Any는
def get(key: str) -> Any: ...와 같이 사용합니다. - Union은
Union[Type1, Type2, Type3]으로 사용합니다. - 호출 가능 객체는
Callable[[Arg1Type, Arg2Type], ReturnType]으로 사용합니다. - Tuple은 요소 유형을 나열하여 사용하며, 예를 들어
Tuple[int, int, str]과 같습니다. 빈 튜플은Tuple[()]로 타입을 지정할 수 있습니다. 임의 길이의 동일한 타입을 갖는 튜플은 하나의 타입과 줄임표를 사용하여 표현할 수 있습니다. 예를 들어Tuple[int, ...]입니다. 여기의...은 구문의 일부인 리터럴 줄임표입니다. - TypeVar는
X = TypeVar('X', Type1, Type2, Type3)또는 단순히Y = TypeVar('Y')로 사용합니다. - Generic은 사용자 정의 제네릭 클래스를 생성하는 데 사용합니다.
- Type은 클래스 객체에 어노테이션을 지정하는 데 사용합니다.
내장 컬렉션의 제네릭 변형:
- Dict는
Dict[key_type, value_type]로 사용합니다. - DefaultDict는
DefaultDict[key_type, value_type]로 사용하며,collections.defaultdict의 제네릭 변형입니다. - List는
List[element_type]로 사용합니다. - Set은
Set[element_type]로 사용합니다. 아래의AbstractSet에 대한 설명을 참조하십시오. - FrozenSet은
FrozenSet[element_type]로 사용합니다.
참고: Dict, DefaultDict, List, Set 및 FrozenSet은 주로 반환값에 어노테이션을 추가하는 데 유용합니다. 인자에는 아래에 정의된 추상 컬렉션 타입, 예를 들어 Mapping, Sequence 또는 AbstractSet을 우선적으로 사용하십시오.
컨테이너 ABC의 제네릭 변형(및 일부 비컨테이너):
- Awaitable
- AsyncIterable
- AsyncIterator
- ByteString
- 호출 가능 객체(완전성을 위해 여기에 나열합니다)입니다.
- Collection
- Container
- ContextManager
- 코루틴입니다.
- 제너레이터는
Generator[yield_type, send_type, return_type]로 사용합니다. 제너레이터 함수의 반환값을 나타냅니다.Iterable의 서브타입이며,send()메서드가 허용하는 타입에 대한 추가 타입 변수를 가집니다(이 변수에 대해 반공변입니다. 즉,Employee인스턴스를 보내는 것을 허용하는 제너레이터는Manager인스턴스를 보내는 것을 허용하는 제너레이터가 필요한 컨텍스트에서 유효합니다). 또한 제너레이터의 반환 타입에 대한 타입 변수도 가집니다. - Hashable(제네릭은 아니지만 완전성을 위해 포함됩니다)
- ItemsView
- Iterable
- 이터레이터입니다.
- KeysView
- 매핑입니다.
- MappingView
- MutableMapping
- MutableSequence
- MutableSet
- Sequence
- Set이며,
AbstractSet으로 이름이 변경되었습니다. 이 이름 변경이 필요했던 이유는typing모듈에서Set이 제네릭이 적용된set()을 의미하기 때문입니다. - Sized(제네릭은 아니지만 완전성을 위해 포함됩니다)
- ValuesView
단일 특수 메서드를 검사하는 몇 가지 일회성 타입이 정의되어 있습니다(Hashable 또는 Sized와 유사합니다):
__reversed__를 검사하는 Reversible__abs__를 검사하는 SupportsAbs__complex__를 검사하는 SupportsComplex__float__을 검사하는 SupportsFloat__int__를 검사하는 SupportsInt__round__를 검사하는 SupportsRound__bytes__를 검사하는 SupportsBytes
편의상 제공되는 정의입니다:
- Optional은
Optional[t] == Union[t, None]으로 정의됩니다. - Python 3에서
str의 간단한 별칭이며, Python 2에서unicode의 간단한 별칭입니다. TypeVar('AnyStr', Text, bytes)로 정의됩니다.NamedTuple(type_name, [(field_name, field_type), ...])로 사용되며collections.namedtuple(type_name, [field_name, ...])과 동등한 NamedTuple입니다. 이는 명명된 튜플 타입 필드의 타입을 선언하는 데 유용합니다.- 런타임 오버헤드를 거의 발생시키지 않으면서 고유한 타입을 만드는 데 사용되는 NewType은
UserId = NewType('UserId', int)로 사용합니다. - 앞에서 설명한 cast()입니다.
- 아래에서 설명하는, 클래스 또는 함수별로 타입 검사를 비활성화하는 데코레이터인 @no_type_check입니다.
- 아래에서 설명하는,
@no_type_check와 동일한 의미를 갖는 자신만의 데코레이터를 만드는 데코레이터인 @no_type_check_decorator입니다. - 위에서 설명한 스텁 파일에서 사용하기 위한 타입 검사 중에만 사용할 수 있는 데코레이터인 @type_check_only입니다. 런타임에는 클래스 또는 함수를 사용할 수 없는 것으로 표시합니다.
- 앞에서 설명한 @overload입니다.
- 함수 또는 메서드에서 타입 힌트를 가져오는 유틸리티 함수인 get_type_hints()입니다. 함수 또는 메서드 객체가 주어지면,
__annotations__와 동일한 형식의 딕셔너리를 반환하지만, 문자열 리터럴로 주어진 전방 참조를 원래 함수 또는 메서드 정의의 컨텍스트에서 표현식으로 평가합니다. - 런타임에는
False이지만 타입 검사기에는True인 TYPE_CHECKING입니다.
I/O 관련 타입은 다음과 같습니다.
AnyStr를 제네릭으로 사용하는 IO입니다.IO[bytes]의 단순한 서브타입인 BinaryIO입니다.IO[str]의 단순한 서브타입인 TextIO입니다.
정규 표현식 및 re 모듈과 관련된 타입은 다음과 같습니다.
re.match()와re.compile()의 결과 타입인 Match 및 Pattern입니다.AnyStr를 제네릭으로 사용합니다.
Python 2.7 및 양쪽 버전을 아우르는 코드에 권장되는 구문입니다.
일부 도구는 Python 2.7과 호환되어야 하는 코드에서 타입 어노테이션을 지원하려고 할 수 있습니다. 이를 위해 이 PEP에서는 함수 어노테이션을 # type: 주석에 배치하는 권장 확장 기능을 제안합니다. 다만 필수 사항은 아닙니다. 이러한 주석은 함수 헤더 바로 뒤에, 독스트링 앞에 배치해야 합니다. 예를 들어 다음 Python 3 코드가 있습니다.:
def embezzle(self, account: str, funds: int = 1000000, *fake_receipts: str) -> None:
"""Embezzle funds from account using fake receipts."""
<code goes here>
다음과 동등합니다.:
def embezzle(self, account, funds=1000000, *fake_receipts):
# type: (str, int, *str) -> None
"""Embezzle funds from account using fake receipts."""
<code goes here>
메서드에는 self에 대한 타입이 필요하지 않다는 점에 유의하십시오.
인자가 없는 메서드라면 다음과 같이 작성합니다.:
def load_cache(self):
# type: () -> bool
<code>
때로는 인자 타입을 아직 지정하지 않고 함수 또는 메서드의 반환 타입만 지정하려는 경우가 있습니다. 이를 명시적으로 지원하기 위해 인자 목록을 생략 부호로 대체할 수 있습니다. 예시는 다음과 같습니다.:
def send_email(address, sender, cc, bcc, subject, body):
# type: (...) -> bool
"""Send an email message. Return True if successful."""
<code>
때로는 매개변수 목록이 길어서 해당 타입을 하나의 # type: 주석으로 지정하기가 번거로울 수 있습니다. 이를 위해 인자를 한 줄에 하나씩 나열하고, 인자에 대응하는 쉼표가 있다면 그 뒤에 줄마다 # type: 주석을 추가할 수 있습니다. 반환 타입을 지정하려면 줄임표 구문을 사용하십시오. 반환 타입 지정은 필수가 아니며 모든 인자에 타입을 지정할 필요도 없습니다. # type: 주석이 있는 줄에는 인자가 정확히 하나만 있어야 합니다. 마지막 인자(있는 경우)의 타입 주석은 닫는 괄호 앞에 와야 합니다. 예제:
def send_email(address, # type: Union[str, List[str]]
sender, # type: str
cc, # type: Optional[List[str]]
bcc, # type: Optional[List[str]]
subject='',
body=None # type: List[str]
):
# type: (...) -> bool
"""Send an email message. Return True if successful."""
<code>
참고:
- 이 구문을 지원하는 도구는 검사 중인 Python 버전과 관계없이 이 구문을 지원해야 합니다. 이는 Python 2와 Python 3에 걸쳐 있는 코드를 지원하는 데 필요합니다.
- 인자나 반환 값에 타입 어노테이션과 타입 주석을 모두 지정할 수는 없습니다.
- 단축 형식(예:
# type: (str, int) -> None)을 사용할 때는 인스턴스 메서드와 클래스 메서드의 첫 번째 인자를 제외한 모든 인자를 빠짐없이 지정해야 합니다(이 인자들은 보통 생략하지만 포함해도 됩니다). - 단축 형식에서는 반환 타입이 필수입니다. Python 3에서 일부 인자나 반환 타입을 생략한다면 Python 2 표기에서는
Any를 사용해야 합니다. - 단축 형식을 사용할 때
*args및**kwds의 경우 해당 타입 어노테이션 앞에 별표를 1개 또는 2개 붙이십시오. (Python 3 어노테이션과 마찬가지로 여기의 어노테이션은 특수 인자 값인args또는kwds로 받는 튜플/딕셔너리의 타입이 아니라 개별 인자 값의 타입을 나타냅니다.) - 다른 타입 주석과 마찬가지로 어노테이션에 사용되는 모든 이름은 해당 어노테이션을 포함하는 모듈에서 임포트되거나 정의되어 있어야 합니다.
- 단축 형식을 사용할 때 전체 어노테이션은 한 줄이어야 합니다.
- 단축 형식은 닫는 괄호와 같은 줄에 올 수도 있습니다. 예를 들면 다음과 같습니다.:
def add(a, b): # type: (int, int) -> int return a + b
- 잘못 배치된 타입 주석은 타입 검사기에 의해 오류로 표시됩니다. 필요한 경우 이러한 주석을 두 번 주석 처리할 수 있습니다. 예를 들면 다음과 같습니다.:
def f(): '''Docstring''' # type: () -> None # Error! def g(): '''Docstring''' # # type: () -> None # This is OK
Python 2.7 코드를 검사할 때 타입 검사기는 int 및 long 타입을 동등하게 취급해야 합니다. Text로 타입이 지정된 매개변수의 경우 str 및 unicode 타입의 인자를 모두 허용해야 합니다.
거부된 대안
이 PEP의 초기 초안에 대해 논의하는 동안 여러 이의가 제기되었고 대안이 제안되었습니다. 여기에서는 이러한 이의 중 일부를 논의하고 이를 거부하는 이유를 설명합니다.
몇 가지 주요 이의가 제기되었습니다.
제네릭 타입 매개변수에는 어떤 괄호를 사용합니까?
대부분의 사람은 C++, Java, C# 및 Swift와 같은 언어에서 제네릭 타입의 매개변수화를 표현하기 위해 꺾쇠괄호(예: List<int>)를 사용하는 데 익숙합니다. 이러한 괄호의 문제는 특히 Python과 같은 단순한 파서에서 파싱하기가 매우 어렵다는 것입니다. 대부분의 언어에서는 일반 표현식이 허용되지 않는 특정 구문 위치에서만 꺾쇠괄호를 허용하는 방식으로 이러한 모호성을 대체로 처리합니다. (또한 코드의 임의 구간을 되짚어 갈 수 있는 매우 강력한 파싱 기법을 사용합니다.)
그러나 Python에서는 타입 표현식이 (문법적으로) 다른 표현식과 동일하기를 원하므로, 예를 들어 변수 할당을 사용하여 타입 별칭을 만들 수 있습니다. 다음의 간단한 타입 표현식을 살펴보십시오.:
List<int>
Python 파서의 관점에서 이 표현식은 연쇄 비교와 동일한 네 개의 토큰(NAME, LESS, NAME, GREATER)으로 시작합니다.:
a < b > c # I.e., (a < b) and (b > c)
두 방식 모두로 파싱될 수 있는 예를 직접 만들어 볼 수도 있습니다.:
a < b > [ c ]
언어에 꺾쇠 괄호가 있다고 가정하면, 이는 다음 두 가지 중 하나로 해석할 수 있습니다.:
(a<b>)[c] # I.e., (a<b>).__getitem__(c)
a < b > ([c]) # I.e., (a < b) and (b > [c])
이러한 경우를 구별하기 위한 규칙을 마련하는 것은 분명 가능하겠지만, 대부분의 사용자에게 그 규칙은 자의적이고 복잡하게 느껴질 것입니다. 또한 CPython 파서(및 Python을 위한 다른 모든 파서)를 대폭 변경해야 합니다. Python의 현재 파서는 의도적으로 “멍청하다”는 점에 유의해야 합니다. 단순한 문법이 사용자가 추론하기 더 쉽기 때문입니다.
이러한 모든 이유로 대괄호(예: List[int])는 제너릭 타입 매개변수에 선호되는 문법이며, 오랫동안 그래 왔습니다. 메타클래스에 __getitem__() 메서드를 정의하여 구현할 수 있으므로, 새로운 문법은 전혀 필요하지 않습니다. 이 방식은 최근의 모든 Python 버전(Python 2.2부터)에서 작동합니다. 이러한 문법적 선택을 하는 것은 Python만이 아닙니다. Scala의 제너릭 클래스도 대괄호를 사용합니다.
기존 어노테이션 사용 사례는 어떻게 됩니까?
한 가지 주장은 PEP 3107이 함수 어노테이션에서 임의의 표현식을 사용하는 것을 명시적으로 지원한다는 점을 지적합니다. 따라서 새로운 제안은 PEP 3107의 사양과 호환되지 않는 것으로 간주됩니다.
이에 대한 우리의 답변은 우선 현재 제안이 직접적인 비호환성을 도입하지 않는다는 것입니다. 따라서 Python 3.4에서 어노테이션을 사용하는 프로그램은 Python 3.5에서도 여전히 올바르게, 불이익 없이 작동합니다.
결국 타입 힌트가 어노테이션의 유일한 용도가 되기를 바라지만, 이를 위해서는 typing 모듈을 Python 3.5와 함께 처음 출시한 후 추가 논의와 사용 중단 기간이 필요합니다. 현재 PEP는 Python 3.6이 출시될 때까지 잠정 상태(PEP 411 참조)를 유지합니다. 생각할 수 있는 가장 빠른 방식은 3.6에서 타입 힌트가 아닌 어노테이션에 대한 조용한 사용 중단 예고를 도입하고, 3.7에서 완전한 사용 중단을 시행하며, Python 3.8에서는 타입 힌트를 어노테이션에 허용되는 유일한 용도로 선언하는 것입니다. 타입 힌트가 하룻밤 사이에 성공하더라도, 이는 어노테이션을 사용하는 패키지 작성자에게 다른 접근 방식을 고안할 충분한 시간을 제공할 것입니다.
(업데이트: 2017년 가을을 기준으로 이 PEP와 typing.py 모듈의 잠정 상태 종료 일정이 변경되었으며, 어노테이션의 다른 용도에 대한 사용 중단 일정도 변경되었습니다. 업데이트된 일정은 PEP 563을 참조하십시오.)
또 다른 가능한 결과는 타입 힌트가 결국 어노테이션의 기본 의미가 되지만, 이를 비활성화할 수 있는 선택지는 항상 남는 것입니다. 이를 위해 현재 제안은 특정 클래스나 함수에서 어노테이션을 타입 힌트로 해석하는 기본 동작을 비활성화하는 데코레이터 @no_type_check 를 정의합니다. 또한 데코레이터를 데코레이트하는 데 사용할 수 있는 메타데코레이터 @no_type_check_decorator (!)도 정의합니다. 이 메타데코레이터로 데코레이트된 모든 함수나 클래스의 어노테이션은 타입 검사기에 의해 무시됩니다.
# type: ignore 주석도 있으며, 정적 검사기는 선택한 패키지에서 타입 검사를 비활성화할 수 있는 설정 옵션을 지원해야 합니다.
이러한 모든 선택지가 있음에도 불구하고, 개별 인자에 대해 타입 힌트와 다른 형태의 어노테이션이 공존하도록 허용하자는 제안들이 제기되었습니다. 한 제안은 특정 인자에 대한 어노테이션이 딕셔너리 리터럴인 경우 각 키가 서로 다른 어노테이션 형식을 나타내며, 'type' 키가 타입 힌트에 사용되도록 하자는 것입니다. 이 아이디어와 그 변형의 문제는 표기가 매우 “번잡하고” 읽기 어려워진다는 점입니다. 또한 기존 라이브러리가 어노테이션을 사용하는 대부분의 경우에는 어노테이션을 타입 힌트와 결합할 필요가 거의 없습니다. 따라서 타입 힌트를 선택적으로 비활성화하는 더 단순한 접근 방식으로 충분해 보입니다.
전방 선언의 문제
현재 제안은 타입 힌트에 순방향 참조가 포함되어야 하는 경우 솔직히 말해 최적이라고 하기는 어렵습니다. Python에서는 이름이 사용될 때까지 모든 이름이 정의되어 있어야 합니다. 순환 임포트를 제외하면 이는 거의 문제가 되지 않습니다. 여기서 “사용”은 “런타임에 조회”한다는 뜻이며, 대부분의 “순방향” 참조에서는 해당 이름을 사용하는 함수가 호출되기 전에 이름이 정의되어 있도록 보장하는 데 문제가 없습니다.
타입 힌트의 문제는 어노테이션이 (PEP 3107에 따르며 기본값과 마찬가지로) 함수가 정의되는 시점에 평가된다는 점이고, 따라서 어노테이션에서 사용되는 모든 이름은 함수가 정의되는 시점에 이미 정의되어 있어야 한다는 점입니다. 흔한 상황은 메서드의 어노테이션에서 클래스 자체를 참조해야 하는 클래스 정의입니다. (더 일반적으로는 상호 재귀적인 클래스에서도 발생할 수 있습니다.) 이는 컨테이너 타입에서는 자연스러운 일이며, 예를 들어:
class Node:
"""Binary tree node."""
def __init__(self, left: Node, right: Node):
self.left = left
self.right = right
작성된 그대로는 작동하지 않습니다. Python에서는 클래스의 전체 본문이 실행되고 나서야 클래스 이름이 정의된다는 특이성이 있기 때문입니다. 특별히 우아하지는 않지만 목적을 달성하는 우리의 해결책은 어노테이션에서 문자열 리터럴을 사용할 수 있도록 허용하는 것입니다. 하지만 대부분의 경우에는 이를 사용할 필요가 없습니다. 타입 힌트의 대부분의 uses는 내장 타입이나 다른 모듈에 정의된 타입을 참조할 것으로 예상됩니다.
대안 제안에서는 타입 힌트의 의미를 변경하여 런타임에 전혀 평가되지 않도록 할 수 있습니다. (어차피 타입 검사는 오프라인에서 수행되므로, 타입 힌트를 런타임에 전혀 평가해야 할 이유가 무엇이겠습니까.) 물론 이는 하위 호환성에 어긋나게 됩니다. Python 인터프리터는 특정 어노테이션이 타입 힌트를 의미하는지 아니면 다른 것을 의미하는지 실제로 알 수 없기 때문입니다.
타협안으로, __future__ 임포트를 사용하여 주어진 모듈의 all 어노테이션을 문자열 리터럴로 변환할 수 있게 하는 방법이 가능합니다. 다음과 같습니다.:
from __future__ import annotations
class ImSet:
def add(self, a: ImSet) -> List[ImSet]: ...
assert ImSet.add.__annotations__ == {'a': 'ImSet', 'return': 'List[ImSet]'}
이러한 __future__ 임포트 문은 별도의 PEP에서 제안될 수 있습니다.
(업데이트: 해당 __future__ 임포트 문과 그 결과는 PEP 563에서 논의됩니다.)
이중 콜론
몇몇 창의적인 사람들은 이 문제에 대한 해결책을 고안하려고 시도했습니다. 예를 들어 타입 힌트에 이중 콜론(::를 사용하여 두 가지 문제를 한 번에 해결하자는 제안이 있었습니다. 타입 힌트와 다른 어노테이션을 구분하고, 런타임 평가를 배제하도록 의미를 변경하는 것입니다. 그러나 이 아이디어에는 여러 가지 문제가 있습니다.
- 보기 좋지 않습니다. Python에서 단일 콜론은 여러 용도로 사용되며, 그 모든 용도가 영어 텍스트에서 콜론이 사용되는 방식과 비슷하기 때문에 익숙해 보입니다. 이는 대부분의 문장 부호에 대해 Python이 따르는 일반적인 경험칙이며, 예외는 대개 다른 프로그래밍 언어에서 잘 알려진 것들입니다. 그러나 영어에서
::를 이렇게 사용하는 것은 전례가 없으며, 다른 언어(예: C++)에서는 매우 다른 성격의 스코프 연산자로 사용됩니다. 반면 타입 힌트에 사용하는 단일 콜론은 자연스럽게 읽힙니다. 이 목적을 위해 신중하게 설계되었으므로 당연한 일입니다 (the idea는 PEP 3107보다 훨씬 오래전부터 존재했습니다). 또한 파스칼부터 Swift에 이르는 다른 언어에서도 같은 방식으로 사용됩니다. - 반환 타입 어노테이션에는 어떻게 하시겠습니까?
- 타입 힌트가 런타임에 평가된다는 것은 실제로 하나의 기능입니다.
- 타입 힌트를 런타임에 사용할 수 있으면 타입 힌트를 기반으로 런타임 타입 검사기를 구축할 수 있습니다.
- 타입 검사기를 실행하지 않아도 실수를 발견할 수 있습니다. 타입 검사기는 별도의 프로그램이므로 사용자는 이를 실행하지 않거나 심지어 설치하지 않을 수도 있지만, 타입 힌트를 간결한 문서 형식으로 사용하고 싶을 수도 있습니다. 잘못된 타입 힌트는 문서로서도 아무런 쓸모가 없습니다.
- 새로운 구문이므로 타입 힌트에 이중 콜론을 사용하면 Python 3.5에서만 작동하는 코드로 제한됩니다. 기존 구문을 사용하면 현재 제안은 이전 버전의 Python 3에서도 쉽게 작동할 수 있습니다. (실제로 mypy는 Python 3.2 이상을 지원합니다.)
- 타입 힌트가 성공을 거두면, 향후 변수의 타입을 선언하기 위한 새로운 구문(예:
var age: int = 42)을 추가하기로 결정할 수도 있습니다. 인자 타입 힌트에 이중 콜론을 사용한다면, 일관성을 위해 향후 구문에도 동일한 관례를 사용해야 하므로 이 흉한 방식이 계속 이어지게 될 것입니다.
다른 형태의 새 구문
이 외에도 where 키워드의 도입이나 Cobra에서 영감을 받은 requires 절 등 몇 가지 대안적인 구문 형태가 제안되었습니다. 하지만 이들 모두 이중 콜론과 동일한 문제를 안고 있는데, 이전 버전의 Python 3에서는 작동하지 않습니다. 이는 새로운 __future__ 임포트에도 마찬가지로 적용될 것입니다.
하위 호환되는 다른 관례들
제안된 아이디어에는 다음이 포함됩니다:
- 데코레이터(예:
@typehints(name=str, returns=str))가 있습니다. 이 방식은 동작할 수는 있지만, 상당히 장황하며(추가 줄이 필요하고 인자 이름을 반복해야 함), PEP 3107 표기법의 우아함과는 거리가 멉니다. - 스텁 파일. 스텁 파일을 원하기는 하지만, 이는 주로 타입 힌트를 추가하기 적합하지 않은 기존 코드, 예를 들어 서드파티 패키지, Python 2와 Python 3를 모두 지원해야 하는 코드, 그리고 특히 확장 모듈에 타입 힌트를 추가하는 데 유용합니다. 대부분의 상황에서는 어노테이션을 함수 정의와 나란히 두는 것이 훨씬 더 유용합니다.
- 독스트링. 독스트링에는 Sphinx 표기법(
:type arg1: description)에 기반한 기존 관례가 있습니다. 이는 매개변수마다 한 줄씩 추가되어 꽤 장황하고, 그다지 우아하지도 않습니다. 새로운 방식을 고안할 수도 있지만, 어노테이션 구문은 (바로 이 목적을 위해 설계되었기 때문에) 능가하기 어렵습니다.
단순히 다음 릴리스까지 기다리자는 제안도 있었습니다. 하지만 그것이 어떤 문제를 해결하겠습니까? 그것은 그저 미루는 것에 불과할 것입니다.
PEP 개발 프로세스
이 PEP의 실시간 초안은 GitHub에 있습니다. 또한 issue tracker도 있으며, 여기서 많은 기술적 논의가 이루어집니다.
GitHub의 초안은 작은 단위로 정기적으로 업데이트됩니다. official PEPS repo는 (보통) 새 초안이 python-dev에 게시될 때만 업데이트됩니다.
감사의 말
이 문서는 Jim Baker, Jeremy Siek, Michael Matson Vitousek, Andrey Vlasovskikh, Radomir Dopieralski, Peter Ludemann, 그리고 BDFL-Delegate인 Mark Shannon의 귀중한 의견, 격려, 조언 없이는 완성될 수 없었습니다.
영향을 받은 요소로는 PEP 482에 언급된 기존 언어, 라이브러리, 프레임워크가 있습니다. 이들의 창작자들에게 알파벳 순으로 깊은 감사를 드립니다: Stefan Behnel, William Edwards, Greg Ewing, Larry Hastings, Anders Hejlsberg, Alok Menghrajani, Travis E. Oliphant, Joe Pamer, Raoul-Gabriel Urma, Julien Verlaguet.
Copyright
This document has been placed in the public domain.