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

Python 개선 제안 한국어 번역

PEP 586 – 리터럴 타입

Author:
Michael Lee <michael.lee.0x2a at gmail.com>, Ivan Levkivskyi <levkivskyi at gmail.com>, Jukka Lehtosalo <jukka.lehtosalo at iki.fi>
BDFL-Delegate:
Guido van Rossum <guido at python.org>
Discussions-To:
Typing-SIG list
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
14-Mar-2019
Python-Version:
3.8
Post-History:
14-Mar-2019
Resolution:
Typing-SIG message

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document: see Literals and typing.Literal 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 생태계에 리터럴 타입을 추가할 것을 제안합니다. 리터럴 타입은 어떤 표현식이 말 그대로 특정 값을 가진다는 것을 나타냅니다. 예를 들어, 다음 함수는 말 그대로 값이 “4”인 표현식만 허용합니다.:

from typing import Literal

def accepts_only_four(x: Literal[4]) -> None:
    pass

accepts_only_four(4)   # OK
accepts_only_four(19)  # Rejected

동기 및 근거

Python에는 제공된 인자의 값에 따라 서로 다른 타입을 반환하는 API가 많습니다. 예를 들면 다음과 같습니다.

  • open(filename, mode)는 두 번째 인자가 r이나 rb와 같은 것인지에 따라 IO[bytes] 또는 IO[Text]를 반환합니다.
  • subprocess.check_output(...)universal_newlines 키워드 인자가 True로 설정되었는지 여부에 따라 바이트 또는 텍스트를 반환합니다.

이러한 패턴은 널리 사용되는 많은 서드파티 라이브러리에서도 상당히 일반적입니다. 예를 들어, pandas와 numpy에서 각각 가져온 두 가지 예만 살펴보겠습니다.

  • pandas.concat(...)axis 인자가 0으로 설정되었는지 1로 설정되었는지에 따라 Series 또는 DataFrame을 반환합니다.
  • numpy.unique는 세 개의 불리언 플래그 값에 따라 단일 배열 또는 두 개에서 네 개 사이의 배열을 포함하는 튜플을 반환합니다.

typing 이슈 트래커에는 추가 예시와 논의가 있습니다.

현재 이러한 함수의 타입 시그니처를 표현할 방법이 없습니다. PEP 484에는 전달된 값에 따라 반환 타입이 달라지는 시그니처를 작성하는 메커니즘이 포함되어 있지 않습니다. 이러한 API를 대신 열거형을 허용하도록 다시 설계하더라도 이 문제는 계속된다는 점에 유의하십시오. MyEnum.FOOMyEnum.BAR는 모두 MyEnum 타입으로 간주됩니다.

현재 타입 검사기는 중요한 내장 함수와 표준 라이브러리 함수에 임시 확장을 추가하는 방식으로 이러한 제한을 우회합니다. 예를 들어, mypy에는 open(...)에 대해 더 정확한 타입을 추론하려는 플러그인이 함께 제공됩니다. 이 방식은 표준 라이브러리 함수에는 적용되지만, 일반적으로 지속 가능하지 않습니다. 서드파티 라이브러리 작성자가 서로 다른 N개의 타입 검사기를 위한 플러그인을 유지 관리할 것으로 기대하는 것은 합리적이지 않습니다.

이러한 공백을 해소하기 위해 리터럴 타입을 추가할 것을 제안합니다.

핵심 의미론

이 절에서는 리터럴 타입의 기본 동작을 개괄합니다.

핵심 동작

리터럴 타입은 변수가 구체적이고 특정한 값을 가진다는 것을 나타냅니다. 예를 들어 어떤 변수 foo의 타입을 Literal[3]로 정의하면, foo가 정확히 3과 같아야 하며 다른 어떤 값과도 같아서는 안 된다고 선언하는 것입니다.

타입 T의 멤버인 어떤 값 v가 주어졌을 때, Literal[v]타입은 T의 서브타입으로 취급해야 합니다. 예를 들어 Literal[3]int의 서브타입입니다.

부모 타입의 모든 메서드는 리터럴 타입에 직접 상속됩니다. 따라서 타입이 Literal[3]인 변수 foo가 있다면 foo + 5와 같은 작업을 수행해도 안전합니다. foo가 int의 __add__ 메서드를 상속하기 때문입니다. foo + 5의 결과 타입은 int입니다.

이 “상속” 동작은 NewType을 처리하는 방식과 동일합니다.

두 Literal의 동등성

Literal[v1]Literal[v2]라는 두 타입은 다음 조건을 모두 만족할 때 동등합니다.

  1. type(v1) == type(v2)
  2. v1 == v2

예를 들어, Literal[20]Literal[0x14]는 동등합니다. 그러나 런타임에 0 == False가 ‘true’로 평가되더라도 Literal[0]Literal[False]는 동등하지 않습니다: 0의 타입은 int이고 False의 타입은 bool입니다.

리터럴 합집합 줄이기

리터럴은 하나 이상의 값으로 매개변수화됩니다. Literal이 둘 이상의 값으로 매개변수화되면 해당 타입들의 합집합과 정확히 동등한 것으로 취급됩니다. 즉, Literal[v1, v2, v3]Union[Literal[v1], Literal[v2], Literal[v3]]와 동등합니다.

이 간단한 표기법을 사용하면 여러 리터럴을 허용하는 함수의 시그니처를 더 편리하게 작성할 수 있습니다 — 예를 들어 open(...)과 같은 함수가 있습니다.:

# Note: this is a simplification of the true type signature.
_PathType = Union[str, bytes, int]

@overload
def open(path: _PathType,
         mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
         ) -> IO[Text]: ...
@overload
def open(path: _PathType,
         mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"],
         ) -> IO[bytes]: ...

# Fallback overload for when the user isn't using literal types
@overload
def open(path: _PathType, mode: str) -> IO[Any]: ...

제공되는 값이 모두 같은 타입의 멤버일 필요는 없습니다. 예를 들어, Literal[42, "foo", True]는 유효한 타입입니다.

그러나 Literal은 반드시 하나 이상의 타입으로 매개변수화해야 합니다. Literal[]이나 Literal과 같은 타입은 유효하지 않습니다.

유효한 매개변수화와 유효하지 않은 매개변수화

이 절에서는 정확히 무엇이 유효한 Literal[...]타입을 구성하는지, 즉 어떤 값을 매개변수로 사용할 수 있고 어떤 값을 사용할 수 없는지를 설명합니다.

간단히 말해, Literal[...]타입은 하나 이상의 리터럴 표현식으로만 매개변수화할 수 있으며, 다른 것은 사용할 수 없습니다.

타입 검사 시점에 Literal에 사용할 수 있는 유효한 매개변수

Literal은 리터럴 정수, 바이트 및 유니코드 문자열, 불리언, Enum 값 및 None으로 매개변수화할 수 있습니다. 예를 들어 다음은 모두 유효합니다.:

Literal[26]
Literal[0x1A]  # Exactly equivalent to Literal[26]
Literal[-4]
Literal["hello world"]
Literal[b"hello world"]
Literal[u"hello world"]
Literal[True]
Literal[Color.RED]  # Assuming Color is some enum
Literal[None]

참고: None타입은 단 하나의 값만 가지므로 None타입과 Literal[None]타입은 정확히 동등합니다. 타입 검사기는 Literal[None]을 단순히 None으로 단순화할 수 있습니다.

Literal은 다른 리터럴 타입이나 다른 리터럴 타입에 대한 타입 별칭으로도 매개변수화할 수 있습니다. 예를 들어 다음은 유효합니다.:

ReadOnlyMode         = Literal["r", "r+"]
WriteAndTruncateMode = Literal["w", "w+", "wt", "w+t"]
WriteNoTruncateMode  = Literal["r+", "r+t"]
AppendMode           = Literal["a", "a+", "at", "a+t"]

AllModes = Literal[ReadOnlyMode, WriteAndTruncateMode,
                   WriteNoTruncateMode, AppendMode]

이 기능은 리터럴 타입을 사용하고 재사용하는 작업을 다시 한층 더 편리하게 만들기 위한 것입니다.

참고: 위 규칙의 결과로, 타입 검사기는 다음과 같이 보이는 타입도 지원할 것으로 예상됩니다.:

Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]

이는 다음 타입과 정확히 동등해야 합니다.:

Literal[1, 2, 3, "foo", 5, None]

…그리고 다음 타입과도 동등해야 합니다.:

Optional[Literal[1, 2, 3, "foo", 5]]

참고: Literal["foo"]와 같은 문자열 리터럴 타입은 일반 문자열 리터럴이 런타임에 동작하는 것과 같은 방식으로 bytes 또는 unicode의 서브타입이어야 합니다.

예를 들어 Python 3에서 Literal["foo"]타입은 Literal[u"foo"]와 동등합니다. Python 3에서는 "foo"u"foo"와 동등하기 때문입니다.

마찬가지로 Python 2에서는 Literal["foo"] 타입이 Literal[b"foo"]와 동등합니다. 단, 파일에 from __future__ import unicode_literals 임포트가 포함된 경우에는 Literal[u"foo"]와 동등합니다.

타입 검사 시 Literal에 허용되지 않는 매개변수

다음 매개변수는 설계상 의도적으로 허용되지 않습니다.

  • Literal[3 + 4] 또는 Literal["foo".replace("o", "b")]와 같은 임의의 표현식입니다.
    • 근거: 리터럴 타입은 PEP 484 타이핑 생태계에 대한 최소한의 확장으로 설계되었으며, 타입 내부의 잠재적인 표현식을 해석하도록 타입 검사기에 요구하면 복잡성이 지나치게 커집니다. Rejected or out-of-scope ideas도 참조하십시오.
    • 따라서 Literal[4 + 3j]Literal[-4 + 2j]와 같은 복소수도 금지됩니다. 일관성을 위해 단일 복소수만 포함하는 Literal[4j]와 같은 리터럴도 금지됩니다.
    • 이 규칙의 유일한 예외는 정수에 대한 단항 -(빼기) 연산자입니다. Literal[-5]와 같은 타입은 허용됩니다.
  • Literal[(1, "foo", "bar")]와 같이 유효한 리터럴 타입을 포함하는 튜플입니다. 사용자는 언제든지 대신 이 타입을 Tuple[Literal[1], Literal["foo"], Literal["bar"]]로 표현할 수 있습니다. 또한 튜플은 Literal[1, 2, 3] 단축 표현과 혼동될 가능성이 큽니다.
  • 딕셔너리 리터럴, 리스트 리터럴 또는 세트 리터럴과 같은 변경 가능한 리터럴 데이터 구조입니다. 리터럴은 항상 암묵적으로 최종적이며 변경할 수 없습니다. 따라서 Literal[{"a": "b", "c": "d"}]는 잘못된 표현입니다.
  • 그 밖의 모든 타입도 허용되지 않습니다. 예를 들어 Literal[Path] 또는 Literal[some_object_instance]가 이에 해당합니다. 여기에는 타입 변수도 포함됩니다. T가 타입 변수인 경우 Literal[T]는 허용되지 않습니다. 타입 변수는 타입에 대해서만 변할 수 있으며, 값에 대해서는 변할 수 없습니다.

다음 항목은 단순성을 위해 잠정적으로 허용되지 않습니다. 이 PEP의 향후 확장에서 이러한 항목을 허용하는 방안을 고려할 수 있습니다.

  • 부동 소수점 수입니다. 예를 들어 Literal[3.14]입니다. 무한대 또는 NaN의 리터럴을 깔끔하게 표현하는 일은 까다롭습니다. 실제 세계의 API가 부동 소수점 매개변수에 따라 동작을 달리할 가능성도 낮습니다.
  • Any입니다. 예를 들어 Literal[Any]입니다. Any는 타입이며, Literal[...]은 값만 포함하도록 설계되었습니다. 또한 Literal[Any]가 실제로 의미하는 바가 무엇인지도 명확하지 않습니다.

런타임의 매개변수

타입 검사 시 Literal[...]이 포함할 수 있는 매개변수 집합은 매우 작지만, typing.Literal의 실제 구현은 런타임에 어떠한 검사도 수행하지 않습니다. 예를 들어 다음과 같습니다.:

def my_function(x: Literal[1 + 2]) -> int:
    return x * 3

x: Literal = 3
y: Literal[my_function] = my_function

타입 검사기는 이 프로그램을 거부해야 합니다. 이 사양에 따르면 Literal을 사용하는 세 가지 경우가 모두 유효하지 않기 때문입니다. 그러나 Python 자체는 오류 없이 이 프로그램을 실행해야 합니다.

이는 부분적으로 향후 Literal을 사용할 수 있는 범위를 확장하고자 할 경우 유연성을 유지하는 데 도움이 되며, 부분적으로는 애초에 런타임에 허용되지 않는 모든 매개변수를 감지할 수 없기 때문입니다. 예를 들어 런타임에는 Literal[1 + 2]Literal[3]을 구별할 수 없습니다.

리터럴, 열거형 및 전방 참조

한 가지 가능한 모호성은 리터럴 문자열과 리터럴 열거 멤버에 대한 정방향 참조 사이에 있습니다. 예를 들어, Literal["Color.RED"] 타입이 있다고 가정하십시오. 이 리터럴 타입에는 문자열 리터럴이 포함됩니까, 아니면 어떤 Color.RED 열거 멤버에 대한 정방향 참조가 포함됩니까?

이러한 경우에는 항상 사용자가 리터럴 문자열을 만들려고 했다고 가정합니다. 사용자가 정방향 참조를 원한다면 전체 리터럴 타입을 문자열로 감싸야 합니다. 예를 들면 "Literal[Color.RED]"와 같습니다.

타입 추론

이 절에서는 몇 가지 예와 함께 타입 추론 및 리터럴에 관한 몇 가지 규칙을 설명합니다.

하위 호환성

타입 검사기가 Literal 지원을 추가할 때에는 하위 호환성을 최대화하는 방식으로 추가하는 것이 중요합니다. 타입 검사기는 최선의 노력 원칙에 따라 Literal 지원이 추가된 후에도 이전에 타입 검사를 통과하던 코드가 계속 통과하도록 보장해야 합니다.

이는 타입 추론을 수행할 때 특히 중요합니다. 예를 들어 x = "blue"라는 문장이 주어졌을 때, x의 추론된 타입은 str이어야 합니까, 아니면 Literal["blue"]이어야 합니까?

한 가지 순진한 전략은 표현식이 항상 Literal 타입을 의도한다고 가정하는 것입니다. 따라서 위 예에서 x는 항상 Literal["blue"]라는 추론된 타입을 갖게 됩니다. 이 순진한 전략은 거의 확실히 지나치게 큰 혼란을 초래합니다. 이전에는 실패하지 않던 다음과 같은 프로그램이 실패하기 시작하게 만들기 때문입니다:

# If a type checker infers 'var' has type Literal[3]
# and my_list has type List[Literal[3]]...
var = 3
my_list = [var]

# ...this call would be a type-error.
my_list.append(4)

이 전략이 실패하는 또 다른 예는 객체의 필드를 설정할 때입니다:

class MyObject:
    def __init__(self) -> None:
        # If a type checker infers MyObject.field has type Literal[3]...
        self.field = 3

m = MyObject()

# ...this assignment would no longer type check
m.field = 4

모든 경우에 호환성을 유지하는 대안 전략은 표현식에 명시적으로 다른 어노테이션이 지정되지 않는 한 항상 표현식이 Literal 타입이 아니라고 가정하는 것입니다. 이 전략을 사용하는 타입 검사기는 위의 첫 번째 예에서 xstr 타입이라고 항상 추론합니다.

이것이 실행 가능한 유일한 전략은 아닙니다. 타입 검사기는 더 정교한 추론 기법을 자유롭게 실험해야 합니다. 이 PEP는 특정 전략을 요구하지 않으며, 하위 호환성의 중요성만을 강조합니다.

Literal 컨텍스트에서 리터럴이 아닌 값 사용

Literal 타입은 추가적인 특수 처리 없이 서브타이핑에 관한 기존 규칙을 따릅니다. 예를 들어 다음과 같은 프로그램은 타입 안전합니다:

def expects_str(x: str) -> None: ...
var: Literal["foo"] = "foo"

# Legal: Literal["foo"] is a subtype of str
expects_str(var)

이는 일반적으로 리터럴이 아닌 표현식을 자동으로 Literal로 캐스팅해서는 안 된다는 의미이기도 합니다. 예를 들면 다음과 같습니다:

def expects_literal(x: Literal["foo"]) -> None: ...

def runner(my_str: str) -> None:
    # ILLEGAL: str is not a subclass of Literal["foo"]
    expects_literal(my_str)

참고: 사용자가 자신의 API에서 리터럴 원래 타입을 모두 허용하도록 하려는 경우(레거시 목적일 수 있음), 폴백 오버로드를 구현해야 합니다. Interactions with overloads를 참조하십시오.

다른 타입 및 기능과의 상호 작용

이 절에서는 Literal 타입이 기존의 다른 타입과 상호 작용하는 방식을 설명합니다.

구조화된 데이터의 지능적 인덱싱

리터럴은 튜플, NamedTuple 및 클래스와 같은 구조적 타입을 “지능적으로 인덱싱”하는 데 사용할 수 있습니다. (참고: 이는 전체 목록이 아닙니다.)

예를 들어 타입 검사기는 유효한 인덱스에 해당하는 int 키를 사용하여 튜플을 인덱싱할 때 올바른 값 타입을 추론해야 합니다.:

a: Literal[0] = 0
b: Literal[5] = 5

some_tuple: Tuple[int, str, List[bool]] = (3, "abc", [True, False])
reveal_type(some_tuple[a])   # Revealed type is 'int'
some_tuple[b]                # Error: 5 is not a valid index into the tuple

getattr 같은 함수 사용 시에도 유사한 동작이 이루어질 것으로 예상합니다.:

class Test:
    def __init__(self, param: int) -> None:
        self.myfield = param

    def mymethod(self, val: int) -> str: ...

a: Literal["myfield"]  = "myfield"
b: Literal["mymethod"] = "mymethod"
c: Literal["blah"]     = "blah"

t = Test()
reveal_type(getattr(t, a))  # Revealed type is 'int'
reveal_type(getattr(t, b))  # Revealed type is 'Callable[[int], str]'
getattr(t, c)               # Error: No attribute named 'blah' in Test

참고: 위의 변수 선언을 더 간결한 방식으로 표현하는 제안은 Interactions with Final을 참조하십시오.

오버로드와의 상호작용입니다.

리터럴 타입과 오버로드는 특별한 방식으로 상호작용할 필요가 없습니다. 기존 규칙이 잘 작동합니다.

그러나 타입 검사기가 반드시 지원해야 하는 중요한 사용 사례 중 하나는 사용자가 리터럴 타입을 사용하지 않을 때 fallback을 사용할 수 있는 기능입니다. 예를 들어, open을 생각해 보십시오.:

_PathType = Union[str, bytes, int]

@overload
def open(path: _PathType,
         mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
         ) -> IO[Text]: ...
@overload
def open(path: _PathType,
         mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"],
         ) -> IO[bytes]: ...

# Fallback overload for when the user isn't using literal types
@overload
def open(path: _PathType, mode: str) -> IO[Any]: ...

open의 시그니처를 처음 두 오버로드만 사용하도록 변경하면, 리터럴 문자열 표현식을 전달하지 않는 모든 코드가 중단됩니다. 예를 들어, 다음과 같은 코드가 중단됩니다.:

mode: str = pick_file_mode(...)
with open(path, mode) as f:
    # f should continue to be of type IO[Any] here

조금 더 넓게 말하면, 기존 API에 리터럴 타입을 추가할 때마다 하위 호환성을 유지하기 위해 항상 fallback 오버로드도 포함하도록 typeshed에 정책을 추가할 것을 제안합니다.

제네릭과의 상호작용입니다.

Literal[3]과 같은 타입은 그저 평범한 int의 서브클래스가 되도록 의도되었습니다. 이는 제네릭과 함께 사용하는 경우처럼 일반 타입을 사용할 수 있는 어디에서나 Literal[3]과 같은 타입을 사용할 수 있다는 의미입니다.

따라서 리터럴 타입을 사용하여 제네릭 함수나 클래스에 매개변수를 지정하는 것이 합법적입니다.:

A = TypeVar('A', bound=int)
B = TypeVar('B', bound=int)
C = TypeVar('C', bound=int)

# A simplified definition for Matrix[row, column]
class Matrix(Generic[A, B]):
    def __add__(self, other: Matrix[A, B]) -> Matrix[A, B]: ...
    def __matmul__(self, other: Matrix[B, C]) -> Matrix[A, C]: ...
    def transpose(self) -> Matrix[B, A]: ...

foo: Matrix[Literal[2], Literal[3]] = Matrix(...)
bar: Matrix[Literal[3], Literal[7]] = Matrix(...)

baz = foo @ bar
reveal_type(baz)  # Revealed type is 'Matrix[Literal[2], Literal[7]]'

마찬가지로, 리터럴 타입을 포함하는 값 제한 또는 상한을 사용하여 TypeVar를 생성하는 것도 합법적입니다.:

T = TypeVar('T', Literal["a"], Literal["b"], Literal["c"])
S = TypeVar('S', bound=Literal["foo"])

…하지만 리터럴 상한이 있는 TypeVar를 생성하는 것이 실제로 언제 유용할지는 명확하지 않습니다. 예를 들어, 위 예시의 S TypeVar는 사실상 의미가 없습니다. 대신 S = Literal["foo"]를 사용하여 동등한 동작을 얻을 수 있습니다.

참고: 리터럴 타입과 제네릭은 의도적으로 매우 기본적이고 제한적인 방식으로만 상호작용합니다. 특히, 많은 양의 수치 연산이나 numpy 스타일 조작이 포함된 코드를 타입 검사하려는 라이브러리는 거의 확실히 이 PEP에서 제안하는 리터럴 타입이 자신들의 요구에 불충분하다고 판단할 것입니다.

이 문제를 해결하기 위한 여러 가지 제안을 검토했지만, 결국 정수 제네릭 문제는 추후로 미루기로 결정했습니다. 자세한 내용은 Rejected or out-of-scope ideas를 참조하십시오.

열거형 및 완전성 검사와의 상호작용입니다.

타입 검사기는 열거형과 같이 변형의 수가 정해져 있는 리터럴 타입을 다룰 때 완전성 검사를 수행할 수 있어야 합니다. 예를 들어, Status 열거형의 세 값이 모두 이미 소진되었으므로 마지막 else 문이 str 타입이어야 한다고 타입 검사기가 추론할 수 있어야 합니다.:

class Status(Enum):
    SUCCESS = 0
    INVALID_DATA = 1
    FATAL_ERROR = 2

def parse_status(s: Union[str, Status]) -> None:
    if s is Status.SUCCESS:
        print("Success!")
    elif s is Status.INVALID_DATA:
        print("The given data is invalid because...")
    elif s is Status.FATAL_ERROR:
        print("Unexpected fatal error...")
    else:
        # 's' must be of type 'str' since all other options are exhausted
        print("Got custom status: " + s)

위에서 설명한 상호작용은 새로운 것이 아닙니다. 이미 PEP 484에 명문화되어 있습니다. 그러나 많은 타입 검사기(예: mypy)는 구현 작업의 예상되는 복잡성 때문에 아직 이를 구현하지 않습니다.

리터럴 타입이 도입되면 이러한 복잡성 중 일부가 완화됩니다. 열거형을 완전히 특수 처리하는 대신, 열거형을 해당 값들의 합집합과 대략 동등한 것으로 취급하고, 타입 검사기가 이미 구현했을 수 있는 합집합, 완전성, 타입 축소, 도달 가능성 등에 관한 기존 로직을 활용할 수 있습니다.

따라서 여기서는 Status 열거형을 Literal[Status.SUCCESS, Status.INVALID_DATA, Status.FATAL_ERROR]와 대략 동등한 것으로 취급하고 s의 타입을 그에 맞게 좁힐 수 있습니다.

타입 축소와의 상호작용입니다.

타입 검사기는 위 절에서 설명한 것 이상으로 열거형 및 비열거형 리터럴 타입에 대해 선택적으로 추가 분석을 수행할 수 있습니다.

예를 들어, 포함 여부 또는 동등성 검사와 같은 것을 기반으로 타입을 좁히는 것이 유용할 수 있습니다.:

def parse_status(status: str) -> None:
    if status in ("MALFORMED", "ABORTED"):
        # Type checker could narrow 'status' to type
        # Literal["MALFORMED", "ABORTED"] here.
        return expects_bad_status(status)

    # Similarly, type checker could narrow 'status' to Literal["PENDING"]
    if status == "PENDING":
        expects_pending_status(status)

리터럴 불리언과 관련된 표현식을 고려하여 타입을 좁히는 것도 유용할 수 있습니다. 예를 들어, Literal[True], Literal[False] 및 오버로드를 결합하여 “custom type guards”를 구성할 수 있습니다.:

@overload
def is_int_like(x: Union[int, List[int]]) -> Literal[True]: ...
@overload
def is_int_like(x: object) -> bool: ...
def is_int_like(x): ...

vector: List[int] = [1, 2, 3]
if is_int_like(vector):
    vector.append(3)
else:
    vector.append("bad")   # This branch is inferred to be unreachable

scalar: Union[int, str]
if is_int_like(scalar):
    scalar += 3      # Type checks: type of 'scalar' is narrowed to 'int'
else:
    scalar += "foo"  # Type checks: type of 'scalar' is narrowed to 'str'

Final과의 상호 작용

PEP 591은 typing 생태계에 “Final” 한정자를 추가할 것을 제안합니다. 이 한정자는 일부 변수 또는 속성을 재할당할 수 없음을 선언하는 데 사용할 수 있습니다.:

foo: Final = 3
foo = 4           # Error: 'foo' is declared to be Final

위의 예제에서는 foo가 항상 정확히 3과 같다는 점에 유의하십시오. 타입 검사기는 이 정보를 사용하여 fooLiteral[3]을 기대하는 모든 컨텍스트에서 사용하기에 유효하다고 추론할 수 있습니다.:

def expects_three(x: Literal[3]) -> None: ...

expects_three(foo)  # Type checks, since 'foo' is Final and equal to 3

Final 한정자는 변수가 사실상 Literal임을 선언하기 위한 축약형입니다.

이 PEP와 PEP 591이 모두 승인되면 타입 검사기는 이 단축 표현을 지원할 것으로 예상됩니다. 구체적으로 var: Final = value 형식의 변수 또는 속성 할당이 주어지고, valueLiteral[...]의 유효한 매개변수인 경우, 타입 검사기는 varLiteral[value]를 기대하는 모든 컨텍스트에서 사용할 수 있음을 이해해야 합니다.

타입 검사기는 Final의 다른 사용을 이해할 의무가 없습니다. 예를 들어, 다음 프로그램이 타입 검사를 통과하는지 여부는 지정되지 않습니다.:

# Note: The assignment does not exactly match the form 'var: Final = value'.
bar1: Final[int] = 3
expects_three(bar1)  # May or may not be accepted by type checkers

# Note: "Literal[1 + 2]" is not a legal type.
bar2: Final = 1 + 2
expects_three(bar2)  # May or may not be accepted by type checkers

거부되었거나 범위를 벗어난 아이디어

이 절에서는 명시적으로 범위를 벗어난 몇 가지 잠재적 기능을 설명합니다.

진정한 종속 타입/정수 제네릭

이 제안은 본질적으로 PEP 484 생태계에 매우 단순화된 종속 타입 시스템을 추가하는 내용을 설명합니다. 한 가지 명백한 확장 방법은 사용자가 임의의 방식으로 값에 기반하여 타입에 조건을 지정할 수 있는 완전한 종속 타입 시스템을 구현하는 것입니다. 이를 통해 다음과 같은 시그니처를 작성할 수 있습니다.:

# A vector has length 'n', containing elements of type 'T'
class Vector(Generic[N, T]): ...

# The type checker will statically verify our function genuinely does
# construct a vector that is equal in length to "len(vec1) + len(vec2)"
# and will throw an error if it does not.
def concat(vec1: Vector[A, T], vec2: Vector[B, T]) -> Vector[A + B, T]:
    # ...snip...

최소한 어떤 형태의 정수 제네릭을 추가하는 것은 유용할 것입니다.

이러한 타입 시스템이 분명 유용하겠지만, 현재 제안과 비교하면 이를 완성하는 데 훨씬 더 많은 구현 작업과 논의 및 연구가 필요하므로 이 PEP의 범위를 벗어납니다.

앞으로 이 주제로 돌아와 다시 검토하게 될 가능성은 충분합니다. numpy와 같은 인기 라이브러리를 지원하려면 가변 제네릭과 같은 다른 확장과 함께 어떤 형태의 종속 타이핑이 필요할 가능성이 매우 높습니다.

이 PEP는 포괄적인 해결책을 제공하려는 시도가 아니라 이 목표를 향한 디딤돌로 보아야 합니다.

더 간결한 구문 추가

이 PEP에 대한 한 가지 반론은 Literal[...]을 명시적으로 작성해야 하는 것이 장황하게 느껴진다는 점입니다. 예를 들어, 다음과 같이 작성하는 대신:

def foobar(arg1: Literal[1], arg2: Literal[True]) -> None:
    pass

…대신 다음과 같이 작성할 수 있다면 좋을 것입니다.:

def foobar(arg1: 1, arg2: True) -> None:
    pass

안타깝게도 이러한 약어는 기존 typing 구현에서는 런타임에 전혀 작동하지 않습니다. 예를 들어, 다음 코드 조각은 Python 3.7에서 실행하면 충돌합니다.:

from typing import Tuple

# Supposed to accept tuple containing the literals 1 and 2
def foo(x: Tuple[1, 2]) -> None:
    pass

이를 실행하면 다음 예외가 발생합니다.:

TypeError: Tuple[t0, t1, ...]: each t must be a type. Got 1.

사용자가 Literal을 생략해도 되는 정확한 시점을 외워야 하는 것을 원하지 않으므로, Literal은 항상 포함하도록 요구합니다.

좀 더 넓게 보면, Python의 타입 구문을 전면 개편하는 것은 이 PEP의 범위에 속하지 않는다고 생각합니다. 그러한 논의는 이 PEP에 덧붙이기보다는 별도의 PEP에서 다루는 것이 가장 좋을 것입니다. 따라서, 이 PEP는 의도적으로 Python의 타입 구문을 혁신하려 하지 않습니다.

Literal 타입 백포트하기

이 PEP가 승인되면, Literal 타입은 더 오래된 버전의 typing 모듈이 함께 번들로 제공되는 Python 버전을 위해 백포트되어야 합니다. 저희는 다양한 다른 백포트된 타입을 포함하는 서드파티 모듈인 typing_extensionsLiteral을 추가함으로써 이를 수행할 계획입니다.

구현

mypy 타입 검사기는 현재 위에서 설명한 enum 리터럴과 좀 더 복잡한 일부 타입 좁히기 상호작용을 제외하고, 이 명세에서 설명한 동작의 상당 부분을 이미 구현했습니다.

관련 작업

이 제안은 다음 스레드에서 이루어진 논의를 바탕으로 작성되었습니다.

이 제안의 전체적인 설계는 결국 TypeScript에서 리터럴 타입이 처리되는 방식과 유사한 형태로 수렴하게 되었습니다.

감사의 말

이 PEP에 대한 의견을 주신 Mark Mendoza, Ran Benita, Rebecca Chen 및 typing-sig의 다른 구성원들께 감사드립니다.

이 PEP의 동기와 근거 상당 부분을 제공하는 데 도움을 준 mypy 및 typing 이슈 트래커의 다양한 참여자들께도 추가로 감사드립니다.