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

Python 개선 제안 한국어 번역

PEP 483 – 타입 힌트의 이론

Author:
Guido van Rossum <guido at python.org>, Ivan Levkivskyi <levkivskyi at gmail.com>
Discussions-To:
Python-Ideas list
Status:
Final
Type:
Informational
Topic:
Typing
Created:
19-Dec-2014
Post-History:


Table of Contents

번역·라이선스 안내

이 문서는 Open Publication License v1.0 이상에 따라 만든 수정된 한국어 번역본입니다. 수정자: yeokja/yeokja 프로젝트. 수정일: 2026-08-29. 변경 내용: 영어 원문을 한국어로 번역했습니다. 원저자와 저작권 표시는 위 Author 필드와 아래 Copyright 절에 유지했으며, 이 번역은 원저자의 승인이나 보증을 뜻하지 않습니다. 수정되지 않은 기준 원문 · 공식 최신판 · Open Publication License v1.0

초록

이 PEP는 PEP 484에서 참조하는 이론을 설명합니다.

소개

이 문서는 Python 3.5를 위한 새로운 타입 힌팅 제안의 이론을 설명합니다. 해결해야 할 세부 사항이 많기 때문에 완전한 제안서나 명세서라고 하기는 어렵지만, 더 자세한 명세를 논의하기 어렵게 만드는 이론을 설명합니다. 먼저 타입 이론의 기본 개념을 되짚어 봅니다. 그런 다음 점진적 타이핑을 설명하고, 몇 가지 일반 규칙을 제시한 후 어노테이션에서 사용할 수 있는 새로운 특수 타입(예: Union)을 정의하며, 마지막으로 제네릭 타입에 대한 접근 방식과 타입 힌팅의 실용적인 측면을 정의합니다.

표기 규칙

  • t1, t2 등과 u1, u2 등은 타입입니다. 때때로 ti 또는 tjt1, t2 등의 임의의 항목을 가리키는 데 사용합니다.
  • T, U 등은 타입 변수입니다(TypeVar()로 정의하며, 아래를 참조하십시오).
  • 객체, class 문으로 정의된 클래스 및 인스턴스는 표준 PEP 8 규칙을 사용하여 나타냅니다.
  • 이 PEP의 문맥에서 타입에 적용된 == 기호는 두 표현식이 동일한 타입을 나타낸다는 의미입니다.
  • 참고로 PEP 484는 타입과 클래스가 서로 다르다는 점을 구분합니다(타입은 타입 검사기를 위한 개념이고, 클래스는 런타임 개념입니다). 이 PEP에서는 타입 검사기의 구현에 더 많은 유연성을 허용하기 위해 이 구분을 명확히 하되 불필요하게 엄격하게 적용하지 않습니다.

배경

문헌에는 타입이라는 개념에 대한 정의가 많습니다. 여기서는 타입을 값의 집합과 해당 값에 적용할 수 있는 함수의 집합으로 가정합니다.

특정 타입을 정의하는 방법에는 여러 가지가 있습니다.

  • 모든 값을 명시적으로 나열하는 방법입니다. 예를 들어 TrueFalsebool 타입을 이룹니다.
  • 어떤 타입의 변수와 함께 사용할 수 있는 함수를 지정하는 방법입니다. 예를 들어 __len__ 메서드를 가진 모든 객체는 Sized 타입을 이룹니다. [1, 2, 3]'abc'는 모두 이 타입에 속합니다. 이들에 len을 호출할 수 있기 때문입니다.:
    len([1, 2, 3])  # OK
    len('abc')      # also OK
    len(42)         # not a member of Sized
    
  • 간단한 클래스 정의를 사용하는 방법입니다. 예를 들어 클래스를 정의하면:
    class UserID(int):
        pass
    

    그러면 이 클래스의 모든 인스턴스도 하나의 타입을 이룹니다.

  • 더 복잡한 타입도 있습니다. 예를 들어 int, str 또는 그 서브클래스의 인스턴스만 포함하는 모든 리스트로 FancyList 타입을 정의할 수 있습니다. [1, 'abc', UserID(42)] 값은 이 타입을 가집니다.

사용자가 타입 검사기가 이해할 수 있는 형식으로 타입을 정의할 수 있는 것이 중요합니다. 이 PEP의 목표는 PEP 3107 구문을 사용하여 변수와 함수의 타입 어노테이션에 사용할 타입을 정의하는 체계적인 방법을 제안하는 것입니다. 이러한 어노테이션은 여러 종류의 버그를 방지하거나, 문서화 목적으로 사용하거나, 어쩌면 프로그램 실행 속도를 높이는 데에도 사용할 수 있습니다. 여기서는 정적 타입 검사기를 사용하여 버그를 방지하는 데에만 초점을 맞춥니다.

서브타입 관계

정적 타입 검사기에서 중요한 개념은 서브타입 관계입니다. 이는 다음과 같은 질문에서 비롯됩니다. first_varfirst_type타입이고 second_varsecond_type타입이라면, first_var = second_var로 대입해도 안전합니까?

언제 안전하다고 판단해야 하는지에 대한 강력한 기준은 다음과 같습니다:

  • second_type의 모든 값이 first_type의 값 집합에도 포함됩니다.
  • first_type의 모든 함수가 second_type의 함수 집합에도 포함됩니다.

이와 같이 정의된 관계를 서브타입 관계라고 합니다.

이 정의에 따르면:

  • 모든 타입은 자기 자신의 서브타입입니다.
  • 서브타이핑 과정에서 값의 집합은 작아지는 반면, 함수의 집합은 커집니다.

직관적인 예를 들어 보겠습니다. 모든 DogAnimal이며, Dog는 더 많은 함수를 가지고 있고 예를 들어 짖을 수 있습니다. 따라서 DogAnimal의 서브타입입니다. 반대로 AnimalDog의 서브타입이 아닙니다.

보다 형식적인 예를 들면, 정수는 실수의 서브타입입니다. 실제로 모든 정수는 당연히 실수이기도 하며, 정수는 비트 시프트 <<>>와 같은 더 많은 연산을 지원합니다.:

lucky_number = 3.14    # type: float
lucky_number = 42      # Safe
lucky_number * 2       # This works
lucky_number << 5      # Fails

unlucky_number = 13    # type: int
unlucky_number << 5    # This works
unlucky_number = 2.72  # Unsafe

까다로운 예를 하나 더 살펴보겠습니다. List[int]가 정수만 포함하는 모든 리스트로 형성된 타입을 나타낸다면, 이는 실수만 포함하는 모든 리스트로 형성된 List[float]의 하위 타입이 아닙니다. 서브타이핑의 첫 번째 조건은 성립하지만, 실수를 추가하는 작업은 List[float]에서만 가능하므로 두 번째 조건은 성립하지 않습니다.:

def append_pi(lst: List[float]) -> None:
    lst += [3.14]

my_list = [1, 3, 5]  # type: List[int]

append_pi(my_list)   # Naively, this should be safe...

my_list[-1] << 5     # ... but this fails

타입 검사기에 서브타입 정보를 선언하기 위한 널리 사용되는 접근법은 두 가지입니다.

명목적 서브타이핑에서는 타입 트리가 클래스 트리를 기반으로 하며, 즉 UserIDint의 서브타입으로 간주됩니다. Python에서는 호환되지 않는 방식으로 속성을 재정의할 수 있으므로 이 접근법은 타입 검사기의 통제하에 사용해야 합니다.:

class Base:
    answer = '42' # type: str

class Derived(Base):
    answer = 5 # should be marked as error by type checker

구조적 서브타이핑에서는 선언된 메서드에서 서브타입 관계를 추론하며, 즉 UserIDint는 동일한 타입으로 간주됩니다. 이로 인해 간혹 혼란이 발생할 수 있지만, 구조적 서브타이핑은 더 유연한 것으로 간주됩니다. 구조적 정보를 명목적 서브타이핑과 함께 사용할 수 있도록 두 접근법 모두를 지원하고자 합니다.

점진적 타이핑 요약

점진적 타이핑을 사용하면 프로그램의 일부에만 타입 어노테이션을 적용하여 동적 타이핑과 정적 타이핑의 바람직한 측면을 모두 활용할 수 있습니다.

새로운 관계인 is-consistent-with를 정의합니다. 이는 is-subtype-of와 유사하지만, 새로운 타입 Any가 관여할 때 추이적이지 않다는 점이 다릅니다. (두 관계 모두 대칭적이지 않습니다.) a_valuea_variable에 대입하는 것은 a_value의 타입이 a_variable의 타입과 일관되면 허용됩니다. (이는 “a_value의 타입이 a_variable의 타입의 서브타입인 경우”와 비교할 수 있으며, 이는 객체 지향 프로그래밍의 기본 원칙 중 하나를 나타냅니다.) is-consistent-with 관계는 다음 세 가지 규칙으로 정의됩니다.

  • t1t2의 서브타입이면, 타입 t1은 타입 t2와 일관됩니다. (반대 방향은 성립하지 않습니다.)
  • Any는 모든 타입과 일관됩니다. (하지만 Any는 모든 타입의 서브타입이 아닙니다.)
  • 모든 타입은 Any와 일관됩니다. (하지만 모든 타입이 Any의 서브타입인 것은 아닙니다.)

이상입니다! 더 자세한 설명과 동기는 Jeremy Siek의 블로그 게시물 What is Gradual Typing을 참조하십시오. Any는 모든 값과 모든 메서드를 가지는 타입으로 간주할 수 있습니다. 위에서 설명한 서브타이핑의 정의와 결합하면, 이는 Any를 타입 계층 구조의 상단(모든 값을 가짐)과 하단(모든 메서드를 가짐)에 부분적으로 배치합니다. object와 대조해 보면, 이는 대부분의 타입과 일관되지 않습니다(예를 들어, int가 예상되는 곳에서는 object() 인스턴스를 사용할 수 없습니다). 즉, 인자를 어노테이션할 때 Anyobject는 모두 “어떤 타입이든 허용됩니다”라는 의미이지만, 예상되는 타입이 무엇이든 전달할 수 있는 것은 Any뿐입니다(본질적으로 Any는 동적 타이핑으로의 대체를 선언하고 정적 타입 검사기의 불만을 억제합니다).

다음은 이러한 규칙이 실제로 어떻게 적용되는지를 보여 주는 예입니다.

Employee클래스와 그 서브클래스인 Manager가 있다고 합시다.:

class Employee: ...
class Manager(Employee): ...

변수 workerEmployee타입으로 선언되었다고 합시다.:

worker = Employee()  # type: Employee

이제 Manager인스턴스를 worker에 할당해도 됩니다(규칙 1).:

worker = Manager()

Manager타입으로 선언된 변수에 Employee인스턴스를 할당하는 것은 허용되지 않습니다.:

boss = Manager()  # type: Manager
boss = Employee()  # Fails static check

그러나 타입이 Any인 변수가 있다고 가정해 보십시오.:

something = some_func()  # type: Any

이제 somethingworker에 할당해도 됩니다(규칙 2).:

worker = something  # OK

물론 workersomething에 할당하는 것도 허용됩니다(규칙 3). 하지만 이를 위해 일관성이라는 개념이 필요하지는 않습니다.:

something = worker  # OK

타입과 클래스

Python에서 클래스는 class문으로 정의되고 type(obj)내장 함수가 반환하는 객체 팩토리입니다. 클래스는 동적 런타임 개념입니다.

타입 개념은 위에서 설명했으며, 타입은 변수 및 함수 타입 어노테이션에 나타나고, 아래에서 설명하는 구성 요소로부터 구성할 수 있으며, 정적 타입 검사기가 사용합니다.

위에서 설명한 것처럼 모든 클래스는 타입입니다. 그러나 주어진 타입의 의미를 정확히 나타내는 클래스를 구현하는 것은 까다롭고 오류가 발생하기 쉬우며, 이는 PEP 484의 목표도 아닙니다. 설명된 정적 타입은 PEP 484 런타임 클래스와 혼동해서는 안 됩니다. 예를 들면 다음과 같습니다:

  • int는 클래스이자 타입입니다.
  • UserID는 클래스이자 타입입니다.
  • Union[str, int]는 타입이지만 적절한 클래스는 아닙니다.:
    class MyUnion(Union[str, int]): ...  # raises TypeError
    
    Union[str, int]()  # raises TypeError
    

타이핑 인터페이스는 클래스로 구현되므로, 즉 런타임에 예를 들어 Generic[T].__bases__를 평가할 수 있습니다. 그러나 클래스와 타입의 차이를 강조하기 위해 다음과 같은 일반 규칙이 적용됩니다.

  • 아래에 정의된 타입(즉, Any, Union등)은 인스턴스화할 수 없으며, 그렇게 시도하면 TypeError가 발생합니다. (단, Generic의 비추상 서브클래스는 인스턴스화할 수 있습니다.)
  • 아래에 정의된 타입은 Generic과 그로부터 파생된 클래스를 제외하고 서브클래싱할 수 없습니다.
  • 이들 타입이 isinstance또는 issubclass에 나타나면(매개변수가 지정되지 않은 제너릭은 제외) 모두 TypeError가 발생합니다.

기본 구성 요소

  • Any. 모든 타입은 Any와 일관되며, 위에서 설명한 대로 모든 타입과도 일관됩니다.
  • Union[t1, t2, …]. t1등 중 하나 이상을 서브타입으로 가지는 타입은 이것의 서브타입입니다.
    • 구성 요소가 모두 t1등의 서브타입인 유니언은 이것의 서브타입입니다. 예: Union[int, str]Union[int, float, str]의 서브타입입니다.
    • 인자의 순서는 중요하지 않습니다. 예: Union[int, str] == Union[str, int]입니다.
    • ti자체가 Union이면 결과가 평탄화됩니다. 예: Union[int, Union[float, str]] == Union[int, float, str]입니다.
    • titj가 서브타입 관계를 가지면, 덜 구체적인 타입이 남습니다. 예: Union[Employee, Manager] == Union[Employee]입니다.
    • Union[t1]은 단순히 t1을 반환합니다. Union[]은 불법이며, Union[()]도 마찬가지입니다.
    • 귀결: Union[..., object, ...]object를 반환합니다.
  • Optional[t1]. Union[t1, None]의 별칭입니다. 즉, Union[t1, type(None)]입니다.
  • Tuple[t1, t2, …, tn]. 항목이 t1등의 인스턴스인 튜플입니다. 예를 들어 Tuple[int, float]은 두 항목으로 이루어진 튜플을 의미하며, 첫 번째 항목은 int이고 두 번째 항목은 float입니다. 예: (42, 3.14)입니다.
    • Tuple[u1, u2, ..., um]은 길이가 같고 n==m이며 각 uiti의 서브타입인 경우 Tuple[t1, t2, ..., tn]의 서브타입입니다.
    • 빈 튜플의 타입을 표기하려면 Tuple[()]을 사용하십시오.
    • 가변 길이 동종 튜플 타입은 Tuple[t1, ...]로 작성할 수 있습니다. (점 세 개, 즉 리터럴 줄임표입니다. 그리고 이것은 Python 구문에서 유효한 토큰입니다.)
  • Callable[[t1, t2, …, tn], tr]는 호출 가능 객체입니다. t1등의 위치 인자 타입과 반환 타입 tr을 가지는 함수입니다. 인자 목록은 비어 있을 수 있습니다(n==0). 선택적 인자나 키워드 인자 또는 가변 인자를 나타내는 방법은 없지만, Callable[..., tr]로 작성하여 인자 목록 전체를 검사하지 않도록 지정할 수 있습니다(다시 말해 리터럴 줄임표입니다).

다음을 추가할 수도 있습니다:

  • Intersection[t1, t2, …]. t1등의 타입 각각의 서브타입인 타입은 이것의 서브타입입니다. (정의에서 각각이 아니라 적어도 하나를 사용하는 Union과 비교하십시오.)
    • 인자의 순서는 중요하지 않습니다. 중첩된 교집합은 평탄화됩니다. 예: Intersection[int, Intersection[float, str]] == Intersection[int, float, str].
    • 더 적은 타입의 교집합은 더 많은 타입의 교집합의 상위 타입입니다. 예: Intersection[int, str]Intersection[int, float, str]의 상위 타입입니다.
    • 인자가 하나인 교집합은 해당 인자 자체입니다. 예: Intersection[int]int입니다.
    • 인자들이 서브타입 관계를 가지면 더 구체적인 타입이 남습니다. 예: Intersection[str, Employee, Manager]Intersection[str, Manager]입니다.
    • Intersection[]은 불법이며, Intersection[()]도 마찬가지입니다.
    • 귀결: Any는 인자 목록에서 사라집니다. 예: Intersection[int, str, Any] == Intersection[int, str]입니다. Intersection[Any, object]object입니다.
    • IntersectionUnion의 상호작용은 복잡하지만, 일반 집합의 교집합과 합집합 사이의 상호작용을 이해한다면 놀랄 일이 아닙니다(타입 집합에는 새 서브클래스의 수에 제한이 없으므로 그 크기가 무한할 수 있음에 유의하십시오).

제네릭 타입

앞에서 정의한 기본 구성 요소를 사용하면 제네릭 방식으로 새로운 타입을 구성할 수 있습니다. 예를 들어, Tuple은 구체적인 타입 float을 받아 구체적인 타입 Vector = Tuple[float, ...]를 만들거나, 다른 타입 UserID를 받아 또 다른 구체적인 타입 Registry = Tuple[UserID, ...]를 만들 수 있습니다. 이러한 의미론을 제네릭 타입 생성자라고 하며, 함수의 의미론과 유사하지만 함수는 값을 받아 값을 반환하는 반면 제네릭 타입 생성자는 타입을 받아 타입을 “반환”합니다.

특정 클래스나 함수가 이처럼 제네릭한 타입 방식으로 동작하는 경우는 흔합니다. 두 가지 예를 살펴보십시오.

  • listdict와 같은 컨테이너 클래스는 일반적으로 특정 타입의 값만 포함합니다. 따라서 사용자는 이에 맞게 타입 어노테이션을 지정하고 싶을 수 있습니다.:
    users = [] # type: List[UserID]
    users.append(UserID(42)) # OK
    users.append('Some guy') # Should be rejected by the type checker
    
    examples = {} # type: Dict[str, Any]
    examples['first example'] = object() # OK
    examples[2] = None                   # rejected by the type checker
    
  • 다음 함수는 int 타입의 인자 두 개를 받아 int를 반환하거나, float 타입의 인자 두 개를 받아 float를 반환하는 등의 작업을 수행할 수 있습니다.:
    def add(x, y):
        return x + y
    
    add(1, 2) == 3
    add('1', '2') == '12'
    add(2.7, 3.5) == 6.2
    

첫 번째 예의 상황에서 타입 어노테이션을 사용할 수 있도록, 내장 컨테이너와 컨테이너 추상 베이스 클래스는 타입 매개변수로 확장되어 제네릭 타입 생성자처럼 동작합니다. 제네릭 타입 생성자처럼 동작하는 클래스를 제네릭 타입이라고 합니다. 예:

from typing import Iterable

class Task:
    ...

def work(todo_list: Iterable[Task]) -> None:
    ...

여기서 Iterable은 구체적인 타입 Task를 받아 구체적인 타입 Iterable[Task]를 반환하는 제네릭 타입입니다.

두 번째 예와 같이 타입 제네릭 방식으로 동작하는 함수를 제네릭 함수라고 합니다. 제네릭 함수의 타입 어노테이션은 타입 변수를 통해 지정할 수 있습니다. 제네릭 타입과 관련된 타입 변수의 의미론은 함수의 매개변수 의미론과 어느 정도 유사합니다. 그러나 타입 변수에 구체적인 타입을 할당하지는 않습니다. 가능한 값을 찾고 찾지 못할 경우 사용자에게 경고하는 것은 정적 타입 검사기의 역할입니다. 예:

def take_first(seq: Sequence[T]) -> T: # a generic function
    return seq[0]

accumulator = 0 # type: int

accumulator += take_first([1, 2, 3])   # Safe, T deduced to be int
accumulator += take_first((2.7, 3.5))  # Unsafe

타입 변수는 타입 어노테이션에서 광범위하게 사용되며, 타입 검사기의 타입 추론 내부 메커니즘도 일반적으로 타입 변수를 기반으로 구축됩니다. 따라서 이를 자세히 살펴보겠습니다.

타입 변수

X = TypeVar('X')는 고유한 타입 변수를 선언합니다. 이름은 변수 이름과 일치해야 합니다. 기본적으로 타입 변수의 범위는 가능한 모든 타입입니다. 예:

def do_nothing(one_arg: T, other_arg: T) -> None:
    pass

do_nothing(1, 2)               # OK, T is int
do_nothing('abc', UserID(42))  # also OK, T is object

Y = TypeVar('Y', t1, t2, ...)입니다. 마찬가지로 t1 등에 제약됩니다. Union[t1, t2, ...]와 유사하게 동작합니다. 제약된 타입 변수는 t1 등의 제약 조건만을 정확히 대상으로 합니다. 제약 조건의 서브클래스는 t1 등의 제약 조건 중 가장 파생된 베이스 클래스로 대체됩니다. 예:

  • 제약된 타입 변수를 사용한 함수 타입 어노테이션:
    AnyStr = TypeVar('AnyStr', str, bytes)
    
    def longest(first: AnyStr, second: AnyStr) -> AnyStr:
        return first if len(first) >= len(second) else second
    
    result = longest('a', 'abc')  # The inferred type for result is str
    
    result = longest('a', b'abc')  # Fails static type check
    

    이 예에서는 longest()의 두 인자가 동일한 타입(str 또는 bytes)이어야 하며, 더 나아가 인자들이 공통 str 서브클래스의 인스턴스이더라도 반환 타입은 해당 서브클래스가 아니라 여전히 str입니다(다음 예 참조).

  • 비교를 위해, 타입 변수가 제한되지 않았다면 공통 서브클래스가 반환 타입으로 선택됩니다. 예를 들면 다음과 같습니다.:
    S = TypeVar('S')
    
    def longest(first: S, second: S) -> S:
        return first if len(first) >= len(second) else second
    
    class MyStr(str): ...
    
    result = longest(MyStr('a'), MyStr('abc'))
    

    result의 추론된 타입은 MyStr입니다(반면 AnyStr 예에서는 str입니다).

  • 비교를 위해서도 Union을 사용하는 경우 반환 타입 역시 Union이어야 합니다.:
    U = Union[str, bytes]
    
    def longest(first: U, second: U) -> U:
        return first if len(first) >= len(second) else second
    
    result = longest('a', 'abc')
    

    두 인자가 모두 str임에도 불구하고 result의 추론된 타입은 여전히 Union[str, bytes]입니다.

    타입 검사기가 이 함수를 거부한다는 점에 유의하십시오.:

    def concat(first: U, second: U) -> U:
        return first + second  # Error: can't concatenate str and bytes
    

    매개변수의 타입이 동시에만 변경될 수 있는 경우에는 제약된 타입 변수를 사용해야 합니다.

제네릭 타입 정의 및 사용

사용자는 특수한 구성 요소인 Generic을 사용하여 자신의 클래스를 제네릭 타입으로 선언할 수 있습니다. class MyGeneric(Generic[X, Y, ...]): ...라는 정의는 타입 변수 X등을 기반으로 하는 제네릭 타입 MyGeneric을 정의합니다. MyGeneric자체는 매개변수화할 수 있게 되며, 예를 들어 MyGeneric[int, str, ...]X -> int등으로 치환된 특정 타입입니다. 예시는 다음과 같습니다.:

class CustomQueue(Generic[T]):

    def put(self, task: T) -> None:
        ...
    def get(self) -> T:
        ...

def communicate(queue: CustomQueue[str]) -> Optional[str]:
    ...

제네릭 타입에서 파생된 클래스는 제네릭이 됩니다. 하나의 클래스는 여러 제네릭 타입을 서브클래스로 지정할 수 있습니다. 그러나 제네릭이 반환하는 특정 타입에서 파생된 클래스는 제네릭이 아닙니다. 예시는 다음과 같습니다.:

class TodoList(Iterable[T], Container[T]):
    def check(self, item: T) -> None:
        ...

def check_all(todo: TodoList[T]) -> None:  # TodoList is generic
    ...

class URLList(Iterable[bytes]):
    def scrape_all(self) -> None:
        ...

def search(urls: URLList) -> Optional[bytes]  # URLList is not generic
    ...

제네릭 타입을 서브클래스로 지정하면 이에 대응하는 특정 타입에 서브타입 관계가 적용되므로, 위의 예에서 TodoList[t1]Iterable[t1]의 서브타입입니다.

제네릭 타입은 여러 단계로 특수화(인덱싱)할 수 있습니다. 각 타입 변수는 특정 타입 또는 다른 제네릭 타입으로 치환할 수 있습니다. 기본 클래스 목록에 Generic이 나타나는 경우 여기에는 모든 타입 변수가 포함되어야 하며, 타입 매개변수의 순서는 해당 타입 변수가 Generic에 나타나는 순서에 따라 결정됩니다. 예시는 다음과 같습니다.:

Table = Dict[int, T]     # Table is generic
Messages = Table[bytes]  # Same as Dict[int, bytes]

class BaseGeneric(Generic[T, S]):
    ...

class DerivedGeneric(BaseGeneric[int, T]): # DerivedGeneric has one parameter
    ...

SpecificType = DerivedGeneric[int]         # OK

class MyDictView(Generic[S, T, U], Iterable[Tuple[U, T]]):
    ...

Example = MyDictView[list, int, str]       # S -> list, T -> int, U -> str

타입 변수가 생략된 타입 어노테이션에 제네릭 타입이 나타나는 경우 Any로 간주됩니다. 이러한 형식은 동적 타이핑의 대체 수단으로 사용할 수 있으며 issubclassisinstance와 함께 사용할 수 있습니다. 인스턴스의 모든 타입 정보는 런타임에 제거됩니다. 예시는 다음과 같습니다.:

def count(seq: Sequence) -> int:      # Same as Sequence[Any]
    ...

class FrameworkBase(Generic[S, T]):
    ...

class UserClass:
    ...

issubclass(UserClass, FrameworkBase)  # This is OK

class Node(Generic[T]):
   ...

IntNode = Node[int]
my_node = IntNode()  # at runtime my_node.__class__ is Node
                     # inferred static type of my_node is Node[int]

공변성과 반공변성

t2t1의 서브타입인 경우, 제네릭 타입 생성자 GenType는 다음과 같이 불립니다:

  • 공변: 이러한 모든 t1t2에 대해 GenType[t2]GenType[t1]의 서브타입인 경우입니다.
  • 반공변: 이러한 모든 t1t2에 대해 GenType[t1]GenType[t2]의 서브타입인 경우입니다.
  • 불변: 위의 어느 경우도 성립하지 않는 경우입니다.

이 정의를 더 잘 이해하기 위해 일반 함수를 사용하여 비유해 보겠습니다. 다음과 같다고 가정하십시오.:

def cov(x: float) -> float:
    return 2*x

def contra(x: float) -> float:
    return -x

def inv(x: float) -> float:
    return x*x

x1 < x2이면 항상 cov(x1) < cov(x2)이고 contra(x2) < contra(x1)이지만, inv에 대해서는 아무것도 말할 수 없습니다. <를 is-subtype-of로, 함수를 제네릭 타입 생성자로 바꾸면 공변, 반공변 및 불변 동작의 예를 얻을 수 있습니다. 이제 실제 예를 살펴보겠습니다.

  • Union은 모든 인자에 대해 공변적으로 동작합니다. 실제로 위에서 설명한 것처럼 t1u1의 서브타입인 경우 등에는 Union[t1, t2, ...]Union[u1, u2, ...]의 서브타입입니다.
  • FrozenSet[T]도 공변적입니다. T 대신 intfloat을 생각해 봅시다. 첫째, intfloat의 서브타입입니다. 둘째, FrozenSet[int]의 값 집합은 명백히 FrozenSet[float]의 값 집합의 부분집합인 반면, FrozenSet[float]로부터의 함수 집합은 FrozenSet[int]로부터의 함수 집합의 부분집합입니다. 따라서 정의에 따라 FrozenSet[int]FrozenSet[float]의 서브타입입니다.
  • List[T]는 불변적입니다. 실제로 List[int]의 값 집합이 List[float]의 값 집합의 부분집합이더라도, “배경” 절에서 논의했듯이 List[int]에는 int만 추가할 수 있습니다. 따라서 List[int]List[float]의 서브타입이 아닙니다. 이는 가변 타입에서 전형적으로 나타나는 상황이며, 가변 타입은 일반적으로 불변적입니다.

(다소 직관에 반하는) 반공변적 동작을 설명하는 가장 좋은 예 중 하나는 호출 가능 객체 타입입니다. 이는 반환 타입에 대해서는 공변적이지만, 인자에 대해서는 반공변적입니다. 반환 타입만 다른 두 호출 가능 객체 타입의 경우, 호출 가능 객체 타입의 서브타입 관계는 반환 타입의 서브타입 관계를 따릅니다. 예:

  • Callable[[], int]Callable[[], float]의 서브타입입니다.
  • Callable[[], Manager]Callable[[], Employee]의 서브타입입니다.

반면 인자 하나의 타입만 다른 두 호출 가능 객체 타입의 경우, 호출 가능 객체 타입의 서브타입 관계는 인자 타입의 서브타입 관계와 반대 방향으로 진행됩니다. 예:

  • Callable[[float], None]Callable[[int], None]의 서브타입입니다.
  • Callable[[Employee], None]Callable[[Manager], None]의 서브타입입니다.

그렇습니다, 제대로 읽으신 것입니다. 실제로 매니저의 급여를 계산할 수 있는 함수가 필요한 경우:

def calculate_all(lst: List[Manager], salary: Callable[[Manager], Decimal]):
    ...

모든 직원의 급여를 계산할 수 있는 Callable[[Employee], Decimal]도 사용할 수 있습니다.

Callable를 사용한 이 예는 함수에 대해 더 정밀한 타입 어노테이션을 작성하는 방법을 보여줍니다: 모든 인자에 대해서는 가장 일반적인 타입을 선택하고, 반환값에 대해서는 가장 구체적인 타입을 선택하십시오.

매개변수로 사용되는 타입 변수를 정의할 때 특수 키워드 covariantcontravariant를 사용하여 사용자 정의 제네릭 타입의 분산을 선언하는 것이 가능합니다. 타입은 기본적으로 불변(invariant)입니다. 예제:

T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)

class LinkedList(Generic[T]):  # invariant by default
    ...
    def append(self, element: T) -> None:
        ...

class Box(Generic[T_co]):      #  this type is declared covariant
    def __init__(self, content: T_co) -> None:
        self._content = content
    def get_content(self) -> T_co:
        return self._content

class Sink(Generic[T_contra]): # this type is declared contravariant
    def send_to_nowhere(self, data: T_contra) -> None:
        with open(os.devnull, 'w') as devnull:
            print(data, file=devnull)

분산이 타입 변수를 통해 정의되지만, 이는 타입 변수의 속성이 아니라 제네릭 타입의 속성이라는 점에 유의하십시오. 파생 제네릭의 복잡한 정의에서 분산은 오직 사용된 타입 변수로부터만 결정됩니다. 복잡한 예제:

T_co = TypeVar('T_co', Employee, Manager, covariant=True)
T_contra = TypeVar('T_contra', Employee, Manager, contravariant=True)

class Base(Generic[T_contra]):
    ...

class Derived(Base[T_co]):
    ...

타입 검사기는 두 번째 선언으로부터 Derived[Manager]Derived[Employee]의 서브타입이고, Derived[t1]Base[t1]의 서브타입이라는 것을 알아냅니다. 서브타입 관계를 <로 나타내면, 이 경우의 전체 서브타이핑 다이어그램은 다음과 같습니다.:

Base[Manager]    >  Base[Employee]
    v                   v
Derived[Manager] <  Derived[Employee]

따라서 타입 검사기는 예를 들어 Derived[Manager]Base[Employee]의 서브타입이라는 것도 알아냅니다.

타입 변수, 제네릭 타입, 분산에 대한 자세한 내용은 PEP 484, 제네릭에 대한 mypy 문서, 위키백과를 참조하십시오.

실용적 측면

일부 사항들은 이론과는 무관하지만 실제 사용을 더 편리하게 만듭니다. (이는 전체 목록이 아니며, 몇 가지를 빠뜨렸을 수 있고 일부는 여전히 논란의 여지가 있거나 완전히 명시되지 않았을 수 있습니다.)

  • 타입이 예상되는 곳에서는 Nonetype(None)으로 대체할 수 있습니다. 예를 들어 Union[t1, None] == Union[t1, type(None)]과 같습니다.
  • 타입 별칭, 예를 들어:
    Point = Tuple[float, float]
    def distance(point: Point) -> float: ...
    
  • 문자열을 통한 전방 참조, 예를 들어:
    class MyComparable:
        def compare(self, other: 'MyComparable') -> int: ...
    
  • 타입 변수는 제약 없는(unconstrained), 제약된(constrained), 또는 경계가 있는(bounded) 형태로 선언할 수 있습니다. 제네릭 타입의 변성(variance)은 특별한 구문 없이도 특수 키워드 인자로 선언된 타입 변수를 사용하여 나타낼 수 있습니다. 예::
    T = TypeVar('T', bound=complex)
    
    def add(x: T, y: T) -> T:
        return x + y
    
    T_co = TypeVar('T_co', covariant=True)
    
    class ImmutableList(Generic[T_co]): ...
    
  • 주석을 이용한 타입 선언. 예::
    lst = []  # type: Sequence[int]
    
  • cast(T, obj)를 사용한 캐스트. 예::
    zork = cast(Any, frobozz())
    
  • 오버로딩과 스텁 모듈 같은 그 밖의 것들은 PEP 484를 참조하십시오.

typing.py의 미리 정의된 제네릭 타입과 프로토콜

(typing.py 모듈도 참조하십시오.)

  • collections.abc의 모든 것(단, SetAbstractSet으로 이름이 변경됨).
  • Dict, List, Set, FrozenSet 등 몇 가지 더.
  • re.Pattern[AnyStr], re.Match[AnyStr].
  • io.IO[AnyStr], io.TextIO ~ io.IO[str], io.BinaryIO ~ io.IO[bytes].