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로 설정되었는지 여부에 따라 bytes 또는 text를 반환합니다.

이 패턴은 널리 쓰이는 여러 서드파티 라이브러리에서도 꽤 흔합니다. 예를 들어, 다음은 각각 pandas와 numpy에서 가져온 두 가지 예시일 뿐입니다:

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

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

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

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

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

핵심 의미론

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

핵심 동작

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

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

부모 타입의 모든 메서드는 리터럴 타입에 직접 상속됩니다. 따라서 Literal[3] 타입의 변수 foo가 있다면, foo가 int의 __add__ 메서드를 상속하므로 foo + 5와 같은 연산을 안전하게 수행할 수 있습니다. foo + 5의 결과 타입은 int입니다.

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

두 리터럴의 동등성

두 타입 Literal[v1]과 Literal[v2]는 다음 두 조건이 모두 참일 때 동등합니다:

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

예를 들어 Literal[20]과 Literal[0x14]는 동등합니다. 하지만 런타임에 0 == False가 ‘참’으로 평가됨에도 불구하고 Literal[0]과 Literal[False]는 동등하지 않습니다: 0의 타입은 int이고 False의 타입은 bool입니다.

리터럴 유니온 단축 표기

리터럴은 하나 이상의 값으로 매개변수화됩니다. 리터럴이 둘 이상의 값으로 매개변수화되면, 해당 타입들의 유니온과 정확히 동등한 것으로 취급됩니다. 즉, 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은 리터럴 정수, 바이트 문자열과 유니코드 문자열, 불리언, 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 또는 유니코드 중 하나의 서브타입이 되어야 합니다.

예를 들어 Python 3에서는 "foo"가 u"foo"와 동등하므로, Literal["foo"] 타입은 Literal[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] 단축 표기와 혼동될 가능성이 높습니다.
  • dict 리터럴, list 리터럴, set 리터럴과 같은 가변 리터럴 자료 구조: 리터럴은 항상 암묵적으로 final이며 불변입니다. 따라서 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]".

타입 추론

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

하위 호환성

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

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

한 가지 단순한 전략은 표현식이 항상 리터럴 타입으로 의도되었다고 가정하는 것입니다. 그러면 위 예제에서 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

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

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

리터럴 컨텍스트에서 리터럴이 아닌 것을 사용하기

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

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

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

이는 또한 일반적으로 리터럴이 아닌 표현식이 자동으로 리터럴로 변환되어서는 안 된다는 의미이기도 합니다. 예를 들면 다음과 같습니다:

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를 참고하십시오.

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

이 절에서는 리터럴 타입이 기존의 다른 타입과 어떻게 상호작용하는지 설명합니다.

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

리터럴은 튜플, 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을 참조하십시오.

오버로드와의 상호작용

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

그러나 타입 검사기가 반드시 신경 써서 지원해야 하는 중요한 사용 사례 하나는 사용자가 리터럴 타입을 사용하지 않을 때 대체를 사용할 수 있는 기능입니다. 예를 들어 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에 리터럴 타입을 추가할 때마다 하위 호환성을 유지하기 위해 항상 대체 오버로드도 함께 포함하도록 의무화하는 정책을 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를 참조하십시오.

열거형 및 망라성 검사와의 상호작용

타입 검사기는 enum과 같이 변형의 수가 닫혀 있는 리터럴 타입을 다룰 때 완전성 검사를 수행할 수 있어야 합니다. 예를 들어 타입 검사기는 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 등)는 예상되는 구현 작업의 복잡성 때문에 아직 이를 구현하지 않았습니다.

리터럴 타입이 도입되면 이러한 복잡성의 일부가 완화될 것입니다. enum을 전적으로 특수 처리하는 대신, enum을 그 값들의 유니온과 대략 동등한 것으로 취급하고 타입 검사기가 이미 구현했을 수 있는 유니온, 완전성, 타입 좁히기, 도달 가능성 등에 관한 기존 로직을 활용할 수 있습니다.

따라서 여기서 Status 열거형은 Literal[Status.SUCCESS, Status.INVALID_DATA, Status.FATAL_ERROR]와 대략 동등한 것으로 취급될 수 있으며, s의 타입도 그에 따라 좁혀질 수 있습니다.

타입 좁히기와의 상호작용

타입 검사기는 enum 리터럴 타입과 enum이 아닌 리터럴 타입 모두에 대해 위 절에서 설명한 것 이상의 추가 분석을 선택적으로 수행할 수 있습니다.

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

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)

리터럴 bool을 포함하는 표현식을 고려하여 타입 좁히기를 수행하는 것도 유용할 수 있습니다. 예를 들어 Literal[True], Literal[False], 오버로드를 조합하여 “사용자 정의 타입 가드”를 만들 수 있습니다.:

@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은 타이핑 생태계에 “Final” 한정자를 추가할 것을 제안합니다. 이 한정자는 어떤 변수나 속성을 재할당할 수 없음을 선언하는 데 사용할 수 있습니다:

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

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

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

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

Final 한정자는 변수가 사실상 리터럴임을 선언하는 축약 표현 역할을 합니다.

이 PEP와 PEP 591이 모두 채택된다면, 타입 검사기는 이 약식 표기를 지원해야 합니다. 구체적으로, value가 Literal[...]의 유효한 매개변수인 var: Final = value 형태의 변수 또는 속성 할당이 주어지면, 타입 검사기는 Literal[value]를 기대하는 모든 문맥에서 var를 사용할 수 있다고 이해해야 합니다.

타입 검사기는 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가 승인되면, 이전 버전의 typing 모듈이 함께 제공되는 Python 버전을 위해 Literal 타입을 백포트해야 합니다. 여러 다른 백포트된 타입을 담고 있는 서드파티 모듈인 typing_extensions에 Literal을 추가하는 방식으로 이를 수행할 계획입니다.

구현

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

관련 작업

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

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

감사의 글

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

이 PEP의 동기와 근거를 마련하는 데 많은 도움을 주신 mypy 및 typing 이슈 트래커의 여러 참여자분들께도 감사드립니다.