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

Python 개선 제안 한국어 번역

PEP 695 – 타입 매개변수 구문

Author:
Eric Traut <erictr at microsoft.com>
Sponsor:
Guido van Rossum <guido at python.org>
Discussions-To:
Typing-SIG thread
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
15-Jun-2022
Python-Version:
3.12
Post-History:
20-Jun-2022, 04-Dec-2022
Resolution:
Discourse message

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document: see Variance Inference, Type aliases, Type parameter lists, The type statementAnnotation scopes for up-to-date specs and documentation. Canonical typing specs are maintained at the typing specs site; runtime typing behaviour is described in the CPython documentation.

×

See the typing specification update process for how to propose changes to the typing spec.

초록

이 PEP는 제네릭 클래스, 함수 또는 타입 별칭 내에서 타입 매개변수를 지정하기 위한 개선된 구문을 명시합니다. 또한 타입 별칭을 선언하기 위한 새로운 문을 도입합니다.

동기

PEP 484에서는 타입 변수를 언어에 도입했습니다. PEP 612에서는 매개변수 명세를 도입하여 이 개념을 확장했으며, PEP 646에서는 가변 타입 변수를 추가했습니다.

제네릭 타입과 타입 매개변수가 점점 더 널리 사용되고 있지만, 타입 매개변수를 지정하는 구문은 여전히 Python에 억지로 덧붙여진 것처럼 느껴집니다. 이는 Python 개발자 사이에서 혼란을 일으키는 원인입니다.

Python 정적 타입 지정 커뮤니티에서는 제네릭 타입을 지원하는 다른 현대 프로그래밍 언어와 유사한 공식 구문을 제공할 때가 되었다는 데 의견이 일치합니다.

널리 사용되는 타입이 지정된 Python 라이브러리 25개를 분석한 결과, 타입 변수, 특히 typing.TypeVar 기호가 모듈의 14%에서 사용되는 것으로 나타났습니다.

혼동을 일으키는 지점

타입 변수의 사용이 널리 퍼졌지만, 코드 내에서 타입 변수를 지정하는 방식은 많은 Python 개발자에게 혼란의 원인입니다. 이러한 혼란에 기여하는 요인은 몇 가지가 있습니다.

타입 변수의 스코프 규칙은 이해하기 어렵습니다. 타입 변수는 일반적으로 전역 스코프 내에서 할당되지만, 그 의미는 제네릭 클래스, 함수 또는 타입 별칭의 컨텍스트 내에서 사용될 때만 유효합니다. 하나의 타입 변수 런타임 인스턴스를 여러 제네릭 컨텍스트에서 재사용할 수 있으며, 각 컨텍스트에서는 서로 다른 의미를 가집니다. 이 PEP는 클래스, 함수 또는 타입 별칭 선언문 내의 자연스러운 위치에서 타입 매개변수를 선언하여 이러한 혼란의 원인을 제거할 것을 제안합니다.

제네릭 타입 별칭을 사용할 때 타입 인자를 제공해야 한다는 점이 개발자에게 명확하지 않기 때문에 제네릭 타입 별칭이 오용되는 경우가 많습니다. 이로 인해 Any라는 암시적 타입 인자가 사용되며, 이는 의도인 경우가 드뭅니다. 이 PEP는 제네릭 타입 별칭 선언을 명확하게 만드는 새로운 구문을 추가할 것을 제안합니다.

PEP 483PEP 484에서는 제네릭 클래스 내에서 사용되는 타입 변수에 대한 “변성” 개념을 도입했습니다. 타입 변수는 불변, 공변 또는 반변일 수 있습니다. 변성은 타입 이론의 고급 세부 사항으로, 대부분의 Python 개발자가 잘 이해하지 못하지만, 오늘날 첫 제네릭 클래스를 정의할 때 이 개념을 마주해야 합니다. 이 PEP는 제네릭 클래스를 정의할 때 대부분의 개발자가 변성 개념을 이해해야 할 필요를 크게 줄입니다.

제네릭 클래스 또는 타입 별칭에 둘 이상의 타입 매개변수를 사용하는 경우 타입 매개변수 순서 지정 규칙이 혼란스러울 수 있습니다. 일반적으로 이는 클래스 또는 타입 별칭 선언문 내에서 타입 매개변수가 처음 나타나는 순서를 기반으로 합니다. 그러나 클래스 정의에 “Generic” 또는 “Protocol” 프로토콜 베이스 클래스를 포함하여 이를 재정의할 수 있습니다. 예를 들어, class ClassA(Mapping[K, V])라는 클래스 선언에서는 타입 매개변수의 순서가 K다음에 V가 됩니다. 그러나 class ClassB(Mapping[K, V], Generic[V, K])라는 클래스 선언에서는 타입 매개변수의 순서가 V다음에 K가 됩니다. 이 PEP는 모든 경우에 타입 매개변수 순서를 명시적으로 지정할 것을 제안합니다.

여러 제네릭 컨텍스트에서 타입 변수를 공유하는 관행은 오늘날 다른 문제를 일으킵니다. 최신 편집기는 의미론 수준에서 기호에 작동하는 “모든 참조 찾기” 및 “모든 참조 이름 바꾸기”와 같은 기능을 제공합니다. 타입 매개변수를 여러 제네릭 클래스, 함수 및 타입 별칭에서 공유하면 모든 참조가 의미론적으로 동등합니다.

전역 범위에서 정의된 타입 변수에도 해당 변수가 모듈에 대해 비공개임을 나타내도록 밑줄로 시작하는 이름을 지정해야 합니다. 전역에서 정의된 타입 변수에는 변성을 나타내는 이름을 지정하는 경우도 많아서, “_T_contra” 및 “_KT_co”와 같이 번거로운 이름이 생깁니다. 현재 타입 변수를 할당하는 메커니즘에서는 개발자가 따옴표 안에 중복된 이름(예: T = TypeVar("T"))을 제공해야 합니다. 이 PEP에서는 중복된 이름과 번거로운 변수 이름이 필요하지 않도록 합니다.

오늘날 타입 매개변수를 정의하려면 typing 모듈에서 TypeVarGeneric기호를 가져와야 합니다. 지난 여러 Python 릴리스에 걸쳐 일반적인 사용 사례에서 typing 기호를 가져올 필요를 없애려는 노력이 이루어졌으며, 이 PEP는 이러한 목표를 더욱 진전시킵니다.

요약 예제

이 PEP 이전에 제네릭 클래스를 정의하는 방식은 다음과 비슷합니다.

from typing import Generic, TypeVar

_T_co = TypeVar("_T_co", covariant=True, bound=str)

class ClassA(Generic[_T_co]):
    def method1(self) -> _T_co:
        ...

새로운 구문을 사용하면 다음과 같습니다.

class ClassA[T: str]:
    def method1(self) -> T:
        ...

다음은 현재 제네릭 함수의 예입니다.

from typing import TypeVar

_T = TypeVar("_T")

def func(a: _T, b: _T) -> _T:
    ...

새로운 구문은 다음과 같습니다.

def func[T](a: T, b: T) -> T:
    ...

다음은 현재 제네릭 타입 별칭의 예입니다.

from typing import TypeAlias

_T = TypeVar("_T")

ListOrSet: TypeAlias = list[_T] | set[_T]

새로운 구문을 사용하면 다음과 같습니다.

type ListOrSet[T] = list[T] | set[T]

명세

타입 매개변수 선언

제네릭 클래스, 함수 및 타입 별칭의 타입 매개변수를 선언하는 새로운 구문은 다음과 같습니다. 이 구문은 클래스, 함수 또는 타입 별칭의 이름 뒤 대괄호 안에 쉼표로 구분된 타입 매개변수 목록을 지원합니다.

단순한(가변이 아닌) 타입 변수는 별도의 표식이 없는 이름으로 선언합니다. 가변 타입 변수 앞에는 *가 옵니다(PEP 646 참조). 매개변수 명세 앞에는 **가 옵니다(PEP 612 참조).

# This generic class is parameterized by a TypeVar T, a
# TypeVarTuple Ts, and a ParamSpec P.
class ChildClass[T, *Ts, **P]: ...

Generic을 기본 클래스로 포함할 필요가 없습니다. 타입 매개변수가 있으면 기본 클래스로 포함된 것으로 간주하며, 클래스의 __mro____orig_bases__ 특성에도 자동으로 포함됩니다. Generic 기본 클래스를 명시적으로 사용하면 런타임 오류가 발생합니다.

class ClassA[T](Generic[T]): ...  # Runtime error

타입 인자가 있는 Protocol 기본 클래스는 런타임 오류를 일으킬 수 있습니다. 이 경우 타입 인자를 사용할 필요가 없고 클래스의 타입 매개변수 순서가 더 이상 Protocol 기본 클래스에서의 순서로 결정되지 않으므로, 타입 검사기는 오류를 생성해야 합니다.

class ClassA[S, T](Protocol): ... # OK

class ClassB[S, T](Protocol[S, T]): ... # Recommended type checker error

제네릭 클래스, 함수 또는 타입 별칭 내부의 타입 매개변수 이름은 동일한 클래스, 함수 또는 타입 별칭 내에서 고유해야 합니다. 중복된 이름은 컴파일 시점에 구문 오류를 발생시킵니다. 이는 함수 시그니처 내의 매개변수 이름이 고유해야 한다는 요구 사항과 일치합니다.

class ClassA[T, *T]: ... # Syntax Error

def func1[T, **T](): ... # Syntax Error

클래스 타입 매개변수 이름은 이중 밑줄로 시작하면 클래스 내부에서 사용되는 이름의 이름 조회 메커니즘이 복잡해지는 것을 방지하기 위해 맹글링됩니다. 그러나 타입 매개변수의 __name__속성에는 맹글링되지 않은 이름이 저장됩니다.

상한 지정

비가변 타입 매개변수의 경우 타입 어노테이션 표현식을 사용하여 “상한” 타입을 지정할 수 있습니다. 상한을 지정하지 않으면 상한은 object로 간주합니다.

class ClassA[T: str]: ...

지정된 상한 타입은 타입 어노테이션에서 허용되는 표현식 형식을 사용해야 합니다. 타입 검사기는 더 복잡한 표현식 형식을 오류로 표시해야 합니다. 인용된 전방 참조는 허용됩니다.

지정된 상한 타입은 구체적이어야 합니다. 제네릭 타입을 사용하려는 시도는 타입 검사기가 오류로 표시해야 합니다. 이는 타입 검사기가 TypeVar 생성자 호출에 대해 적용하는 기존 규칙과 일치합니다.

class ClassA[T: dict[str, int]]: ...  # OK

class ClassB[T: "ForwardReference"]: ...  # OK

class ClassC[V]:
    class ClassD[T: dict[str, V]]: ...  # Type checker error: generic type

class ClassE[T: [str, int]]: ...  # Type checker error: illegal expression form

제약 타입 지정

PEP 484에서는 두 개 이상의 타입 집합으로 제한되는 “제약된 타입 변수” 개념을 도입했습니다. 새로운 구문은 두 개 이상의 타입을 포함하는 리터럴 튜플 표현식을 사용하여 이러한 제약 유형을 지원합니다.

class ClassA[AnyStr: (str, bytes)]: ...  # OK

class ClassB[T: ("ForwardReference", bytes)]: ...  # OK

class ClassC[T: ()]: ...  # Type checker error: two or more types required

class ClassD[T: (str, )]: ...  # Type checker error: two or more types required

t1 = (bytes, str)
class ClassE[T: t1]: ...  # Type checker error: literal tuple expression required

지정된 타입이 튜플 표현식이 아니거나 튜플 표현식에 타입 어노테이션에서 허용되지 않는 복잡한 표현식 형식이 포함된 경우, 타입 검사기는 오류를 생성해야 합니다. 인용된 전방 참조는 허용됩니다.

class ClassF[T: (3, bytes)]: ...  # Type checker error: invalid expression form

지정된 제약 타입은 구체적이어야 합니다. 제네릭 타입을 사용하려는 시도는 타입 검사기가 오류로 표시해야 합니다. 이는 타입 검사기가 TypeVar 생성자 호출에 대해 적용하는 기존 규칙과 일치합니다.

class ClassG[T: (list[S], str)]: ...  # Type checker error: generic type

상한 및 제약의 런타임 표현

TypeVar객체의 상한과 제약은 런타임에 __bound____constraints__속성을 통해 액세스할 수 있습니다. 새로운 구문을 통해 정의된 TypeVar객체의 경우, 아래의 Lazy Evaluation에서 설명하는 대로 이러한 속성은 지연 평가됩니다.

제네릭 타입 별칭

타입 별칭을 선언하기 위한 새로운 문을 도입할 것을 제안합니다. classdef 문과 마찬가지로 type 문은 타입 매개변수를 위한 스코프를 정의합니다.

# A non-generic type alias
type IntOrStr = int | str

# A generic type alias
type ListOrSet[T] = list[T] | set[T]

타입 별칭은 따옴표를 사용하지 않고도 자기 자신을 참조할 수 있습니다.

# A type alias that includes a forward reference
type AnimalOrVegetable = Animal | "Vegetable"

# A generic self-referential type alias
type RecursiveList[T] = T | list[RecursiveList[T]]

type 키워드는 새로운 소프트 키워드입니다. 이 문법 부분에서만 키워드로 해석됩니다. 다른 모든 위치에서는 식별자 이름으로 간주됩니다.

제네릭 타입 별칭의 일부로 선언된 타입 매개변수는 타입 별칭의 우변을 평가할 때만 유효합니다.

typing.TypeAlias와 마찬가지로 타입 검사기는 우변 표현식을 타입 어노테이션 내에서 허용되는 표현식 형식으로 제한해야 합니다. 더 복잡한 표현식 형식(호출 표현식, 삼항 연산자, 산술 연산자, 비교 연산자 등)을 사용하면 타입 검사기가 오류로 표시해야 합니다.

타입 별칭 표현식에서는 전통적인 타입 변수(즉, 명시적인 TypeVar 생성자 호출로 할당된 변수)를 사용할 수 없습니다. 타입 검사기는 이 경우 오류를 생성해야 합니다.

T = TypeVar("T")
type MyList = list[T]  # Type checker error: traditional type variable usage

관련 PEP 613에서 도입된 기존 typing.TypeAlias를 폐기할 것을 제안합니다. 새로운 구문은 이를 전혀 필요로 하지 않게 합니다.

런타임 타입 별칭 클래스

런타임에 type 문은 typing.TypeAliasType의 인스턴스를 생성합니다. 이 클래스는 타입을 나타냅니다. 해당 속성은 다음과 같습니다.

  • __name__은 타입 별칭의 이름을 나타내는 str입니다.
  • __type_params__는 제네릭인 경우 타입 별칭을 매개변수화하는 TypeVar, TypeVarTuple 또는 ParamSpec 객체의 튜플입니다.
  • __value__는 타입 별칭의 평가된 값입니다.

이러한 모든 속성은 읽기 전용입니다.

타입 별칭의 값은 느리게 평가됩니다(Lazy Evaluation을 아래에서 참조하십시오).

타입 매개변수 스코프

새로운 구문을 사용하면 새로운 어휘적 스코프가 도입되며, 이 스코프에는 타입 매개변수가 포함됩니다. 내부 스코프에서는 이름으로 타입 매개변수에 액세스할 수 있습니다. Python의 다른 기호와 마찬가지로 내부 스코프는 동일한 이름의 외부 스코프 기호를 재정의하는 자체 기호를 정의할 수 있습니다. 이 절에서는 새로운 스코프 규칙을 말로 설명합니다. 아래의 Scoping Behavior 절에서는 기존 Python 코드와 거의 동등한 코드로의 변환이라는 관점에서 동작을 명시합니다.

목록의 다른 곳에서 선언된 다른 타입 매개변수에서도 타입 매개변수가 보입니다. 이를 통해 타입 매개변수의 정의에서 다른 타입 매개변수를 사용할 수 있습니다. 현재 이 기능을 사용할 곳은 없지만, 향후 앞선 타입 매개변수에 의존하는 상한 표현식이나 타입 인자 기본값을 지원할 수 있는 가능성을 보존합니다.

더 앞선 타입 매개변수의 정의가 더 뒤에 있는 타입 매개변수를 참조하면, 해당 이름이 외부 스코프에 정의되어 있더라도 컴파일러 오류 또는 런타임 예외가 생성됩니다.

# The following generates no compiler error, but a type checker
# should generate an error because an upper bound type must be concrete,
# and ``Sequence[S]`` is generic. Future extensions to the type system may
# eliminate this limitation.
class ClassA[S, T: Sequence[S]]: ...

# The following generates no compiler error, because the bound for ``S``
# is lazily evaluated. However, type checkers should generate an error.
class ClassB[S: Sequence[T], T]: ...

제네릭 클래스의 일부로 선언된 타입 매개변수는 클래스 본문과 그 안에 포함된 내부 스코프에서 유효합니다. 또한 클래스 정의를 구성하는 인자 목록(베이스 클래스 및 모든 키워드 인자)을 평가할 때 타입 매개변수에 액세스할 수 있습니다. 이를 통해 베이스 클래스를 이러한 타입 매개변수로 매개변수화할 수 있습니다. 클래스 데코레이터를 포함하여 클래스 본문 외부에서는 타입 매개변수에 액세스할 수 없습니다.

class ClassA[T](BaseClass[T], param = Foo[T]): ...  # OK

print(T)  # Runtime error: 'T' is not defined

@dec(Foo[T])  # Runtime error: 'T' is not defined
class ClassA[T]: ...

제네릭 함수의 일부로 선언된 타입 매개변수는 함수 본문과 그 안에 포함된 모든 스코프에서 유효합니다. 또한 매개변수 및 반환 타입 어노테이션에서도 유효합니다. 함수 매개변수의 기본 인자 값은 이 스코프 외부에서 평가되므로, 기본값 표현식에서는 타입 매개변수에 액세스할 수 없습니다. 마찬가지로 함수 데코레이터에서는 타입 매개변수가 스코프에 포함되지 않습니다.

def func1[T](a: T) -> T: ...  # OK

print(T)  # Runtime error: 'T' is not defined

def func2[T](a = list[T]): ...  # Runtime error: 'T' is not defined

@dec(list[T])  # Runtime error: 'T' is not defined
def func3[T](): ...

제네릭 타입 별칭의 일부로 선언된 타입 매개변수는 타입 별칭 표현식에서 유효합니다.

type Alias1[K, V] = Mapping[K, V] | Sequence[K]

바깥 스코프에서 정의된 타입 매개변수 기호는 내부 스코프의 nonlocal 문으로 바인딩할 수 없습니다.

S = 0

def outer1[S]():
    S = 1
    T = 1

    def outer2[T]():

        def inner1():
            nonlocal S  # OK because it binds variable S from outer1
            nonlocal T  # Syntax error: nonlocal binding not allowed for type parameter

        def inner2():
            global S  # OK because it binds variable S from global scope

새로운 타입 매개변수 구문으로 도입되는 어휘적 스코프는 def 또는 class 문으로 도입되는 기존의 스코프와 다릅니다. 타입 매개변수 스코프는 포함하는 스코프에 대한 임시 “오버레이”처럼 동작합니다. 해당 기호 테이블에 포함되는 유일한 새 기호는 새 구문을 사용하여 정의된 타입 매개변수입니다. 다른 모든 기호에 대한 참조는 포함하는 스코프에서 발견된 것처럼 처리됩니다. 이를 통해 기본 클래스 목록(클래스 정의에서)과 타입 어노테이션 표현식(함수 정의에서)이 포함하는 스코프에 정의된 기호를 참조할 수 있습니다.

class Outer:
    class Private:
        pass

    # If the type parameter scope was like a traditional scope,
    # the base class 'Private' would not be accessible here.
    class Inner[T](Private, Sequence[T]):
        pass

    # Likewise, 'Inner' would not be available in these type annotations.
    def method1[T](self, a: Inner[T]) -> Inner[T]:
        return a

컴파일러는 내부 스코프에서 바깥 스코프의 타입 매개변수를 재정의하는 지역 기호를 정의하도록 허용합니다.

관련 PEP 484에 정의된 스코프 규칙에 따라, 내부 스코프의 제네릭 클래스, 함수 또는 타입 별칭이 외부 스코프와 동일한 타입 매개변수 이름을 재사용하면 타입 검사기는 오류를 생성해야 합니다.

T = 0

@decorator(T)  # Argument expression `T` evaluates to 0
class ClassA[T](Sequence[T]):
    T = 1

    # All methods below should result in a type checker error
    # "type parameter 'T' already in use" because they are using the
    # type parameter 'T', which is already in use by the outer scope
    # 'ClassA'.
    def method1[T](self):
        ...

    def method2[T](self, x = T):  # Parameter 'x' gets default value of 1
        ...

    def method3[T](self, x: T):  # Parameter 'x' has type T (scoped to method3)
        ...

내부 스코프에서 참조되는 기호는 이름 확인 중에 타입 매개변수 스코프도 고려된다는 점을 제외하면 기존 규칙을 사용하여 확인됩니다.

T = 0

# T refers to the global variable
print(T)  # Prints 0

class Outer[T]:
    T = 1

    # T refers to the local variable scoped to class 'Outer'
    print(T)  # Prints 1

    class Inner1:
        T = 2

        # T refers to the local type variable within 'Inner1'
        print(T)  # Prints 2

        def inner_method(self):
            # T refers to the type parameter scoped to class 'Outer';
            # If 'Outer' did not use the new type parameter syntax,
            # this would instead refer to the global variable 'T'
            print(T)  # Prints 'T'

    def outer_method(self):
        T = 3

        # T refers to the local variable within 'outer_method'
        print(T)  # Prints 3

        def inner_func():
            # T refers to the variable captured from 'outer_method'
            print(T)  # Prints 3

제네릭 클래스에 새로운 타입 매개변수 구문을 사용하는 경우 클래스 정의의 인자 목록 내에서 대입 표현식을 사용할 수 없습니다. 마찬가지로 새로운 타입 매개변수 구문을 사용하는 함수에서는 매개변수 또는 반환 타입 어노테이션 내에서 대입 표현식을 사용할 수 없으며, 타입 별칭을 정의하는 표현식 내에서도, TypeVar의 범위와 제약 조건 내에서도 사용할 수 없습니다. 이와 마찬가지로 이러한 컨텍스트에서는 yield, yield fromawait 표현식을 사용할 수 없습니다.

새로운 어휘적 스코프 내에서 평가되는 표현식이 정의된 타입 매개변수 이외의 기호를 해당 스코프에 도입해서는 안 되며, 바깥 함수가 제너레이터인지 코루틴인지에 영향을 주어서도 안 되므로 이러한 제한이 필요합니다.

class ClassA[T]((x := Sequence[T])): ...  # Syntax error: assignment expression not allowed

def func1[T](val: (x := int)): ...  # Syntax error: assignment expression not allowed

def func2[T]() -> (x := Sequence[T]): ...  # Syntax error: assignment expression not allowed

type Alias1[T] = (x := list[T])  # Syntax error: assignment expression not allowed

런타임에서 타입 매개변수에 액세스하기

제네릭 클래스, 함수 및 타입 별칭에서는 __type_params__라는 새 특성을 사용할 수 있습니다. 이 특성은 클래스, 함수 또는 별칭을 매개변수화하는 타입 매개변수의 튜플입니다. 이 튜플에는 TypeVar, ParamSpecTypeVarTuple 인스턴스가 포함됩니다.

새로운 구문을 사용하여 선언된 타입 매개변수는 globals() 또는 locals()가 반환하는 딕셔너리에 나타나지 않습니다.

분산 추론

이 PEP는 타입 매개변수에 분산을 지정할 필요를 없앱니다. 대신 타입 검사기는 클래스 내에서의 사용 방식에 따라 타입 매개변수의 분산을 추론합니다. 타입 매개변수는 사용 방식에 따라 불변, 공변 또는 반공변으로 추론됩니다.

Python 타입 검사기에는 이미 제네릭 프로토콜 클래스 내에서 분산을 검증하기 위해 타입 매개변수의 분산을 판별하는 기능이 포함되어 있습니다. 이 기능은 모든 클래스(프로토콜인지 여부와 관계없이)에 사용하여 각 타입 매개변수의 분산을 계산할 수 있습니다.

타입 매개변수의 분산을 계산하는 알고리즘은 다음과 같습니다.

제네릭 클래스의 각 타입 매개변수에 대해 다음을 수행합니다.

1. 타입 매개변수가 가변적(TypeVarTuple)이거나 매개변수 명세(ParamSpec)인 경우에는 항상 불변으로 간주합니다. 추가 추론은 필요하지 않습니다.

2. 타입 매개변수가 기존 TypeVar 선언에서 비롯되었고 infer_variance로 지정되지 않은 경우(아래 참조), 해당 분산은 TypeVar 생성자 호출로 지정됩니다. 추가 추론은 필요하지 않습니다.

3. 클래스의 특수화 버전 두 개를 만듭니다. 이를 다음과 같이 부르겠습니다. upperlower 특수화라고 합니다. 이 두 특수화에서는 추론 중인 타입 매개변수를 제외한 모든 타입 매개변수를 더미 타입 인스턴스(자기 자신과 타입 호환되고 해당 타입 매개변수의 바운드 또는 제약 조건을 충족한다고 가정하는 구체적인 익명 클래스)로 대체합니다. upper 특수화 클래스에서는 대상 타입 매개변수를 object 인스턴스로 특수화합니다. 이 특수화에서는 타입 매개변수의 상한 또는 제약 조건을 무시합니다. lower 특수화 클래스에서는 대상 타입 매개변수를 자기 자신으로 특수화합니다(즉, 해당 타입 인자는 타입 매개변수 자체입니다).

4. 일반 타입을 사용하여 lowerupper에 할당할 수 있는지 결정합니다 호환성 규칙을 사용하여 확인합니다. 그렇다면 대상 타입 매개변수는 공변입니다. 그렇지 않다면 upperlower에 할당할 수 있는지 판단합니다. 그렇다면 대상 타입 매개변수는 반공변입니다. 이 두 조합 중 어느 것도 할당할 수 없다면 대상 타입 매개변수는 불변입니다.

다음은 예시입니다.

class ClassA[T1, T2, T3](list[T1]):
    def method1(self, a: T2) -> None:
        ...

    def method2(self) -> T3:
        ...

T1의 분산을 확인하기 위해 다음과 같이 ClassA를 특수화합니다.

upper = ClassA[object, Dummy, Dummy]
lower = ClassA[T1, Dummy, Dummy]

관련 PEP 484에 정의된 일반적인 타입 호환성 규칙을 사용하면 upperlower에 할당할 수 없음을 확인할 수 있습니다. 마찬가지로 lowerupper에 할당할 수 없으므로 T1은 불변이라고 결론 내립니다.

T2의 분산을 확인하기 위해 다음과 같이 ClassA를 특수화합니다.

upper = ClassA[Dummy, object, Dummy]
lower = ClassA[Dummy, T2, Dummy]

upperlower에 할당할 수 있으므로 T2는 반공변입니다.

T3의 분산을 확인하기 위해 다음과 같이 ClassA를 특수화합니다.

upper = ClassA[Dummy, Dummy, object]
lower = ClassA[Dummy, Dummy, T3]

lowerupper에 할당할 수 있으므로 T3는 공변입니다.

TypeVar의 자동 분산

기존 TypeVar 클래스 생성자는 covariantcontravariant라는 키워드 매개변수를 허용합니다. 이 두 매개변수가 모두 False이면 해당 타입 변수는 불변으로 간주합니다. 타입 검사기가 추론을 사용하여 타입 변수가 불변인지, 공변인지 또는 반공변인지 결정해야 함을 나타내는 infer_variance라는 키워드 매개변수를 추가할 것을 제안합니다. 해당 인스턴스 변수 __infer_variance__는 런타임에 액세스하여 분산이 추론되는지 확인할 수 있습니다. 새 구문을 사용하여 암시적으로 할당되는 타입 변수는 항상 __infer_variance__True로 설정됩니다.

기존 구문을 사용하는 제네릭 클래스에는 명시적 분산과 추론된 분산을 사용하는 타입 변수의 조합이 포함될 수 있습니다.

T1 = TypeVar("T1", infer_variance=True)  # Inferred variance
T2 = TypeVar("T2")  # Invariant
T3 = TypeVar("T3", covariant=True)  # Covariant

# A type checker should infer the variance for T1 but use the
# specified variance for T2 and T3.
class ClassA(Generic[T1, T2, T3]): ...

기존 TypeVar와의 호환성

하위 호환성을 위해 TypeVar, TypeVarTupleParamSpec을 할당하는 기존 메커니즘을 유지합니다. 그러나 이러한 “전통적인” 타입 변수는 새 구문을 사용하여 할당한 타입 매개변수와 함께 사용해서는 안 됩니다. 이러한 조합은 타입 검사기가 오류로 표시해야 합니다. 타입 매개변수의 순서가 모호하기 때문에 필요합니다.

클래스, 함수 또는 타입 별칭이 새 구문을 사용하지 않는 경우에는 전통적인 타입 변수와 새로운 스타일의 타입 매개변수를 함께 사용해도 됩니다. 이 경우 새로운 스타일의 타입 매개변수는 바깥쪽 스코프에서 가져와야 합니다.

K = TypeVar("K")

class ClassA[V](dict[K, V]): ...  # Type checker error

class ClassB[K, V](dict[K, V]): ...  # OK

class ClassC[V]:
    # The use of K and V for "method1" is OK because it uses the
    # "traditional" generic function mechanism where type parameters
    # are implicit. In this case V comes from an outer scope (ClassC)
    # and K is introduced implicitly as a type parameter for "method1".
    def method1(self, a: V, b: K) -> V | K: ...

    # The use of M and K are not allowed for "method2". A type checker
    # should generate an error in this case because this method uses the
    # new syntax for type parameters, and all type parameters associated
    # with the method must be explicitly declared. In this case, ``K``
    # is not declared by "method2", nor is it supplied by a new-style
    # type parameter defined in an outer scope.
    def method2[M](self, a: M, b: K) -> M | K: ...

런타임 구현

문법 변경

이 PEP에서는 새로운 소프트 키워드 type을 도입합니다. 다음과 같은 방식으로 문법을 수정합니다.

  1. classdef 문에 선택적 타입 매개변수 절을 추가합니다.
type_params: '[' t=type_param_seq  ']'

type_param_seq: a[asdl_typeparam_seq*]=','.type_param+ [',']

type_param:
    | a=NAME b=[type_param_bound]
    | '*' a=NAME
    | '**' a=NAME

type_param_bound: ":" e=expression

# Grammar definitions for class_def_raw and function_def_raw are modified
# to reference type_params as an optional syntax element. The definitions
# of class_def_raw and function_def_raw are simplified here for brevity.

class_def_raw: 'class' n=NAME t=[type_params] ...

function_def_raw: a=[ASYNC] 'def' n=NAME t=[type_params] ...
  1. 타입 별칭을 정의하기 위한 새로운 type 문을 추가합니다.
type_alias: "type" n=NAME t=[type_params] '=' b=expression

AST 변경

이 PEP에서는 TypeAlias라는 새로운 AST 노드 타입을 도입합니다.

TypeAlias(expr name, typeparam* typeparams, expr value)

또한 타입 매개변수를 나타내는 AST 노드 타입을 추가합니다.

typeparam = TypeVar(identifier name, expr? bound)
    | ParamSpec(identifier name)
    | TypeVarTuple(identifier name)

바운드와 제약 조건은 AST에서 동일하게 표현됩니다. 구현에서는 Tuple AST 노드인 모든 표현식을 제약 조건으로 처리하고, 그 밖의 모든 표현식은 바운드로 처리합니다.

또한 기존 AST 노드 타입인 FunctionDef, AsyncFunctionDefClassDef를 수정하여 해당 함수 또는 클래스와 연결된 타입 매개변수 목록을 포함하는 typeparams라는 추가 선택적 속성을 포함하도록 합니다.

지연 평가

이 PEP에서는 정적 타입을 나타내는 표현식이 사용될 수 있는 세 가지 새로운 컨텍스트, 즉 TypeVar 바운드, TypeVar 제약 조건 및 타입 별칭의 값을 도입합니다. 이러한 표현식에는 아직 정의되지 않은 이름에 대한 참조가 포함될 수 있습니다. 예를 들어 타입 별칭은 재귀적이거나 상호 재귀적일 수 있으며, 타입 변수의 바운드는 현재 클래스를 다시 참조할 수 있습니다. 이러한 표현식을 즉시 평가한다면 런타임 오류를 방지하기 위해 사용자는 이러한 표현식을 따옴표로 묶어야 합니다. 타입 어노테이션에서 이러한 상황이 일으키는 문제는 PEP 563PEP 649에 자세히 설명되어 있습니다.

이 PEP에서 제안하는 새 구문으로 이와 유사한 상황이 발생하는 것을 방지하기 위해, PEP 649와 유사한 방식으로 이러한 표현식에 지연 평가를 사용하자고 제안합니다. 구체적으로 각 표현식은 코드 객체에 저장되며, 해당 코드 객체는 대응하는 속성(TypeVar.__bound__, TypeVar.__constraints__ 또는 TypeAlias.__value__)에 접근할 때만 평가됩니다. 값이 성공적으로 평가되면 해당 값이 저장되고, 이후 호출에서는 코드 객체를 다시 평가하지 않고 동일한 값을 반환합니다.

관련 PEP 649가 구현되면 PEP이 타입 어노테이션에 제공하는 옵션을 반영하도록 추가 평가 메커니즘을 도입해야 합니다. 현재 버전의 PEP에서는 PEP 649의 __annotate__ 메서드와 동일한 의미를 갖는 format 매개변수를 받는 __evaluate_bound__ 메서드를 TypeVar에 추가하는 것이 이에 포함될 수 있습니다(이와 유사한 __evaluate_constraints__ 메서드와 TypeAliasType에 대한 __evaluate_value__ 메서드도 포함될 수 있습니다). 그러나 PEP 649가 승인되고 구현될 때까지는 기본 평가 형식(PEP 649의 “VALUE” 형식)만 지원됩니다.

지연 평가의 결과로 속성에 대해 관찰되는 값은 해당 속성에 접근하는 시점에 따라 달라질 수 있습니다.

X = int

class Foo[T: X, U: X]:
    t, u = T, U

print(Foo.t.__bound__)  # prints "int"
X = str
print(Foo.u.__bound__)  # prints "str"

타입 어노테이션에 영향을 미치는 유사한 예제는 PEP 563 또는 PEP 649의 의미론을 사용하여 구성할 수 있습니다.

지연 평가를 순진하게 구현하면 클래스 네임스페이스를 잘못 처리하게 됩니다. 클래스 내부의 함수는 일반적으로 자신을 둘러싼 클래스 네임스페이스에 접근할 수 없기 때문입니다. 구현은 클래스 스코프의 이름이 올바르게 해석되도록 클래스 네임스페이스에 대한 참조를 유지합니다.

스코프 동작

새로운 구문에는 Python의 기존 스코프와 다르게 동작하는 새로운 종류의 스코프가 필요합니다. 따라서 새로운 구문은 기존 Python 스코프 동작의 관점에서 정확하게 설명할 수 없습니다. 이 절에서는 기존 스코프 동작을 참조하여 이러한 스코프를 더 자세히 규정합니다. 새로운 스코프는 아래에 나열된 몇 가지 사소한 차이를 제외하면 함수 스코프처럼 동작합니다.

모든 예제에는 의사 키워드 def695 로 도입된 함수가 포함되어 있습니다. 이 키워드는 실제 언어에는 존재하지 않으며, 새로운 스코프가 대부분 함수 스코프와 같다는 점을 명확히 하기 위해 사용합니다.

def695 스코프는 다음과 같은 방식으로 일반 함수 스코프와 다릅니다.

  • def695 스코프가 클래스 스코프 바로 내부에 있거나, 클래스 스코프 바로 내부에 있는 다른 def695 스코프 내부에 있다면 해당 클래스 스코프에서 정의된 이름에 def695 스코프 내부에서 접근할 수 있습니다. (반대로 일반 함수는 자신을 둘러싼 클래스 스코프 내부에 정의된 이름에 접근할 수 없습니다.)
  • 다음 구문 구성은 def695 스코프 내부에서 직접 사용할 수 없지만, def695 스코프 내부에 중첩된 다른 스코프에서는 사용할 수 있습니다.
    • yield
    • yield from
    • await
    • := (월러스 연산자)
  • def695 스코프 내부에서 정의된 객체(클래스와 함수)의 정규화된 이름(__qualname__)은 객체가 가장 가까운 바깥 스코프에서 정의된 것처럼 지정됩니다.
  • def695 스코프 내부에 바인딩된 이름은 중첩된 스코프의 nonlocal 문으로 다시 바인딩할 수 없습니다.

def695 스코프는 이 PEP에서 제안하는 여러 새로운 구문 구성의 평가에 사용됩니다. 일부는 즉시 평가되고(타입 별칭, 함수 또는 클래스가 정의될 때), 다른 일부는 지연 평가됩니다(평가가 명시적으로 요청될 때만). 모든 경우에 스코프 의미론은 동일합니다.

  • 즉시 평가되는 값:
    • 제네릭 타입 별칭의 타입 매개변수
    • 제네릭 함수의 타입 매개변수와 어노테이션
    • 제네릭 클래스의 타입 매개변수와 베이스 클래스 표현식
  • 지연 평가되는 값:
    • 제네릭 타입 별칭의 값
    • 타입 변수의 범위
    • 타입 변수의 제약 조건

아래 번역에서 두 개의 밑줄로 시작하는 이름은 구현 내부용이며 실제 Python 코드에서는 볼 수 없습니다. 실제 구현에서는 인터프리터에 직접 정의되는 다음 내재 함수를 사용합니다.

  • __make_typealias(*, name, type_params=(), evaluate_value): 지정된 이름, 타입 매개변수 및 지연 평가되는 값을 사용하여 새로운 typing.TypeAlias 객체를 생성합니다. __value__ 속성에 접근할 때까지 값은 평가되지 않습니다.
  • __make_typevar_with_bound(*, name, evaluate_bound): 주어진 이름과 지연 평가되는 바운드를 사용하여 새로운 typing.TypeVar 객체를 생성합니다. __bound__ 속성에 접근할 때까지 바운드를 평가하지 않습니다.
  • __make_typevar_with_constraints(*, name, evaluate_constraints): 주어진 이름과 지연 평가되는 제약 조건을 사용하여 새로운 typing.TypeVar 객체를 생성합니다. __constraints__ 속성에 접근할 때까지 제약 조건을 평가하지 않습니다.

비제네릭 타입 별칭은 다음과 같이 변환됩니다.:

type Alias = int

다음과 동등합니다.:

def695 __evaluate_Alias():
    return int

Alias = __make_typealias(name='Alias', evaluate_value=__evaluate_Alias)

제네릭 타입 별칭:

type Alias[T: int] = list[T]

다음과 동등합니다.:

def695 __generic_parameters_of_Alias():
    def695 __evaluate_T_bound():
        return int
    T = __make_typevar_with_bound(name='T', evaluate_bound=__evaluate_T_bound)

    def695 __evaluate_Alias():
        return list[T]
    return __make_typealias(name='Alias', type_params=(T,), evaluate_value=__evaluate_Alias)

Alias = __generic_parameters_of_Alias()

제네릭 함수:

def f[T](x: T) -> T:
    return x

다음과 동등합니다.:

def695 __generic_parameters_of_f():
    T = typing.TypeVar(name='T')

    def f(x: T) -> T:
        return x
    f.__type_params__ = (T,)
    return f

f = __generic_parameters_of_f()

기본값, 데코레이터 및 바운드의 스코프 동작을 보여 주는 제네릭 함수의 더 완전한 예입니다. 이 예제는 ParamSpec을 올바르게 사용하지 않으므로 정적 타입 검사기가 거부해야 합니다. 그러나 런타임에서는 유효하며, 여기서는 런타임 의미를 보여 주기 위해 사용합니다.

@decorator
def f[T: int, U: (int, str), *Ts, **P](
    x: T = SOME_CONSTANT,
    y: U,
    *args: *Ts,
    **kwargs: P.kwargs,
) -> T:
    return x

다음과 동등합니다.:

__default_of_x = SOME_CONSTANT  # evaluated outside the def695 scope
def695 __generic_parameters_of_f():
    def695 __evaluate_T_bound():
        return int
    T = __make_typevar_with_bound(name='T', evaluate_bound=__evaluate_T_bound)

    def695 __evaluate_U_constraints():
        return (int, str)
    U = __make_typevar_with_constraints(name='U', evaluate_constraints=__evaluate_U_constraints)

    Ts = typing.TypeVarTuple("Ts")
    P = typing.ParamSpec("P")

    def f(x: T = __default_of_x, y: U, *args: *Ts, **kwargs: P.kwargs) -> T:
        return x
    f.__type_params__ = (T, U, Ts, P)
    return f

f = decorator(__generic_parameters_of_f())

제네릭 클래스:

class C[T](Base):
    def __init__(self, x: T):
        self.x = x

다음과 동등합니다.:

def695 __generic_parameters_of_C():
    T = typing.TypeVar('T')
    class C(Base):
        __type_params__ = (T,)
        def __init__(self, x: T):
            self.x = x
   return C

C = __generic_parameters_of_C()

def695 스코프에서 기존 동작과 가장 크게 다른 점은 클래스 스코프 내에서의 동작입니다. 클래스 내에 정의된 제네릭이 직관적으로 동작하도록 하려면 이러한 차이가 필요합니다.:

class C:
    class Nested: ...
    def generic_method[T](self, x: T, y: Nested) -> T: ...

다음과 동등합니다.:

class C:
    class Nested: ...

    def695 __generic_parameters_of_generic_method():
        T = typing.TypeVar('T')

        def generic_method(self, x: T, y: Nested) -> T: ...
        return generic_method

    generic_method = __generic_parameters_of_generic_method()

이 예제에서 xy의 어노테이션은 def695 스코프 내에서 평가됩니다. 제네릭 메서드의 타입 매개변수 T에 접근해야 하기 때문입니다. 그러나 클래스 네임스페이스에 정의된 Nested 이름에도 접근해야 합니다. def695 스코프가 일반 함수 스코프처럼 동작한다면 함수 스코프 내에서 Nested가 표시되지 않을 것입니다. 따라서 위에서 설명한 것처럼 클래스 스코프 바로 안에 있는 def695 스코프는 해당 클래스 스코프에 접근할 수 있습니다.

라이브러리 변경 사항

현재 Python으로 구현된 typing 모듈의 여러 클래스는 C로 부분 구현해야 합니다. 여기에는 TypeVar, TypeVarTuple, ParamSpecGeneric과 새로운 클래스 TypeAliasType(위에서 설명함)가 포함됩니다. 구현은 모듈의 나머지 부분과 밀접하게 상호 작용하는 일부 동작에 대해 Python 버전의 typing.py에 처리를 위임할 수 있습니다. 이러한 클래스의 문서화된 동작은 변경되지 않아야 합니다.

참조 구현

이 제안은 CPython PR #103764에서 프로토타입으로 구현되었습니다.

Pyright 타입 검사기는 이 PEP에 설명된 동작을 지원합니다.

거부된 아이디어

접두 절

defclass 문 앞에 오는 타입 매개변수를 지정하기 위한 다양한 구문 옵션을 검토했습니다. 그러한 변형 중 하나로 다음과 같이 using 절을 사용하는 방식을 고려했습니다.

using S, T
class ClassA: ...

타입 매개변수의 스코프 규칙이 덜 명확했기 때문에 이 옵션은 기각했습니다. 또한 이 구문은 Python에서 흔히 사용되는 클래스 및 함수 데코레이터와도 잘 상호작용하지 못했습니다. 다른 인기 프로그래밍 언어 중에서는 C++만 이 방식을 사용합니다.

이와 마찬가지로 데코레이터처럼 보이는 접두 형태(예: @using(S, T)처럼)도 고려했습니다. 이러한 형태는 일반 데코레이터와 혼동될 수 있고 기존 데코레이터와도 잘 조합되지 않기 때문에 이 아이디어는 기각했습니다. 더욱이 데코레이터는 자신이 장식하는 문 뒤에 논리적으로 실행되므로, 데코레이터 자체보다 먼저 논리적으로 실행되는 “장식된” 문 내부에서 보이는 기호(타입 매개변수)를 데코레이터가 도입하게 하는 것은 혼란을 일으킬 수 있습니다.

꺾쇠괄호

제네릭을 지원하는 많은 언어는 꺾쇠괄호를 사용합니다. (요약은 부록 A 끝의 표를 참조하십시오.) Python에서 타입 매개변수 선언에 꺾쇠괄호를 사용하는 방식을 검토했지만, 궁극적으로 두 가지 이유로 이를 기각했습니다. 첫째, Python 스캐너는 꺾쇠괄호를 “쌍”으로 간주하지 않으므로 <> 토큰 사이의 줄 끝 문자가 유지됩니다. 즉, 타입 매개변수 목록 안에 줄 바꿈을 넣으려면 보기 흉하고 번거로운 \ 이스케이프 시퀀스를 사용해야 합니다. 둘째, Python은 이미 제네릭 타입의 명시적 특수화에 대괄호를 사용하는 방식을 확립했습니다(예: list[int]). 제네릭 선언에는 꺾쇠괄호를 사용하면서 명시적 특수화에는 대괄호를 사용하는 것은 일관성이 없고 혼란을 일으킬 것이라고 결론 내렸습니다. 조사한 다른 모든 언어는 이 점에서 일관되었습니다.

경계 구문

타입 변수의 경계와 제약 조건을 지정하기 위한 다양한 구문 옵션을 검토했습니다. Scala와 같은 <: 토큰의 사용, 여러 다른 언어에서와 같은 extends 또는 with 키워드의 사용, 그리고 현재의 typing.TypeVar 생성자와 유사한 함수 호출 구문의 사용을 고려했지만, 궁극적으로 모두 기각했습니다. 단순한 콜론 구문은 다른 많은 프로그래밍 언어와 일관되며(부록 A 참조), 설문에 참여한 다양한 Python 개발자들 사이에서 압도적으로 선호되었습니다.

명시적 분산

타입 매개변수가 불변, 공변 또는 반공변으로 의도되었는지 지정하는 구문을 추가하는 방식을 고려했습니다. Python의 typing.TypeVar메커니즘은 이를 요구합니다. Scala와 C#을 비롯한 일부 다른 언어도 개발자가 분산을 지정하도록 요구합니다. 분산은 일반적으로 추론할 수 있고, 대부분의 최신 프로그래밍 언어는 사용 방식에 따라 분산을 실제로 추론하기 때문에 이 아이디어는 기각했습니다. 분산은 많은 개발자가 이해하기 어려워하는 고급 주제이므로, 대부분의 Python 개발자가 이 개념을 이해해야 할 필요를 없애고자 합니다.

이름 맹글링

구현 옵션을 검토하면서 각 타입 매개변수에 컴파일러가 고유한 “맹글링된” 이름을 부여하는 “이름 맹글링” 방식을 고려했습니다. 이 맹글링된 이름은 해당 타입 매개변수가 연결된 제네릭 클래스, 함수 또는 타입 별칭의 정규화된 이름을 기반으로 하게 됩니다. 정규화된 이름은 반드시 고유하지 않으므로 맹글링된 이름을 다른 무작위 값에 기반해야 한다는 이유로 이 방식은 기각했습니다. 더욱이 이 방식은 따옴표로 묶인(순방향 참조된) 타입 어노테이션을 평가하는 데 사용되는 기법과 호환되지 않습니다.

부록 A: 타입 매개변수 구문 조사

많은 프로그래밍 언어에서 제네릭 타입을 지원합니다. 이 절에서는 다른 인기 프로그래밍 언어에서 사용하는 선택 사항을 개괄합니다. 이는 다른 언어에 익숙할수록 Python 개발자가 이 개념을 더 쉽게 이해할 수 있기 때문에 중요합니다. 여기에서는 Python 타입 시스템의 향후 확장을 고려할 때 유용할 수 있는 추가 세부 정보(예를 들어, 기본 타입 인자 지원)도 제공합니다.

C++

C++은 타입 매개변수를 선언할 때 templatetypename 키워드와 꺾쇠괄호를 함께 사용합니다. 특수화에도 꺾쇠괄호를 사용합니다.

C++20에서는 Python의 프로토콜처럼 동작할 수 있는 일반화된 제약 조건이라는 개념을 도입했습니다. 제약 조건 모음은 concept이라는 이름의 엔터티로 정의할 수 있습니다.

변성은 명시적으로 지정되지 않지만, 제약 조건으로 변성을 강제할 수 있습니다.

기본 타입 인자는 = 연산자를 사용하여 지정할 수 있습니다.

// Generic class
template <typename T>
class ClassA
{
    // Constraints are supported through compile-time assertions.
    static_assert(std::is_base_of<BaseClass, T>::value);

public:
    Container<T> t;
};

// Generic function with default type argument
template <typename S = int>
S func1(ClassA<S> a, S b) {};

// C++20 introduced a more generalized notion of "constraints"
// and "concepts", which are named constraints.

// A sample concept
template<typename T>
concept Hashable = requires(T a)
{
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

// Use of a concept in a template
template<Hashable T>
void func2(T value) {}

// Alternative use of concept
template<typename T> requires Hashable<T>
void func3(T value) {}

// Alternative use of concept
template<typename T>
void func3(T value) requires Hashable<T> {}

Java

Java는 타입 매개변수를 선언하고 특수화할 때 꺾쇠괄호를 사용합니다. 기본적으로 타입 매개변수는 불변입니다. extends 키워드는 상한을 지정하는 데 사용합니다. super 키워드는 반공변 바운드를 지정하는 데 사용합니다.

Java는 사용 지점 변성을 사용합니다. 컴파일러는 제네릭 타입의 사용 방식에 따라 액세스할 수 있는 메서드와 멤버를 제한합니다. 변성은 명시적으로 지정되지 않습니다.

Java에서는 기본 타입 인자를 지정할 방법을 제공하지 않습니다.

// Generic class
public class ClassA<T> {
    public Container<T> t;

    // Generic method
    public <S extends Number> void method1(S value) { }

    // Use site variance
    public void method1(ClassA<? super Integer> value) { }
}

C#

C#은 타입 매개변수를 선언하고 특수화할 때 꺾쇠괄호를 사용합니다. where 키워드와 콜론은 타입 매개변수의 바운드를 지정하는 데 사용합니다.

C#은 반공변성과 공변성에 각각 inout 키워드를 사용하는 선언 지점 변성을 지원합니다. 기본적으로 타입 매개변수는 불변입니다.

C#에서는 기본 타입 인자를 지정할 방법을 제공하지 않습니다.

// Generic class with bounds on type parameters
public class ClassA<S, T>
    where T : SomeClass1
    where S : SomeClass2
{
    // Generic method
    public void MyMethod<U>(U value) where U : SomeClass3 { }
}

// Contravariant and covariant type parameters
public class ClassB<in S, out T>
{
    public T MyMethod(S value) { }
}

TypeScript

TypeScript는 타입 매개변수를 선언하고 특수화할 때 꺾쇠괄호를 사용합니다. extends 키워드는 바운드를 지정하는 데 사용합니다. 이는 keyof와 같은 다른 타입 연산자와 함께 사용할 수 있습니다.

TypeScript는 선언 위치 변성을 사용합니다. 변성은 명시적으로 지정하는 것이 아니라 사용 방식에서 추론됩니다. TypeScript 4.7에서는 inout 키워드를 사용하여 변성을 지정하는 기능이 도입되었습니다. 이는 변성 추론에 비용이 많이 드는 매우 복잡한 타입을 처리하기 위해 추가되었습니다.

기본 타입 인자는 =연산자를 사용하여 지정할 수 있습니다.

TypeScript는 타입 별칭을 선언하는 type 키워드를 지원하며, 이 구문은 제네릭을 지원합니다.

// Generic interface
interface InterfaceA<S, T extends SomeInterface1> {
    val1: S;
    val2: T;

    method1<U extends SomeInterface2>(val: U): S
}

// Generic function
function func1<T, K extends keyof T>(ojb: T, key: K) { }

// Contravariant and covariant type parameters (TypeScript 4.7)
interface InterfaceB<in S, out T> { }

// Type parameter with default
interface InterfaceC<T = SomeInterface3> { }

// Generic type alias
type MyType<T extends SomeInterface4> = Array<T>

스칼라

스칼라에서는 타입 매개변수를 선언할 때 대괄호를 사용합니다. 대괄호는 특수화에도 사용됩니다. <:>: 연산자는 각각 상한과 하한을 지정하는 데 사용됩니다.

스칼라는 사용 위치 변성을 사용하지만 선언 위치 변성 지정도 허용합니다. 공변성과 반공변성에 각각 + 또는 - 접두 연산자를 사용합니다.

스칼라는 기본 타입 인자를 지정할 방법을 제공하지 않습니다.

또한 고차 종류 타입(타입 매개변수를 받는 타입 매개변수)을 지원합니다.

// Generic class; type parameter has upper bound
class ClassA[A <: SomeClass1]
{
    // Generic method; type parameter has lower bound
    def method1[B >: A](val: B) ...
}

// Use of an upper and lower bound with the same type parameter
class ClassB[A >: SomeClass1 <: SomeClass2] { }

// Contravariant and covariant type parameters
class ClassC[+A, -B] { }

// Higher-kinded type
trait Collection[T[_]]
{
    def method1[A](a: A): T[A]
    def method2[B](b: T[B]): B
}

// Generic type alias
type MyType[T <: Int] = Container[T]

스위프트

스위프트는 타입 매개변수를 선언하고 특수화할 때 꺾쇠괄호를 사용합니다. 타입 매개변수의 상한은 콜론을 사용하여 지정합니다.

스위프트는 제네릭 변성을 지원하지 않으며, 모든 타입 매개변수는 불변입니다.

스위프트는 기본 타입 인자를 지정할 방법을 제공하지 않습니다.

// Generic class
class ClassA<T> {
    // Generic method
    func method1<X>(val: T) -> X { }
}

// Type parameter with upper bound constraint
class ClassB<T: SomeClass1> {}

// Generic type alias
typealias MyType<A> = Container<A>

러스트

러스트는 타입 매개변수를 선언하고 특수화할 때 꺾쇠괄호를 사용합니다. 타입 매개변수의 상한은 콜론을 사용하여 지정합니다. 또는 where 절에서 다양한 제약 조건을 지정할 수 있습니다.

러스트에는 전통적인 객체 지향 상속이나 변성이 없습니다. 러스트의 서브타이핑은 매우 제한적이며 수명에 관한 변성 때문에만 발생합니다.

기본 타입 인자는 =연산자를 사용하여 지정할 수 있습니다.

// Generic class
struct StructA<T> { // T's lifetime is inferred as covariant
    x: T
}

fn f<'a>(
    mut short_lifetime: StructA<&'a i32>,
    mut long_lifetime: StructA<&'static i32>,
) {
    long_lifetime = short_lifetime;
    // error: StructA<&'a i32> is not a subtype of StructA<&'static i32>
    short_lifetime = long_lifetime;
    // valid: StructA<&'static i32> is a subtype of StructA<&'a i32>
}

// Type parameter with bound
struct StructB<T: SomeTrait> {}

// Type parameter with additional constraints
struct StructC<T>
where
    T: Iterator,
    T::Item: Copy
{}

// Generic function
fn func1<T>(val: &[T]) -> T { }

// Generic type alias
type MyType<T> = StructC<T>;

코틀린

코틀린은 타입 매개변수를 선언하고 특수화할 때 꺾쇠괄호를 사용합니다. 기본적으로 타입 매개변수는 불변입니다. 타입의 상한은 콜론을 사용하여 지정합니다. 또는 where 절에서 다양한 제약 조건을 지정할 수 있습니다.

Kotlin은 inout 키워드를 사용하여 타입 매개변수의 변성을 명시적으로 선언하는 선언 위치 변성을 지원합니다. 또한 사용할 수 있는 메서드와 멤버를 제한하는 사용 위치 변성도 지원합니다.

Kotlin은 기본 타입 인자를 지정할 방법을 제공하지 않습니다.

// Generic class
class ClassA<T>

// Type parameter with upper bound
class ClassB<T : SomeClass1>

// Contravariant and covariant type parameters
class ClassC<in S, out T>

// Generic function
fun <T> func1(): T {

    // Use site variance
    val covariantA: ClassA<out Number>
    val contravariantA: ClassA<in Number>
}

// Generic type alias
typealias TypeAliasFoo<T> = ClassA<T>

Julia

Julia는 타입 매개변수를 선언하고 특수화하기 위해 중괄호를 사용합니다. <: 연산자는 where 절 내에서 타입의 상한과 하한을 선언하는 데 사용할 수 있습니다.

# Generic struct; type parameter with upper and lower bounds
# Valid for T in (Int64, Signed, Integer, Real, Number)
struct Container{Int <: T <: Number}
    x::T
end

# Generic function
function func1(v::Container{T}) where T <: Real end

# Alternate forms of generic function
function func2(v::Container{T} where T <: Real) end
function func3(v::Container{<: Real}) end

# Tuple types are covariant
# Valid for func4((2//3, 3.5))
function func4(t::Tuple{Real,Real}) end

Dart

Dart는 타입 매개변수를 선언하고 특수화하기 위해 꺾쇠 괄호를 사용합니다. 타입의 상한은 extends 키워드를 사용하여 지정합니다. 기본적으로 타입 매개변수는 공변입니다.

Dart는 타입 매개변수의 변성을 in, outinout 키워드를 사용하여 명시적으로 선언하는 선언 위치 변성을 지원합니다. 사용 위치 변성은 지원하지 않습니다.

Dart는 기본 타입 인자를 지정할 방법을 제공하지 않습니다.

// Generic class
class ClassA<T> { }

// Type parameter with upper bound
class ClassB<T extends SomeClass1> { }

// Contravariant and covariant type parameters
class ClassC<in S, out T> { }

// Generic function
T func1<T>() { }

// Generic type alias
typedef TypeDefFoo<T> = ClassA<T>;

Go

Go는 타입 매개변수를 선언하고 특수화하기 위해 대괄호를 사용합니다. 타입의 상한은 매개변수 이름 뒤에 지정하며, 항상 지정해야 합니다. any 키워드는 바인딩되지 않은 타입 매개변수에 사용됩니다.

Go는 변성을 지원하지 않으며, 모든 타입 매개변수는 불변입니다.

Go는 기본 타입 인자를 지정할 방법을 제공하지 않습니다.

Go는 제네릭 타입 별칭을 지원하지 않습니다.

// Generic type without a bound
type TypeA[T any] struct {
    t T
}

// Type parameter with upper bound
type TypeB[T SomeType1] struct { }

// Generic function
func func1[T any]() { }

요약

선언 구문 상한 하한 기본값 변성 위치 변성
C++ template <> 해당 없음 해당 없음 = 해당 없음 해당 없음
Java <> extends 사용 super, extends
C# <> where 선언 in, out
TypeScript <> extends = 선언 추론됨, in, out
Scala [] T <: X T >: X 사용, 선언 +, -
Swift <> T: X 해당 없음 해당 없음
Rust <> T: X, where = 해당 없음 해당 없음
Kotlin <> T: X, where 사용, 선언 in, out
Julia {} T <: X X <: T 해당 없음 해당 없음
Dart <> 확장 선언 in, out, inout
Go [] T X 해당 없음 해당 없음
Python (제안) [] T: X 선언 추론됨

감사의 말

이 제안으로 이어진 논의를 시작해 주신 Sebastian Rittau, 타입 별칭 문을 위한 구문을 제안해 주신 Jukka Lehtosalo, 그리고 사양과 구현에 귀중한 피드백과 개선안을 제시해 주신 Jelle Zijlstra, Daniel Moisset, Guido van Rossum께 감사드립니다.