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 statement 및 Annotation 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는 제네릭 클래스, 함수, 타입 별칭 안에서 타입 매개변수를 지정하는 개선된 문법을 규정합니다. 또한 타입 별칭을 선언하는 새로운 문(statement)을 도입합니다.

동기

PEP 484는 언어에 타입 변수를 도입했습니다. PEP 612는 이 개념을 바탕으로 매개변수 명세(parameter specification)를 도입했고, PEP 646은 가변 길이 타입 변수를 추가했습니다.

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

Python 정적 타이핑 커뮤니티 안에서는 제네릭 타입을 지원하는 다른 현대 프로그래밍 언어와 비슷한 정식 문법을 제공할 때가 되었다는 공감대가 형성되어 있습니다.

타입이 지정된 인기 Python 라이브러리 25개를 분석한 결과, 타입 변수(특히 typing.TypeVar 심볼)가 모듈의 14%에서 사용되고 있었습니다.

혼란의 원인

타입 변수는 널리 사용되고 있지만, 코드 안에서 타입 변수를 지정하는 방식은 많은 Python 개발자에게 혼란을 줍니다. 이러한 혼란에는 몇 가지 요인이 있습니다.

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

제네릭 타입 별칭은 자주 잘못 사용됩니다. 타입 별칭을 사용할 때 타입 인자를 반드시 제공해야 한다는 점이 개발자에게 분명하지 않기 때문입니다. 그 결과 타입 인자가 암묵적으로 Any가 되는데, 이는 의도한 결과인 경우가 거의 없습니다. 이 PEP는 제네릭 타입 별칭 선언을 명확하게 해 주는 새로운 문법을 추가할 것을 제안합니다.

PEP 483과 PEP 484는 제네릭 클래스 안에서 사용되는 타입 변수에 대해 “변성(variance)”이라는 개념을 도입했습니다. 타입 변수는 불변(invariant), 공변(covariant), 반공변(contravariant)일 수 있습니다. 변성은 타입 이론의 고급 개념이라 대부분의 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 모듈에서 TypeVar와 Generic 심볼을 임포트해야 합니다. 지난 몇 번의 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에서 논의하는 것처럼 이 속성들은 지연 평가됩니다.

제네릭 타입 별칭

타입 별칭을 선언하기 위한 새로운 문을 도입할 것을 제안합니다. class 및 def 문과 마찬가지로, 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를 폐지(deprecate)할 것을 제안합니다. 새 문법을 사용하면 이것이 전혀 필요하지 않게 됩니다.

런타임 타입 별칭 클래스

런타임에 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의 상한(bound) 및 제약(constraint) 안에서도 사용할 수 없습니다. 이와 비슷하게, 이러한 문맥에서는 yield, yield from, await 표현식도 허용되지 않습니다.

새 렉시컬 스코프 안에서 평가되는 표현식은 정의된 타입 매개변수 외의 심볼을 그 스코프에 추가해서는 안 되고, 자신을 둘러싼 함수가 제너레이터나 코루틴인지 여부에도 영향을 주어서는 안 되므로 이 제약이 필요합니다.

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, ParamSpec, TypeVarTuple 인스턴스가 들어 있습니다.

새 문법으로 선언한 타입 매개변수는 globals()나 locals()가 반환하는 딕셔너리에 나타나지 않습니다.

가변성 추론

이 PEP를 통해 타입 매개변수에 가변성을 지정할 필요가 없어집니다. 그 대신 타입 검사기가 클래스 안에서 타입 매개변수가 사용되는 방식을 바탕으로 가변성을 추론합니다. 타입 매개변수는 사용 방식에 따라 불변, 공변, 반공변 중 하나로 추론됩니다.

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

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

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

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

2. 타입 매개변수가 전통적인 TypeVar 선언에서 비롯되었고 infer_variance로 지정되지 않은 경우(아래 참조), 그 변성은 TypeVar 생성자 호출에 의해 지정됩니다. 더 이상의 추론은 필요하지 않습니다.

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

4. 일반적인 타입 호환성 규칙을 사용하여 lower를 upper에 할당할 수 있는지 판단합니다. 할당할 수 있다면 대상 타입 매개변수는 공변입니다. 할당할 수 없다면 upper를 lower에 할당할 수 있는지 판단합니다. 할당할 수 있다면 대상 타입 매개변수는 반공변입니다. 두 조합 모두 대입할 수 없다면 대상 타입 매개변수는 불변입니다.

다음은 예시입니다.

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에 정의된 일반적인 타입 호환성 규칙에 따르면 upper는 lower에 할당할 수 없습니다. 마찬가지로 lower도 upper에 대입할 수 없으므로 T1은 불변이라고 결론 내립니다.

T2의 변성을 결정하기 위해 ClassA를 다음과 같이 특수화합니다.

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

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

T3의 변성을 결정하기 위해 ClassA를 다음과 같이 특수화합니다.

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

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

TypeVar의 자동 변성

기존 TypeVar 클래스 생성자는 covariant와 contravariant라는 이름의 키워드 매개변수를 받습니다. 두 매개변수가 모두 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, TypeVarTuple, ParamSpec을 할당하는 기존 메커니즘은 하위 호환성을 위해 유지됩니다. 그러나 이러한 “전통적인” 타입 변수를 새 문법으로 할당한 타입 매개변수와 함께 사용해서는 안 됩니다. 타입 검사기는 이러한 조합을 오류로 표시해야 합니다. 타입 매개변수의 순서가 모호해지기 때문에 이러한 제한이 필요합니다.

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

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을 도입합니다. 이 PEP는 다음과 같이 문법을 수정합니다:

  1. class 문과 def 문에 선택적 타입 매개변수 절을 추가합니다.
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, AsyncFunctionDef, ClassDef를 수정하여, 해당 함수나 클래스에 연결된 타입 매개변수의 목록을 담는 typeparams라는 선택적 속성을 추가로 포함하도록 합니다.

지연 평가

이 PEP는 정적 타입을 나타내는 표현식이 나타날 수 있는 세 가지 새로운 문맥, 즉 TypeVar 바운드, TypeVar 제약 조건, 타입 별칭의 값을 도입합니다. 이러한 표현식은 아직 정의되지 않은 이름에 대한 참조를 포함할 수 있습니다. 예를 들어 타입 별칭은 재귀적이거나 심지어 상호 재귀적일 수 있으며, 타입 변수의 바운드는 현재 클래스를 다시 참조할 수 있습니다. 이러한 표현식을 즉시 평가한다면, 사용자는 런타임 오류를 막기 위해 이러한 표현식을 따옴표로 감싸야 합니다. PEP 563과 PEP 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): 주어진 이름과 지연 평가되는 상한(bound)을 가진 새 typing.TypeVar 객체를 생성합니다. 상한은 __bound__ 속성에 접근하기 전까지 평가되지 않습니다.
  • __make_typevar_with_constraints(*, name, evaluate_constraints): 주어진 이름과 지연 평가되는 제약 조건(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()

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

라이브러리 변경 사항

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

참조 구현

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

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

기각된 아이디어

접두 절

def와 class 문 앞에 오는 타입 매개변수를 지정하기 위한 다양한 구문 방식을 검토했습니다. 검토한 변형 중 하나는 다음과 같이 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++는 template 및 typename 키워드와 함께 꺾쇠괄호를 사용하여 타입 매개변수를 선언합니다. 특수화에도 꺾쇠괄호를 사용합니다.

C++20은 일반화된 제약 조건이라는 개념을 도입했으며, 이는 Python의 프로토콜과 비슷한 역할을 할 수 있습니다. 제약 조건의 모음은 concept라는 이름이 붙은 개체로 정의할 수 있습니다.

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

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

// 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는 사용 지점 변성(use-site variance)을 사용합니다. 컴파일러는 제네릭 타입이 어떻게 사용되는지에 따라 접근할 수 있는 메서드와 멤버를 제한합니다. 변성은 명시적으로 지정하지 않습니다.

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#은 선언 지점 변성(declaration-site variance)을 사용하며, 반공변성과 공변성에 각각 in과 out 키워드를 사용합니다. 기본적으로 타입 매개변수는 불변입니다.

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는 선언 위치 변성(declaration-site variance)을 사용합니다. 변성은 명시적으로 지정하지 않고 사용 방식에서 추론됩니다. TypeScript 4.7에서는 in과 out 키워드로 변성을 지정하는 기능이 도입되었습니다. 이 기능은 변성 추론 비용이 큰 매우 복잡한 타입을 처리하기 위해 추가되었습니다.

기본 타입 인자는 = 연산자로 지정할 수 있습니다.

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>

Scala

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

Scala는 사용 위치 가변성(use-site variance)을 사용하지만, 선언 위치 가변성(declaration-site variance) 명세도 허용합니다. 공변성과 반공변성에는 각각 +와 - 접두 연산자를 사용합니다.

Scala에는 기본 타입 인자를 지정하는 방법이 없습니다.

고계 타입(higher-kinded type, 타입 매개변수를 받는 타입 매개변수)은 지원합니다.

// 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]

Swift

Swift는 타입 매개변수 선언과 특수화에 꺾쇠괄호를 사용합니다. 타입 매개변수의 상한은 콜론으로 지정합니다.

Swift는 제네릭 변성(variance)을 지원하지 않으며, 모든 타입 매개변수는 불변입니다.

Swift에는 기본 타입 인자를 지정하는 방법이 없습니다.

// 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>

Rust

Rust는 타입 매개변수 선언과 특수화에 꺾쇠괄호를 사용합니다. 타입 매개변수의 상한은 콜론으로 지정합니다. 또는 where 절로 다양한 제약 조건을 지정할 수도 있습니다.

Rust에는 전통적인 객체 지향 상속이나 변성이 없습니다. Rust의 서브타이핑은 매우 제한적이며, 라이프타임에 대한 변성으로 인해서만 발생합니다.

기본 타입 인자는 = 연산자로 지정할 수 있습니다.

// 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>;

Kotlin

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

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

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, out, inout 키워드를 사용하여 타입 매개변수의 변성을 명시적으로 선언하는 선언 위치 변성을 지원합니다. 사용 위치 변성은 지원하지 않습니다.

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# <> 여기서 선언 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 <> extends 선언 in, out, inout
Go [] T X 해당 없음 해당 없음
Python (제안) [] T: X 선언 추론

감사의 글

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