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

Python 개선 제안 한국어 번역

PEP 647 – 사용자 정의 타입 가드

Author:
Eric Traut <erictr at microsoft.com>
Sponsor:
Guido van Rossum <guido at python.org>
Discussions-To:
Typing-SIG list
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
07-Oct-2020
Python-Version:
3.10
Post-History:
28-Dec-2020, 09-Apr-2021
Resolution:
Python-Dev thread

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document: see TypeGuard and typing.TypeGuard 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는 런타임 검사에 기반하여 타입 검사기가 수행하는 조건부 타입 좁히기에 프로그램이 영향을 미칠 수 있는 방법을 명시합니다.

동기

정적 타입 검사기는 일반적으로 프로그램의 코드 흐름 내 표현식의 더 정확한 타입을 결정하기 위해 “타입 좁히기”라는 기법을 사용합니다. 조건부 코드 흐름 문(예: ifwhile 문)에 기반하여 코드 블록 내에서 타입 좁히기가 적용될 때, 조건식은 때때로 “타입 가드”라고 합니다. Python 타입 검사기는 일반적으로 다양한 형태의 타입 가드 표현식을 지원합니다.

def func(val: Optional[str]):
    # "is None" type guard
    if val is not None:
        # Type of val is narrowed to str
        ...
    else:
        # Type of val is narrowed to None
        ...

def func(val: Optional[str]):
    # Truthy type guard
    if val:
        # Type of val is narrowed to str
        ...
    else:
        # Type of val remains Optional[str]
        ...

def func(val: Union[str, float]):
    # "isinstance" type guard
    if isinstance(val, str):
        # Type of val is narrowed to str
        ...
    else:
        # Type of val is narrowed to float
        ...

def func(val: Literal[1, 2]):
    # Comparison type guard
    if val == 1:
        # Type of val is narrowed to Literal[1]
        ...
    else:
        # Type of val is narrowed to Literal[2]
        ...

정적 정보만을 기반으로 타입 좁히기를 적용할 수 없는 경우가 있습니다. 다음 예를 고려하십시오:

def is_str_list(val: List[object]) -> bool:
    """Determines whether all objects in the list are strings"""
    return all(isinstance(x, str) for x in val)

def func1(val: List[object]):
    if is_str_list(val):
        print(" ".join(val)) # Error: invalid type

이 코드는 올바르지만, join 메서드에 전달된 val값이 List[object] 타입으로 이해되므로 타입 검사기는 타입 오류를 보고합니다. 이 시점에서 타입 검사기에는 val의 타입이 List[str] 타입인지 정적으로 확인할 충분한 정보가 없습니다.

이 PEP는 is_str_list와 같은 함수를 “사용자 정의 타입 가드”로 정의하는 방법을 도입합니다. 이를 통해 코드에서 타입 검사기가 지원하는 타입 가드를 확장할 수 있습니다.

이 새로운 메커니즘을 사용하면 위 예제의 is_str_list 함수가 약간 수정됩니다. 반환 타입이 bool에서 TypeGuard[List[str]]로 변경됩니다. 이는 반환 값이 불리언이라는 것뿐 아니라, true가 함수에 입력된 값이 지정된 타입이었다는 것을 나타낸다는 의미입니다.

from typing import TypeGuard

def is_str_list(val: List[object]) -> TypeGuard[List[str]]:
    """Determines whether all objects in the list are strings"""
    return all(isinstance(x, str) for x in val)

사용자 정의 타입 가드를 사용하여 딕셔너리가 TypedDict의 타입 요구 사항을 따르는지도 확인할 수 있습니다.

class Person(TypedDict):
    name: str
    age: int

def is_person(val: dict) -> "TypeGuard[Person]":
    try:
        return isinstance(val["name"], str) and isinstance(val["age"], int)
    except KeyError:
        return False

def print_age(val: dict):
    if is_person(val):
        print(f"Age: {val['age']}")
    else:
        print("Not a person!")

사양

TypeGuard 타입

이 PEP는 typing 모듈에서 내보내는 기호 TypeGuard를 도입합니다. TypeGuard는 단일 타입 인자를 받는 특수 형식입니다. 이는 사용자 정의 타입 가드 함수의 반환 타입에 어노테이션을 지정하는 데 사용됩니다. 타입 가드 함수 내부의 return 문은 bool 값을 반환해야 하며, 타입 검사기는 모든 반환 경로가 bool을 반환하는지 확인해야 합니다.

그 밖의 모든 측면에서 TypeGuard는 bool과 구별되는 타입입니다. 이는 bool의 서브타입이 아닙니다. 따라서 Callable[..., TypeGuard[int]]Callable[..., bool]에 할당할 수 없습니다.

TypeGuard를 하나 이상의 매개변수를 받는 함수 또는 메서드의 반환 타입에 어노테이션으로 사용할 경우, 타입 검사기는 해당 함수 또는 메서드를 사용자 정의 타입 가드로 취급합니다. TypeGuard에 제공된 타입 인자는 함수가 검증한 타입을 나타냅니다.

이 예에서 보듯이 사용자 정의 타입 가드는 제네릭 함수일 수 있습니다:

_T = TypeVar("_T")

def is_two_element_tuple(val: Tuple[_T, ...]) -> TypeGuard[Tuple[_T, _T]]:
    return len(val) == 2

def func(names: Tuple[str, ...]):
    if is_two_element_tuple(names):
        reveal_type(names)  # Tuple[str, str]
    else:
        reveal_type(names)  # Tuple[str, ...]

타입 검사기는 사용자 정의 타입 가드에 첫 번째 위치 인자로 전달되는 표현식에 타입 좁히기를 적용해야 한다고 가정해야 합니다. 타입 가드 함수가 둘 이상의 인자를 받는 경우, 추가 인자 표현식에는 타입 좁히기가 적용되지 않습니다.

타입 가드 함수가 인스턴스 메서드 또는 클래스 메서드로 구현된 경우, 첫 번째 위치 인자는 “self” 또는 “cls” 다음의 두 번째 매개변수에 대응합니다.

다음은 둘 이상의 인자를 받는 사용자 정의 타입 가드 함수의 몇 가지 예입니다:

def is_str_list(val: List[object], allow_empty: bool) -> TypeGuard[List[str]]:
    if len(val) == 0:
        return allow_empty
    return all(isinstance(x, str) for x in val)

_T = TypeVar("_T")

def is_set_of(val: Set[Any], type: Type[_T]) -> TypeGuard[Set[_T]]:
    return all(isinstance(x, type) for x in val)

사용자 정의 타입 가드 함수의 반환 타입은 일반적으로 첫 번째 인자의 타입보다 엄격하게 “더 좁은” 타입을 가리킵니다(즉, 더 일반적인 타입에 할당할 수 있는 더 구체적인 타입입니다). 그러나 반환 타입이 반드시 엄격하게 더 좁아야 하는 것은 아닙니다. 이를 통해 위의 예에서 List[str]List[object]에 할당할 수 없는 경우와 같은 상황을 허용합니다.

조건문에 사용자 정의 타입 가드 함수 호출이 포함되고 해당 함수가 참을 반환하는 경우, 정적 타입 검사기는 타입 가드 함수에 첫 번째 위치 인자로 전달된 표현식이 TypeGuard 반환 타입에 지정된 타입을 갖는 것으로 간주해야 합니다. 조건부 코드 블록 내에서 추가로 좁혀지지 않는 한 그렇습니다.

일부 내장 타입 가드는 양성 테스트와 음성 테스트 모두에서(if 절과 else 절 모두에서) 좁히기를 제공합니다. 예를 들어 x is None형식의 표현식에 대한 타입 가드를 생각해 보십시오. x의 타입이 None과 다른 어떤 타입의 합집합인 경우, 양성 상황에서는 None으로, 음성 상황에서는 다른 타입으로 좁혀집니다. 사용자 정의 타입 가드는 양성 상황(if 절)에서만 좁히기를 적용합니다. 음성 상황에서는 타입이 좁혀지지 않습니다.

OneOrTwoStrs = Union[Tuple[str], Tuple[str, str]]
def func(val: OneOrTwoStrs):
    if is_two_element_tuple(val):
        reveal_type(val)  # Tuple[str, str]
        ...
    else:
        reveal_type(val)   # OneOrTwoStrs
        ...

    if not is_two_element_tuple(val):
        reveal_type(val)   # OneOrTwoStrs
        ...
    else:
        reveal_type(val)  # Tuple[str, str]
        ...

하위 호환성

이 새로운 기능을 사용하지 않는 기존 코드는 영향을 받지 않습니다.

특히 표준 라이브러리 typing 라이브러리와 호환되지 않는 방식으로 어노테이션을 사용하는 코드는 단순히 TypeGuard를 임포트하지 않아야 합니다.

참조 구현

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

거부된 아이디어

데코레이터 구문

타입 가드를 정의하기 위해 데코레이터를 사용하는 방안이 고려되었습니다.

@type_guard(List[str])
def is_str_list(val: List[object]) -> bool: ...

데코레이터 방식은 타입을 런타임에 평가해야 하므로 순방향 참조를 사용할 수 없다는 점에서 열등합니다. 또한 제안된 방식이 이해하기 더 쉽고 구현하기 더 간단한 것으로 판단되었습니다.

엄격한 좁히기 적용

엄격한 타입 좁히기 적용(TypeGuard 타입 인자에 지정된 타입이 첫 번째 매개변수에 지정된 타입보다 더 좁은 형태여야 한다는 요구 사항)이 고려되었지만, 이는 이 기능의 유용한 사용 사례를 없앱니다. 예를 들어 위의 is_str_list예는 불변성 규칙 때문에 List[str]List[object]의 서브타입이 아니므로 유효하지 않은 것으로 간주됩니다.

고려된 한 가지 변형은 기본적으로 엄격한 좁히기 요구 사항을 적용하되, 타입 가드 함수가 이 요구 사항을 따르지 않음을 나타내는 플래그를 지정할 수 있도록 하는 것이었습니다. 이는 번거롭고 불필요한 것으로 판단되어 거부되었습니다.

또 다른 고려 사항은 값 타입과 TypeGuard에 지정된 좁혀진 타입 사이에 어느 정도 겹치는 부분이 있음을 보장하는 덜 엄격한 검사를 정의하는 것이었습니다. 이 제안의 문제는 합집합, 프로토콜, 타입 변수, 제네릭 등을 고려할 때 타입 호환성 규칙이 이미 매우 복잡하다는 점입니다. 이 기능만을 위해 이러한 제약 중 일부를 완화하는 이 규칙의 변형을 정의하려면 규칙이 달라지는 모든 미묘한 방식과 제약이 완화되는 구체적인 상황을 명확히 기술해야 합니다. 이러한 이유로 모든 검사를 생략하기로 결정되었습니다.

엄격한 좁히기를 적용하지 않으면 타입 안전성을 깨뜨릴 수 있다는 점이 지적되었습니다. 잘못 작성된 타입 가드 함수는 안전하지 않거나 심지어 무의미한 결과를 생성할 수 있습니다. 예를 들면:

def f(value: int) -> TypeGuard[str]:
    return True

그러나 결연하거나 지식이 부족한 개발자가 타입 안전성을 무력화할 방법은 많으며, 가장 흔한 방법은 cast또는 Any를 사용하는 것입니다. Python 개발자가 자신의 코드에서 사용자 정의 타입 가드에 대해 학습하고 이를 구현하는 데 시간을 들인다면, 해당 개발자는 타입 안전성에 관심이 있으며 타입 안전성을 훼손하거나 무의미한 결과를 생성하는 방식으로 타입 가드 함수를 작성하지 않을 것이라고 보는 것이 안전합니다.

TypeGuard 타입의 조건부 적용

타입 가드 함수에 첫 번째 인자로 전달되는 표현식의 타입이 TypeGuard 반환 타입에 지정된 타입의 진정한 서브타입인 경우, 해당 표현식이 기존 타입을 유지해야 한다는 제안이 있었습니다. 예를 들어 타입 가드 함수가 def f(value: object) -> TypeGuard[float]이고 이 함수에 전달되는 표현식의 타입이 int인 경우, TypeGuard 반환 타입이 나타내는 float타입을 취하는 대신 int타입을 유지하게 됩니다. 이 제안은 복잡성과 불일치를 추가하고, 표현식의 타입이 유니온이나 여러 제약 조건이 있는 타입 변수와 같은 복합 타입인 경우의 올바른 동작에 관한 추가 질문을 제기했기 때문에 거부되었습니다. 추가된 복잡성과 불일치가 거의 또는 전혀 부가 가치를 제공하지 않는다는 점을 고려할 때 정당화되지 않는다고 결정되었습니다.

임의 매개변수의 좁히기

TypeScript의 사용자 정의 타입 가드 형식에서는 모든 입력 매개변수를 좁히기를 위해 검사되는 값으로 사용할 수 있습니다. TypeScript 언어 작성자들은 검사되는 매개변수가 첫 번째 매개변수가 아닌 TypeScript의 실제 사례를 기억하지 못했습니다. 이러한 이유로, 고안된 사용 사례를 지원하기 위해 Python의 사용자 정의 타입 가드 구현에 추가적인 복잡성을 부담시킬 필요는 없다고 결정되었습니다. 이러한 사용 사례가 향후 식별된다면 TypeGuard 메커니즘을 확장할 방법이 있습니다. 여기에는 PEP 637에서 제안된 것처럼 키워드 인덱싱을 사용하는 방법이 포함될 수 있습니다.

암시적 “self” 및 “cls” 매개변수의 좁히기

이 제안에서는 첫 번째 위치 인자가 좁히기를 위해 검사되는 값이라고 가정합니다. 타입 가드 함수가 인스턴스 메서드 또는 클래스 메서드로 구현되면 암시적인 self또는 cls인자도 함수에 전달됩니다. selfcls에 좁히기 로직을 적용하려는 경우가 있을 수 있다는 우려가 제기되었습니다. 이는 일반적이지 않은 사용 사례이며, 이를 지원하면 사용자 정의 타입 가드의 구현이 상당히 복잡해집니다. 따라서 이를 위한 특별한 지원은 제공하지 않기로 결정되었습니다. self또는 cls의 좁히기가 필요한 경우 해당 값을 타입 가드 함수에 명시적 인자로 전달할 수 있습니다.