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, ParamSpecTypeVarTuple을 비롯한 타입 매개변수의 타입 기본값 개념을 소개합니다. 이들은 타입이 지정되지 않은 타입 매개변수의 기본값으로 작동합니다.

C++, TypeScript 및 Rust와 같은 일부 인기 언어에서는 기본 타입 인자를 지원합니다. 일부 일반적인 언어의 타입 매개변수 구문에 대한 조사는 PEP 695의 작성자가 수행했으며, 해당 조사는 Appendix 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 — 위의 예를 가져온 프로젝트입니다.
  • NumPyndarraydtype와 같은 타입의 기본값은 float64가 됩니다. 현재는 Unknown 또는 Any입니다.
  • TensorFlow — 이는 numpy.ndarray와 유사하게 Tensor에 사용할 수 있으며, 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

새로운 Python 3.12 제네릭 구문(PEP 695에서 도입됨)을 사용하면 이를 컴파일 시점에 강제할 수 있습니다.:

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 s와 동일한 구문을 사용하여 정의하지만, 단일 타입 대신 타입의 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 s와 동일한 구문을 사용하여 정의하지만, 단일 타입이나 다른 범위 내 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__

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

범위 규칙

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

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}

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

previous section에서 정의한 대로 첫 번째 매개변수가 스코프 내에 있을 때, 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]>

bounddefault 사용

bounddefault를 모두 전달하는 경우 defaultbound의 서브타입이어야 합니다. 그렇지 않으면 타입 검사기가 오류를 생성해야 합니다.

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]

서브타이핑

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

TypeVarTuples를 기본값으로 사용하기

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

  • Scoping Rules에서는 외부 스코프의 타입 매개변수를 사용할 수 없습니다.
  • 단일 객체의 타입 매개변수 목록에는 여러 TypeVarTuples가 나타날 수 없습니다. PEP 646에 명시된 바와 같습니다.

이러한 이유로 현재 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이 됩니다.

다음 변경 사항이 두 GenericAliases 모두에 필요합니다:

  • 서브스크립션에 필요한 기본값을 결정하는 로직.
  • 이상적으로는 서브스크립션(예: 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]

타입 매개변수의 bound와 마찬가지로, 기본값은 불필요한 따옴표 사용을 피할 수 있도록 동일한 스코프 규칙에 따라 지연 평가되어야 합니다.

이 기능은 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

컴파일러는 기본값이 없는 타입 매개변수가 기본값이 있는 타입 매개변수 뒤에 올 수 없도록 하고, 기본값이 있는 TypeVars가 TypeVarTuples 바로 뒤에 올 수 없도록 강제합니다.

거부된 대안

타입 매개변수 기본값을 type.__new__’s **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의 이전 버전에서는 기본값이 있는 TypeVarLikes를 함수 시그니처에서 사용할 수 있도록 허용했습니다. 이는 Function Defaults에 설명된 이유로 제거되었습니다. 타입 매개변수의 런타임 값을 가져오는 방법이 추가된다면, 앞으로 이를 추가할 수 있기를 바랍니다.

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

이는 추가되는 복잡성을 감수할 만큼 가치가 있는 틈새 기능은 아닌 것으로 판단되었습니다. 이 기능이 필요한 사례가 발생하면 향후 PEP에서 추가할 수 있습니다.

감사의 말

PEP에 대한 의견을 주신 다음 분들께 감사드립니다:

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