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

Python 개선 제안 한국어 번역

PEP 696 – 타입 매개변수의 타입 기본값

Author:
James Hilton-Balfe <gobot1234yt at gmail.com>
Sponsor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
14-Jul-2022
Python-Version:
3.13
Post-History:
22-Mar-2022, 08-Jan-2023
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 Defaults for Type Parameters and Type parameter lists 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는 TypeVar, ParamSpec, TypeVarTuple을 포함한 타입 매개변수에 타입 기본값이라는 개념을 도입합니다. 타입 기본값은 타입이 지정되지 않은 타입 매개변수의 기본값 역할을 합니다.

기본 타입 인자는 C++, TypeScript, Rust 같은 몇몇 인기 있는 언어에서 지원됩니다. PEP 695의 저자가 몇몇 흔히 쓰이는 언어의 타입 매개변수 문법을 조사하였으며, 그 결과는 해당 PEP의 부록 A에서 볼 수 있습니다.

동기

T = TypeVar("T", default=int)  # This means that if no type is specified T = int

@dataclass
class Box(Generic[T]):
    value: T | None = None

reveal_type(Box())                      # type is Box[int]
reveal_type(Box(value="Hello World!"))  # type is Box[str]

이 문제가 자주 제기되는 곳 중 하나는 Generator입니다. 스텁 정의를 다음과 같은 형태로 바꿀 것을 제안합니다:

YieldT = TypeVar("YieldT")
SendT = TypeVar("SendT", default=None)
ReturnT = TypeVar("ReturnT", default=None)

class Generator(Generic[YieldT, SendT, ReturnT]): ...

Generator[int] == Generator[int, None] == Generator[int, None, None]

이는 흔히 하나의 타입에 대해 쓰이는 Generic에도 유용합니다.

class Bot: ...

BotT = TypeVar("BotT", bound=Bot, default=Bot)

class Context(Generic[BotT]):
    bot: BotT

class MyBot(Bot): ...

reveal_type(Context().bot)         # type is Bot  # notice this is not Any which is what it would be currently
reveal_type(Context[MyBot]().bot)  # type is MyBot

이는 타이핑을 명시적으로 사용하는 사람들의 타이핑 경험을 개선할 뿐만 아니라, 개발 속도를 높이기 위해 자동 완성에 의존하는, 타이핑을 사용하지 않는 사용자에게도 도움이 됩니다.

이 디자인 패턴은 다음과 같은 프로젝트에서 흔히 볼 수 있습니다:

  • discord.py — 위 예제의 출처입니다.
  • NumPy — ndarray의 dtype 같은 타입의 기본값은 float64가 됩니다. 현재는 Unknown 또는 Any입니다.
  • TensorFlow — Tensor에 numpy.ndarray와 비슷한 방식으로 사용할 수 있으며, Layer의 정의를 단순화하는 데 유용합니다.

명세

기본값 순서와 서브스크립션 규칙

기본값의 순서는 표준 함수 매개변수 규칙을 따라야 합니다. 따라서 default가 없는 타입 매개변수는 default 값이 있는 타입 매개변수 뒤에 올 수 없습니다. 이렇게 하면 이상적으로는 typing._GenericAlias/types.GenericAlias에서 TypeError를 발생시켜야 하며, 타입 검사기는 이를 오류로 표시하는 플래그를 지정해야 합니다.

DefaultStrT = TypeVar("DefaultStrT", default=str)
DefaultIntT = TypeVar("DefaultIntT", default=int)
DefaultBoolT = TypeVar("DefaultBoolT", default=bool)
T = TypeVar("T")
T2 = TypeVar("T2")

class NonDefaultFollowsDefault(Generic[DefaultStrT, T]): ...  # Invalid: non-default TypeVars cannot follow ones with defaults


class NoNonDefaults(Generic[DefaultStrT, DefaultIntT]): ...

(
    NoNoneDefaults ==
    NoNoneDefaults[str] ==
    NoNoneDefaults[str, int]
)  # All valid


class OneDefault(Generic[T, DefaultBoolT]): ...

OneDefault[float] == OneDefault[float, bool]  # Valid
reveal_type(OneDefault)          # type is type[OneDefault[T, DefaultBoolT = bool]]
reveal_type(OneDefault[float]()) # type is OneDefault[float, bool]


class AllTheDefaults(Generic[T1, T2, DefaultStrT, DefaultIntT, DefaultBoolT]): ...

reveal_type(AllTheDefaults)                  # type is type[AllTheDefaults[T1, T2, DefaultStrT = str, DefaultIntT = int, DefaultBoolT = bool]]
reveal_type(AllTheDefaults[int, complex]())  # type is AllTheDefaults[int, complex, str, int, bool]
AllTheDefaults[int]  # Invalid: expected 2 arguments to AllTheDefaults
(
    AllTheDefaults[int, complex] ==
    AllTheDefaults[int, complex, str] ==
    AllTheDefaults[int, complex, str, int] ==
    AllTheDefaults[int, complex, str, int, bool]
)  # All valid

(PEP 695에서 도입된) 제네릭을 위한 Python 3.12의 새 문법에서는 이를 컴파일 시점에 강제할 수 있습니다:

type Alias[DefaultT = int, T] = tuple[DefaultT, T]  # SyntaxError: non-default TypeVars cannot follow ones with defaults

def generic_func[DefaultT = int, T](x: DefaultT, y: T) -> None: ...  # SyntaxError: non-default TypeVars cannot follow ones with defaults

class GenericClass[DefaultT = int, T]: ...  # SyntaxError: non-default TypeVars cannot follow ones with defaults

ParamSpec 기본값

ParamSpec 기본값은 TypeVar와 같은 문법으로 정의하지만, 타입의 list, 생략 부호 리터럴 “...”, 또는 스코프 안에 있는 다른 ParamSpec을 사용합니다(Scoping Rules 참고).

DefaultP = ParamSpec("DefaultP", default=[str, int])

class Foo(Generic[DefaultP]): ...

reveal_type(Foo)                  # type is type[Foo[DefaultP = [str, int]]]
reveal_type(Foo())                # type is Foo[[str, int]]
reveal_type(Foo[[bool, bool]]())  # type is Foo[[bool, bool]]

TypeVarTuple 기본값

TypeVarTuple 기본값은 TypeVar와 같은 문법으로 정의하지만, 단일 타입 대신 언패킹된 타입 튜플이나 스코프 안에 있는 다른 TypeVarTuple을 사용합니다(Scoping Rules 참고).

DefaultTs = TypeVarTuple("DefaultTs", default=Unpack[tuple[str, int]])

class Foo(Generic[*DefaultTs]): ...

reveal_type(Foo)               # type is type[Foo[DefaultTs = *tuple[str, int]]]
reveal_type(Foo())             # type is Foo[str, int]
reveal_type(Foo[int, bool]())  # type is Foo[int, bool]

다른 타입 매개변수를 default로 사용하기

이를 통해 제네릭의 타입 매개변수가 누락되었지만 다른 타입 매개변수는 지정된 경우에 값을 재사용할 수 있습니다.

다른 타입 매개변수를 기본값으로 사용하려면 default와 타입 매개변수가 같은 타입이어야 합니다(TypeVar의 기본값은 TypeVar여야 하는 식입니다).

builtins.slice에 이를 사용할 수 있습니다. 이 경우 start 매개변수는 int를, stop은 start의 타입을, step은 int | None을 기본값으로 가져야 합니다.

StartT = TypeVar("StartT", default=int)
StopT = TypeVar("StopT", default=StartT)
StepT = TypeVar("StepT", default=int | None)

class slice(Generic[StartT, StopT, StepT]): ...

reveal_type(slice)  # type is type[slice[StartT = int, StopT = StartT, StepT = int | None]]
reveal_type(slice())                        # type is slice[int, int, int | None]
reveal_type(slice[str]())                   # type is slice[str, str, int | None]
reveal_type(slice[str, bool, timedelta]())  # type is slice[str, bool, timedelta]

T2 = TypeVar("T2", default=DefaultStrT)

class Foo(Generic[DefaultStrT, T2]):
    def __init__(self, a: DefaultStrT, b: T2) -> None: ...

reveal_type(Foo(1, ""))  # type is Foo[int, str]
Foo[int](1, "")          # Invalid: Foo[int, str] cannot be assigned to self: Foo[int, int] in Foo.__init__
Foo[int]("", 1)          # Invalid: Foo[str, int] cannot be assigned to self: Foo[int, int] in Foo.__init__

타입 매개변수를 다른 타입 매개변수의 기본값으로 사용할 때는 다음 규칙이 적용됩니다. 여기서 T1은 T2의 기본값입니다.

스코프 규칙

제네릭의 매개변수 목록에서 T1은 T2보다 먼저 사용되어야 합니다.

T2 = TypeVar("T2", default=T1)

class Foo(Generic[T1, T2]): ...   # Valid
class Foo(Generic[T1]):
    class Bar(Generic[T2]): ...   # Valid

StartT = TypeVar("StartT", default="StopT")  # Swapped defaults around from previous example
StopT = TypeVar("StopT", default=int)
class slice(Generic[StartT, StopT, StepT]): ...
                  # ^^^^^^ Invalid: ordering does not allow StopT to be bound

바깥 스코프의 타입 매개변수를 기본값으로 사용하는 것은 지원되지 않습니다.

바운드 규칙

T1의 바운드는 T2의 바운드의 서브타입이어야 합니다.

T1 = TypeVar("T1", bound=int)
TypeVar("Ok", default=T1, bound=float)     # Valid
TypeVar("AlsoOk", default=T1, bound=int)   # Valid
TypeVar("Invalid", default=T1, bound=str)  # Invalid: int is not a subtype of str

제약 규칙

T2의 제약은 T1의 제약의 상위 집합이어야 합니다.

T1 = TypeVar("T1", bound=int)
TypeVar("Invalid", float, str, default=T1)         # Invalid: upper bound int is incompatible with constraints float or str

T1 = TypeVar("T1", int, str)
TypeVar("AlsoOk", int, str, bool, default=T1)      # Valid
TypeVar("AlsoInvalid", bool, complex, default=T1)  # Invalid: {bool, complex} is not a superset of {int, str}

제네릭의 매개변수로서의 타입 매개변수

이전 절에서 정한 대로 첫 번째 매개변수가 스코프 안에 있으면, default 안에서 타입 매개변수를 제네릭의 매개변수로 사용할 수 있습니다.

T = TypeVar("T")
ListDefaultT = TypeVar("ListDefaultT", default=list[T])

class Bar(Generic[T, ListDefaultT]):
    def __init__(self, x: T, y: ListDefaultT): ...

reveal_type(Bar)                    # type is type[Bar[T, ListDefaultT = list[T]]]
reveal_type(Bar[int])               # type is type[Bar[int, list[int]]]
reveal_type(Bar[int]())             # type is Bar[int, list[int]]
reveal_type(Bar[int, list[str]]())  # type is Bar[int, list[str]]
reveal_type(Bar[int, str]())        # type is Bar[int, str]

특수화 규칙

현재 타입 매개변수에는 추가로 첨자를 붙일 수 없습니다. Higher Kinded TypeVars가 구현되면 이 부분은 바뀔 수 있습니다.

Generic TypeAlias

Generic TypeAlias에는 일반적인 첨자 규칙에 따라 추가로 첨자를 붙일 수 있어야 합니다. 타입 매개변수에 재정의되지 않은 기본값이 있으면, 그 기본값이 TypeAlias에 대입된 것처럼 취급해야 합니다. 하지만 이후에 더 특수화할 수 있습니다.

class SomethingWithNoDefaults(Generic[T, T2]): ...

MyAlias: TypeAlias = SomethingWithNoDefaults[int, DefaultStrT]  # Valid
reveal_type(MyAlias)          # type is type[SomethingWithNoDefaults[int, DefaultStrT]]
reveal_type(MyAlias[bool]())  # type is SomethingWithNoDefaults[int, bool]

MyAlias[bool, int]  # Invalid: too many arguments passed to MyAlias

서브클래싱

기본값이 있는 타입 매개변수를 가진 Generic의 서브클래스는 Generic TypeAlias와 비슷하게 동작합니다. 즉, 서브클래스에는 일반적인 첨자 규칙에 따라 추가로 첨자를 붙일 수 있고, 재정의되지 않은 기본값은 대입되어야 하며, 그런 기본값을 가진 타입 매개변수는 이후에 더 특수화할 수 있습니다.

class SubclassMe(Generic[T, DefaultStrT]):
    x: DefaultStrT

class Bar(SubclassMe[int, DefaultStrT]): ...
reveal_type(Bar)          # type is type[Bar[DefaultStrT = str]]
reveal_type(Bar())        # type is Bar[str]
reveal_type(Bar[bool]())  # type is Bar[bool]

class Foo(SubclassMe[float]): ...

reveal_type(Foo().x)  # type is str

Foo[str]  # Invalid: Foo cannot be further subscripted

class Baz(Generic[DefaultIntT, DefaultStrT]): ...

class Spam(Baz): ...
reveal_type(Spam())  # type is <subclass of Baz[int, str]>

bound와 default를 함께 사용하기

bound와 default가 모두 전달되면 default는 bound의 서브타입이어야 합니다. 그렇지 않으면 타입 검사기가 오류를 발생시켜야 합니다.

TypeVar("Ok", bound=float, default=int)     # Valid
TypeVar("Invalid", bound=str, default=int)  # Invalid: the bound and default are incompatible

제약 조건

제약이 있는 TypeVar의 경우, 기본값은 제약 중 하나여야 합니다. 기본값이 제약 중 하나의 서브타입이더라도 타입 검사기는 오류를 발생시켜야 합니다.

TypeVar("Ok", float, str, default=float)     # Valid
TypeVar("Invalid", float, str, default=int)  # Invalid: expected one of float or str got int

함수 기본값

제네릭 함수에서 타입 매개변수를 어떤 타입으로도 풀 수 없을 때, 타입 검사기는 타입 매개변수의 기본값을 사용할 수 있습니다. 타입 매개변수가 풀리지 않을 수 있는 모든 코드 경로에서 default가 반환되도록 보장하는 것은 구현하기가 너무 어려울 수 있으므로, 이 용법의 의미는 명시하지 않은 채로 둡니다. 타입 검사기는 이 경우를 허용하지 않을 수도 있고, 지원을 시험적으로 구현해 볼 수도 있습니다.

T = TypeVar('T', default=int)
def func(x: int | set[T]) -> T: ...
reveal_type(func(0))  # a type checker may reveal T's default of int here

TypeVarTuple 뒤에 오는 기본값

TypeVarTuple 바로 뒤에 오는 TypeVar는 기본값을 가질 수 없습니다. 타입 인자가 TypeVarTuple에 바인딩되어야 하는지, 기본값이 있는 TypeVar에 바인딩되어야 하는지 모호해지기 때문입니다.

Ts = TypeVarTuple("Ts")
T = TypeVar("T", default=bool)

class Foo(Generic[Ts, T]): ...  # Type checker error

# Could be reasonably interpreted as either Ts = (int, str, float), T = bool
# or Ts = (int, str), T = float
Foo[int, str, float]

Python 3.12의 내장 제네릭 문법에서는 이 경우 SyntaxError가 발생해야 합니다.

하지만 기본값이 있는 TypeVarTuple 뒤에 기본값이 있는 ParamSpec이 오는 것은 허용됩니다. ParamSpec에 대한 타입 인자와 TypeVarTuple에 대한 타입 인자 사이에 모호함이 생길 수 없기 때문입니다.

Ts = TypeVarTuple("Ts")
P = ParamSpec("P", default=[float, bool])

class Foo(Generic[Ts, P]): ...  # Valid

Foo[int, str]  # Ts = (int, str), P = [float, bool]
Foo[int, str, [bytes]]  # Ts = (int, str), P = [bytes]

서브타이핑

타입 매개변수 기본값은 제네릭 클래스의 서브타이핑 규칙에 영향을 주지 않습니다. 특히 어떤 클래스가 제네릭 프로토콜과 호환되는지 판단할 때는 기본값을 무시해도 됩니다.

기본값으로서의 TypeVarTuple

다음과 같은 이유로 TypeVarTuple을 기본값으로 사용하는 것은 지원되지 않습니다.

  • Scoping Rules는 외부 스코프의 타입 매개변수 사용을 허용하지 않습니다.
  • PEP 646에 명시된 대로, 하나의 객체에 대한 타입 매개변수 목록에는 여러 개의 TypeVarTuple이 나타날 수 없습니다.

이러한 이유로, 현재로서는 TypeVarTuple을 다른 TypeVarTuple의 기본값으로 사용할 수 있는 유효한 위치가 존재하지 않습니다.

바인딩 규칙

타입 매개변수 기본값은 속성 접근(호출과 서브스크립트 포함)에 의해 바인딩되어야 합니다.

class Foo[T = int]:
    def meth(self) -> Self:
        return self

reveal_type(Foo.meth)  # type is (self: Foo[int]) -> Foo[int]

구현

런타임에서는 typing 모듈에 다음과 같은 변경이 필요합니다.

  • TypeVar, ParamSpec, TypeVarTuple 클래스는 default에 전달된 타입을 노출해야 합니다. 이 타입은 __default__ 속성으로 제공되며, 인자가 전달되지 않으면 None이고 default=None이면 NoneType입니다.

두 GenericAlias 모두에 다음과 같은 변경이 필요합니다.

  • 서브스크립션에 필요한 기본값을 결정하는 로직.
  • 이상적으로는, (Generic[T, DefaultT]와 같은) 서브스크립션이 유효한지 판단하는 로직.

기본값을 허용하도록 타입 매개변수 목록의 문법을 갱신해야 합니다. 아래를 참조하십시오.

런타임 변경 사항의 참조 구현은 https://github.com/Gobot1234/cpython/tree/pep-696 에서 찾을 수 있습니다.

타입 검사기의 참조 구현은 https://github.com/Gobot1234/mypy/tree/TypeVar-defaults 에서 찾을 수 있습니다.

Pyright는 현재 이 기능을 지원합니다.

문법 변경

PEP 695에서 추가된 문법을 확장하여, 다음과 같이 대괄호 안에서 “=” 연산자를 사용해 타입 매개변수의 기본값을 지정하는 방법을 도입할 것입니다.

# TypeVars
class Foo[T = str]: ...

# ParamSpecs
class Baz[**P = [int, str]]: ...

# TypeVarTuples
class Qux[*Ts = *tuple[int, bool]]: ...

# TypeAliases
type Foo[T, U = str] = Bar[T, U]
type Baz[**P = [int, str]] = Spam[**P]
type Qux[*Ts = *tuple[str]] = Ham[*Ts]
type Rab[U, T = str] = Bar[T, U]

타입 매개변수의 바운드와 마찬가지로, 기본값은 지연 평가되어야 하며, 기본값을 불필요하게 따옴표로 감싸지 않아도 되도록 동일한 스코프 규칙을 따라야 합니다.

이 기능은 PEP 695의 초기 초안에 포함되어 있었지만, 범위가 지나치게 커져서 제거되었습니다.

문법에는 다음과 같은 변경이 이루어질 것입니다.:

type_param:
    | a=NAME b=[type_param_bound] d=[type_param_default]
    | a=NAME c=[type_param_constraint] d=[type_param_default]
    | '*' a=NAME d=[type_param_default]
    | '**' a=NAME d=[type_param_default]

type_param_default:
    | '=' e=expression
    | '=' e=starred_expression

컴파일러는 기본값이 없는 타입 매개변수가 기본값이 있는 타입 매개변수 뒤에 올 수 없으며, 기본값이 있는 TypeVar가 TypeVarTuple 바로 뒤에 올 수 없도록 강제할 것입니다.

기각된 대안

타입 매개변수 기본값을 type.__new__의 **kwargs로 전달하도록 허용하기

T = TypeVar("T")

@dataclass
class Box(Generic[T], T=int):
    value: T | None = None

이 방식은 훨씬 읽기 쉽고 TypeVar 단항 문법과 비슷한 근거를 따르지만, T가 이미 메타클래스/슈퍼클래스에 전달되고 있거나 런타임에 Generic의 서브클래스가 아닌 클래스를 지원하고 있을 수 있으므로 하위 호환되지 않습니다.

이상적으로는, PEP 637이 기각되지 않았다면 다음 코드가 허용되었을 것입니다.:

T = TypeVar("T")

@dataclass
class Box(Generic[T = int]):
    value: T | None = None

기본값이 없는 매개변수가 기본값이 있는 매개변수 뒤에 오도록 허용하기

YieldT = TypeVar("YieldT", default=Any)
SendT = TypeVar("SendT", default=Any)
ReturnT = TypeVar("ReturnT")

class Coroutine(Generic[YieldT, SendT, ReturnT]): ...

Coroutine[int] == Coroutine[Any, Any, int]

기본값이 없는 타입 매개변수가 기본값이 있는 타입 매개변수 뒤에 올 수 있도록 허용하면, 가장 많이 쓰이는 타입 인자가 마지막(반환 타입)에 있는 Coroutine 같은 타입을 함수에서 반환할 때 생기는 문제가 줄어들 것입니다. 위의 두 형태만 유효하다고 하더라도, 기본값이 없는 타입 매개변수가 기본값이 있는 타입 매개변수 뒤에 오도록 허용하면 너무 혼란스럽고 모호해질 수 있습니다. 또한 지금 인자 순서를 바꾸면 수많은 코드베이스가 깨질 것입니다. 이 문제는 대부분의 경우 TypeAlias를 사용해서도 해결할 수 있습니다.

Coro: TypeAlias = Coroutine[Any, Any, T]
Coro[int] == Coroutine[Any, Any, int]

default를 암묵적으로 bound로 설정하기

이 PEP의 이전 버전에서는 default에 값을 전달하지 않으면 default가 암묵적으로 bound로 설정되었습니다. 이 방식은 편리하기는 하지만, 기본값이 없는 타입 매개변수가 기본값이 있는 타입 매개변수 뒤에 올 수 있었습니다. 다음 예를 살펴보십시오:

T = TypeVar("T", bound=int)  # default is implicitly int
U = TypeVar("U")

class Foo(Generic[T, U]):
    ...

# would expand to

T = TypeVar("T", bound=int, default=int)
U = TypeVar("U")

class Foo(Generic[T, U]):
    ...

또한 이 방식은 Any가 암묵적 기본값이라는 점에 의존하던 일부 코드를 깨뜨리는 변경이 되었을 것입니다.

기본값이 있는 타입 매개변수를 함수 시그니처에서 사용할 수 있도록 허용하기

이 PEP의 이전 버전에서는 기본값이 있는 TypeVarLike를 함수 시그니처에서 사용할 수 있도록 허용했습니다. 이 기능은 Function Defaults에서 설명한 이유로 제거되었습니다. 앞으로 타입 매개변수의 런타임 값을 얻는 방법이 추가된다면 이 기능도 다시 추가될 수 있기를 바랍니다.

default에서 외부 스코프의 타입 매개변수를 사용할 수 있도록 허용하기

이 기능은 활용 범위가 너무 좁아서 늘어나는 복잡성을 감수할 만한 가치가 없다고 판단되었습니다. 이 기능이 필요한 경우가 생긴다면 향후 PEP에서 추가할 수 있습니다.

감사의 글

이 PEP에 대해 피드백을 주신 다음 분들께 감사드립니다:

Eric Traut, Jelle Zijlstra, Joshua Butt, Danny Yamamoto, Kaylynn Morgan, Jakub Kuczys