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 사용하기
- 타입 별칭
- 호출 가능 객체(Callable)
- 제네릭
- 사용자 정의 제네릭 타입
- 타입 변수의 스코프 규칙
- 제네릭 클래스의 인스턴스화와 타입 소거
- 임의의 제네릭 타입을 베이스 클래스로 사용하기
- 추상 제네릭 타입
- 상한이 있는 타입 변수
- 공변성과 반공변성
- 숫자 계층 구조
- 전방 참조
- 유니언 타입
- 유니언에서 싱글턴 타입 지원
Any타입NoReturn타입- 클래스 객체의 타입
- 인스턴스 메서드와 클래스 메서드에 어노테이션 달기
- 버전 및 플랫폼 검사
- 런타임 검사입니까, 타입 검사입니까?
- 임의 인자 목록과 기본 인자 값
- 위치 전용 인자
- 제너레이터 함수와 코루틴에 어노테이션 달기
- 함수 어노테이션의 다른 용도와의 호환성
- 타입 주석
- 캐스트
- NewType 헬퍼 함수
- 스텁 파일
- 예외
typing모듈- Python 2.7 및 양쪽 버전을 아우르는 코드를 위한 제안 문법
- 기각된 대안
- PEP 개발 과정
- 감사의 글
- Copyright
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
PEP 3107은 함수 어노테이션 문법을 도입했지만, 그 의미는 의도적으로 정의하지 않은 채 남겨 두었습니다. 이제 정적 타입 분석을 위한 서드파티 사용 사례가 충분히 쌓였으므로, 표준 라이브러리 안에 표준 어휘와 기본 도구를 갖추면 커뮤니티에 도움이 될 것입니다.
이 PEP는 이러한 표준 정의와 도구를 제공하는 잠정적(provisional) 모듈을 도입하며, 어노테이션을 사용할 수 없는 상황을 위한 몇 가지 관례도 함께 제시합니다.
이 PEP는 여전히 어노테이션의 다른 용도를 명시적으로 막지 않으며, 어노테이션이 이 명세를 따르는 경우에도 어노테이션을 특정 방식으로 처리하도록 요구하거나 금지하지 않는다는 점에 유의하십시오. 이 PEP는 PEP 333이 웹 프레임워크에 대해 그러했듯이, 더 나은 조율을 가능하게 할 뿐입니다.
예를 들어, 다음은 인자 타입과 반환 타입이 어노테이션으로 선언된 간단한 함수입니다:
def greeting(name: str) -> str:
return 'Hello ' + name
이러한 어노테이션은 일반적인 __annotations__ 속성을 통해 런타임에 사용할 수 있지만, 런타임에는 타입 검사가 일어나지 않습니다. 대신 이 제안은 사용자가 자신의 소스 코드에 대해 자발적으로 실행할 수 있는 별도의 오프라인 타입 검사기가 존재한다고 가정합니다. 본질적으로 이러한 타입 검사기는 매우 강력한 린터 역할을 합니다. (물론 개별 사용자가 계약에 의한 설계(Design By Contract)를 강제하거나 JIT 최적화를 하기 위해 런타임에 비슷한 검사기를 사용할 수도 있겠지만, 그러한 도구는 아직 그만큼 성숙하지 않았습니다.)
이 제안은 mypy에서 많은 영감을 받았습니다. 예를 들어, “정수의 시퀀스” 타입은 Sequence[int]로 작성할 수 있습니다. 대괄호를 사용하므로 언어에 새로운 문법을 추가할 필요가 없습니다. 여기 예제는 순수 Python 모듈 typing에서 임포트한 사용자 정의 타입 Sequence를 사용합니다. Sequence[int] 표기법은 메타클래스에 __getitem__()을 구현하여 런타임에 동작합니다(하지만 이 표기법은 주로 오프라인 타입 검사기에 의미가 있습니다).
타입 시스템은 유니언, 제네릭 타입, 그리고 모든 타입과 일관된(즉, 모든 타입에 대입할 수 있고 모든 타입으로부터 대입받을 수 있는) Any라는 특별한 타입을 지원합니다. 이 마지막 기능은 점진적 타이핑(gradual typing)이라는 개념에서 가져온 것입니다. 점진적 타이핑과 전체 타입 시스템은 PEP 483에 설명되어 있습니다.
우리가 차용했거나 우리의 접근 방식과 비교하고 대조할 수 있는 다른 접근 방식은 PEP 482에 설명되어 있습니다.
근거와 목표
PEP 3107은 함수 정의의 각 부분에 임의의 어노테이션을 붙일 수 있도록 지원을 추가했습니다. 당시에는 어노테이션에 어떠한 의미도 부여되지 않았지만, 항상 어노테이션을 타입 힌트에 사용하려는 암묵적인 목표가 있었으며, 이는 해당 PEP에서 첫 번째 사용 사례 후보로 제시되어 있습니다.
이 PEP는 타입 어노테이션을 위한 표준 문법을 제공하여, Python 코드에 대해 더 쉬운 정적 분석과 리팩터링, 잠재적인 런타임 타입 검사, 그리고 (어쩌면 일부 상황에서는) 타입 정보를 활용한 코드 생성의 길을 여는 것을 목표로 합니다.
이러한 목표 중 정적 분석이 가장 중요합니다. 여기에는 mypy와 같은 오프라인 타입 검사기를 지원하는 것뿐만 아니라, IDE가 코드 완성과 리팩터링에 사용할 수 있는 표준 표기법을 제공하는 것도 포함됩니다.
목표가 아닌 것
제안된 typing 모듈에는 런타임 타입 검사를 위한 몇 가지 구성 요소 – 특히 get_type_hints() 함수 – 가 포함되겠지만, 구체적인 런타임 타입 검사 기능을 구현하려면 예를 들어 데코레이터나 메타클래스를 사용하는 서드파티 패키지를 개발해야 합니다. 타입 힌트를 성능 최적화에 사용하는 것은 독자의 연습 과제로 남겨 둡니다.
또한 다음 점을 강조해야 합니다. 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)
특정 시그니처의 콜백 함수를 기대하는 프레임워크에는 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 베이스 클래스는 LoggedVar[t]가 타입으로서 유효하도록 __getitem__을 정의하는 메타클래스를 사용합니다:
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의 경우처럼)과 함께 사용할 수 없습니다. 타입 제약은 추론된 타입이 제약 타입 중 _정확히_ 하나가 되도록 하는 반면, 상한은 실제 타입이 경계 타입의 서브타입일 것만 요구합니다.
공변성과 반공변성
서브클래스 Manager를 가진 Employee 클래스를 생각해 보십시오. 이제 List[Employee]로 어노테이션된 인자를 받는 함수가 있다고 가정해 봅시다. 이 함수를 List[Manager] 타입의 변수를 인자로 전달하여 호출하는 것이 허용되어야 합니까? 많은 사람이 그 결과를 따져 보지도 않고 “네, 물론입니다”라고 대답할 것입니다. 하지만 함수에 대해 더 알지 못하는 한, 타입 검사기는 이러한 호출을 거부해야 합니다. 함수가 리스트에 Employee 인스턴스를 추가할 수 있고, 그러면 호출자 쪽 변수의 타입을 위반하게 되기 때문입니다.
알고 보면 이러한 인자는 반공변적으로 동작하는 반면, 직관적인 대답(함수가 인자를 변경하지 않는 경우에는 올바른 대답입니다!)은 인자가 공변적으로 동작할 것을 요구합니다. 이러한 개념에 대한 더 자세한 소개는 Wikipedia와 PEP 483에서 찾을 수 있습니다. 여기서는 타입 검사기의 동작을 제어하는 방법만 보여 드립니다.
기본적으로 제네릭 타입은 모든 타입 변수에 대해 불변(invariant)으로 간주됩니다. 이 불변 조건은 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)는 불변 조건(invariant)을 따르도록 선언됩니다. 반공변 타입의 유일한 예는 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는 이에 대응하는 ABC(Number, Complex, Real, Rational, Integral)를 구현합니다. 이 ABC에는 몇 가지 문제가 있지만, 내장 구체 숫자 클래스인 complex, float, int는 어디에서나 쓰입니다(특히 뒤의 두 개가 그렇습니다 :-).
사용자에게 import numbers를 작성한 뒤 numbers.Float 등을 사용하도록 요구하는 대신, 이 PEP는 거의 같은 효과를 내는 간단한 지름길을 제안합니다. 인자가 float 타입을 가진다고 어노테이션되어 있으면 int 타입의 인자도 허용되며, 마찬가지로 complex 타입을 가진다고 어노테이션된 인자에는 float나 int 타입의 인자도 허용됩니다. 이 방식은 대응하는 ABC를 구현한 클래스나 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/b.py의 from models.a import A 줄에서 ImportError를 일으키며 실패합니다. 이때 models/b.py는 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
이러한 상황에서 정확한 타입 지정을 가능하게 하려면, 사용자는 타입 오류를 정적으로 잡아낼 수 있도록 Union 타입을 표준 라이브러리가 제공하는 enum.Enum 클래스와 함께 사용해야 합니다:
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은 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(Python 메타클래스 계층 구조의 루트)과 동등합니다. 이 동등성은 이 기능을 논의하는 동안 제안되었던 Class나 SubType 같은 대안 대신 Type이라는 이름을 택한 이유이기도 합니다. 이는 예컨대 List와 list의 관계와 비슷합니다.
Type[Any](또는 Type이나 type)의 동작과 관련하여, 이 타입을 가진 변수의 속성에 접근하면 type이 정의한 속성과 메서드(예: __repr__()와 __mro__)만 제공됩니다. 이러한 변수는 임의의 인자로 호출할 수 있으며, 반환 타입은 Any입니다.
Type[Derived]는 Type[Base]의 서브타입이므로 Type은 매개변수에 대해 공변적입니다.:
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 중 어떤 선택지도 타입 제약을 만족하지 못합니다.
이런 경우 기본값을 리터럴 생략 부호(ellipsis)로 지정할 수 있습니다. 즉 위의 예시는 실제로 작성하는 코드 그대로입니다.
위치 전용 인자
일부 함수는 인자를 위치로만 받도록 설계되어 있으며, 호출자가 인자 이름을 사용해 해당 인자를 키워드로 전달하는 일이 결코 없을 것을 전제로 합니다. 이름이 __로 시작하는 모든 인자는 위치 전용으로 간주됩니다. 단, 이름이 __로 끝나기도 하는 경우는 예외입니다:
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 버전에서 변수에 타입을 지정하는 문법이 제공될 수 있습니다. (업데이트: 이 문법은 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라고 우리가 확신한다는 것을 타입 검사기에 알려 줍니다. 런타임에 cast는 항상 표현식을 변경하지 않고 그대로 반환합니다 – 타입을 검사하지도 않고, 값을 변환하거나 강제 변환하지도 않습니다.
캐스트는 타입 주석과 다릅니다(이전 절을 참고하십시오). 타입 주석을 사용할 때에도 타입 검사기는 추론된 타입이 명시된 타입과 일관되는지 검증해야 합니다. 캐스트를 사용할 때에는 타입 검사기가 프로그래머를 맹목적으로 믿어야 합니다. 또한 캐스트는 표현식에서 사용할 수 있지만, 타입 주석은 대입에만 적용됩니다.
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 버전에 따라 달라질 수 있는 특정 모듈의 모든 객체를 다시 내보내기가 더 쉬워집니다.) - 일반 Python 파일과 마찬가지로, 하위 모듈은 임포트되면 자동으로 부모 모듈의 내보낸 속성이 됩니다. 예를 들어,
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 데코레이터가 적용된 일련의 정의 뒤에 (같은 함수/메서드에 대해) @overload 데코레이터가 적용되지 않은 정의가 정확히 하나 와야 합니다. @overload 데코레이터가 적용된 정의는 오직 타입 검사기를 위한 것입니다. 이 정의들은 @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(호출 가능 객체):
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
- Callable(호출 가능 객체) (위를 참조하십시오. 목록을 빠짐없이 제시하기 위해 여기에도 나열합니다)
- Collection
- 컨테이너(Container)
- ContextManager
- Coroutine (코루틴)
- Generator (제너레이터)는
Generator[yield_type, send_type, return_type]형태로 사용합니다. 이는 제너레이터 함수의 반환값을 나타냅니다.Iterable의 서브타입이며,send()메서드가 받는 타입과 제너레이터의 반환 타입을 위한 타입 변수를 추가로 가집니다(send 타입 변수에 대해서는 반공변입니다. 즉,Employee인스턴스를 보낼 수 있는 제너레이터는Manager인스턴스를 보낼 수 있는 제너레이터가 요구되는 문맥에서 유효합니다). - Hashable(제네릭은 아니지만 완전성을 위해 포함되어 있습니다)
- ItemsView
- 이터러블(Iterable)
- Iterator (이터레이터)
- KeysView
- Mapping (매핑)
- MappingView
- MutableMapping
- MutableSequence
- MutableSet
- Sequence
- Set.
AbstractSet으로 이름이 바뀌었습니다.typing모듈에서Set은 제네릭을 적용한set()을 의미하기 때문에 이러한 이름 변경이 필요했습니다. - Sized(제네릭은 아니지만 완전성을 위해 포함되어 있습니다)
- ValuesView
단일 특수 메서드를 검사하는 몇 가지 일회성 타입이 정의되어 있습니다(Hashable이나 Sized와 비슷합니다):
- Reversible.
__reversed__를 검사합니다. - SupportsAbs.
__abs__를 검사합니다. - SupportsComplex.
__complex__를 검사합니다. - SupportsFloat.
__float__을 검사합니다. - SupportsInt.
__int__를 검사합니다. - SupportsRound.
__round__를 검사합니다. - SupportsBytes.
__bytes__를 검사합니다.
편의를 위한 정의는 다음과 같습니다:
- Optional.
Optional[t] == Union[t, None]으로 정의됩니다. - Text. Python 3에서는
str의, Python 2에서는unicode의 단순한 별칭입니다. - AnyStr.
TypeVar('AnyStr', Text, bytes)로 정의됩니다. - NamedTuple,
NamedTuple(type_name, [(field_name, field_type), ...])형태로 사용하며collections.namedtuple(type_name, [field_name, ...])과 동등합니다. 이는 이름 있는 튜플 타입의 필드 타입을 선언하는 데 유용합니다. - NewType, 런타임 오버헤드가 거의 없는 고유한 타입을 만드는 데 사용합니다.
UserId = NewType('UserId', int) - cast(), 앞에서 설명했습니다.
- @no_type_check, 클래스 또는 함수 단위로 타입 검사를 비활성화하는 데코레이터입니다(아래 참조).
- @no_type_check_decorator,
@no_type_check와 같은 의미를 갖는 자신만의 데코레이터를 만드는 데코레이터입니다(아래 참조). - @type_check_only, 스텁 파일에서 사용하기 위해 타입 검사 중에만 사용할 수 있는 데코레이터입니다(위 참조). 클래스나 함수를 런타임에는 사용할 수 없는 것으로 표시합니다.
- @overload, 앞에서 설명했습니다.
- get_type_hints(), 함수나 메서드에서 타입 힌트를 가져오는 유틸리티 함수입니다. 함수 또는 메서드 객체가 주어지면
__annotations__와 같은 형식의 dict를 반환하되, (문자열 리터럴로 주어진) 전방 참조를 원래 함수 또는 메서드 정의의 컨텍스트에서 표현식으로 평가합니다. - TYPE_CHECKING, 런타임에는
False이지만 타입 검사기에는True입니다.
I/O 관련 타입은 다음과 같습니다.
- IO(
AnyStr에 대해 제네릭) - BinaryIO(
IO[bytes]의 단순한 서브타입) - TextIO(
IO[str]의 단순한 서브타입)
정규 표현식 및 re 모듈과 관련된 타입은 다음과 같습니다.
- Match와 Pattern,
re.match()와re.compile()결과의 타입(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>
때로는 인자 타입을 (아직) 지정하지 않고 함수나 메서드의 반환 타입만 지정하고 싶을 때가 있습니다. 이를 명시적으로 지원하기 위해 인자 목록을 생략 부호(ellipsis)로 대체할 수 있습니다. 예시:
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에서도 아무런 불이익 없이 올바르게 동작한다는 것입니다.
저희는 타입 힌트가 결국 어노테이션의 유일한 용도가 되기를 바라지만, 그러려면 Python 3.5와 함께 typing 모듈이 처음 배포된 뒤에 추가 논의와 폐지 예고 기간이 필요합니다. 현재 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' 키는 타입 힌트에 사용하자고 합니다. 이 아이디어와 그 변형들의 문제는 표기법이 매우 “지저분해지고” 읽기 어려워진다는 것입니다. 또한 기존 라이브러리가 어노테이션을 사용하는 대부분의 경우에는 이를 타입 힌트와 결합할 필요가 거의 없을 것입니다. 따라서 타입 힌트를 선택적으로 비활성화하는 더 단순한 접근 방식으로 충분해 보입니다.
전방 선언의 문제
솔직히 말해, 타입 힌트에 전방 참조(forward reference)가 들어가야 하는 경우에는 현재 제안이 최선이 아닙니다. Python에서는 모든 이름이 사용되는 시점까지 정의되어 있어야 합니다. 순환 임포트를 제외하면 이것이 문제가 되는 경우는 드뭅니다. 여기서 “사용”은 “런타임에 조회”한다는 뜻이며, 대부분의 “전방” 참조에서는 이름을 사용하는 함수가 호출되기 전에 그 이름이 정의되도록 하는 데 아무런 문제가 없습니다.
타입 힌트의 문제는 어노테이션이 (PEP 3107에 따라, 그리고 기본값과 마찬가지로) 함수가 정의되는 시점에 평가된다는 것입니다. 따라서 어노테이션에 사용되는 모든 이름은 함수가 정의될 때 이미 정의되어 있어야 합니다. 흔한 경우는 메서드의 어노테이션에서 클래스 자신을 참조해야 하는 클래스 정의입니다. (더 일반적으로는 상호 재귀적인 클래스에서도 발생할 수 있습니다.) 이는 컨테이너 타입에서 자연스러운 일입니다. 예를 들면 다음과 같습니다.:
class Node:
"""Binary tree node."""
def __init__(self, left: Node, right: Node):
self.left = left
self.right = right
작성된 그대로는 동작하지 않는데, 클래스 본문 전체가 실행된 후에야 클래스 이름이 정의된다는 Python의 특이한 점 때문입니다. 저희의 해결책은 특별히 우아하지는 않지만 제 역할은 하는 것으로, 어노테이션에 문자열 리터럴을 사용할 수 있도록 허용하는 것입니다. 하지만 대부분의 경우에는 이것을 사용할 필요가 없습니다. 타입 힌트의 사용은 대부분 내장 타입이나 다른 모듈에 정의된 타입을 참조할 것으로 예상됩니다.
이에 대한 반대 제안은 타입 힌트가 런타임에 전혀 평가되지 않도록 타입 힌트의 의미를 바꾸는 것입니다(어차피 타입 검사는 오프라인에서 이루어지는데, 타입 힌트를 굳이 런타임에 평가할 필요가 있겠습니까). 물론 이는 하위 호환성에 어긋나는데, Python 인터프리터는 특정 어노테이션이 타입 힌트를 의도한 것인지 아니면 다른 무언가를 의도한 것인지 실제로 알지 못하기 때문입니다.
__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에서 제안될 수 있습니다.
(업데이트: 그 __future__ 임포트 문과 그 영향은 PEP 563에서 논의합니다.)
이중 콜론
몇몇 창의적인 사람들이 이 문제에 대한 해결책을 고안하려고 시도했습니다. 예를 들어, 타입 힌트에 이중 콜론(::)을 사용하여 두 가지 문제를 한 번에 해결하자는 제안이 있었습니다. 즉, 타입 힌트와 다른 어노테이션을 구별하고, 의미를 바꾸어 런타임 평가를 막자는 것이었습니다. 하지만 이 아이디어에는 몇 가지 문제가 있습니다.
- 보기 흉합니다. Python에서 단일 콜론은 여러 용도로 쓰이며, 모두 영어 문장에서 콜론을 쓰는 방식과 비슷하기 때문에 친숙하게 보입니다. 이는 Python이 대부분의 구두점 형태에 대해 따르는 일반적인 경험칙이며, 예외는 대개 다른 프로그래밍 언어에서 잘 알려진 것들입니다. 그러나 이러한
::의 용법은 영어에서는 들어본 적이 없는 것이며, 다른 언어(예: C++)에서는 스코프 연산자로 사용되는데, 이는 전혀 다른 것입니다. 반면 타입 힌트를 위한 단일 콜론은 자연스럽게 읽힙니다. 이 목적을 위해 신중하게 설계되었으니 당연한 일입니다(그 아이디어는 PEP 3107보다 훨씬 먼저 나왔습니다). 또한 Pascal부터 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에 있습니다. 또한 이슈 트래커도 있으며, 기술적인 논의의 상당 부분이 그곳에서 이루어집니다.
GitHub의 초안은 작은 단위로 정기적으로 갱신됩니다. 공식 PEPS 저장소는 (보통) 새 초안이 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.