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

Python 개선 제안 한국어 번역

PEP 718 – 첨자화 가능한 함수

Author:
James Hilton-Balfe <gobot1234yt at gmail.com>, Pablo Ruiz Cuevas <pablo.r.c at live.com>
Sponsor:
Guido van Rossum <guido at python.org>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Topic:
Typing
Created:
23-Jun-2023
Python-Version:
3.15
Post-History:
24-Jun-2023

Table of Contents

번역·라이선스 안내

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

초록

이 PEP는 타이핑 목적으로 함수 객체를 첨자화 가능하게 만들 것을 제안합니다. 이렇게 하면 양방향 추론(익명 함수 매개변수의 타입을 추론할 수 있게 하는 추론)과 전문화 이외의 방법으로는 충분하지 않은 경우, 타입 검사기가 생성하는 타입을 개발자가 명시적으로 제어할 수 있습니다. 또한 함수를 첨자화할 수 있다는 점에서 일반 클래스와 일관되게 만듭니다.

동기

현재 클래스에서는 제네릭 컨테이너에 대한 타입 어노테이션을 전달할 수 있습니다. 이는 특히 list, tupledict 등과 같은 일반적인 생성자에서 유용합니다.

my_integer_list = list[int]()
reveal_type(my_integer_list)  # type is list[int]

런타임에 list[int]는 나중에 호출할 수 있는 GenericAlias를 반환하며, 이 호출은 빈 리스트를 반환합니다.

또 다른 예로, 코드의 특정 부분에서 키가 str이고 값이 int인지 확인하려는 경우 특수화된 dict 타입을 생성할 수 있습니다.

NameNumberDict = dict[str, int]

NameNumberDict(
   one=1,
   two=2,
   three="3"  # Invalid: Literal["3"] is not of type int
)

이 구문은 유용하지만 함수에 사용하려고 하면 함수는 첨자화할 수 없으므로 오류가 발생합니다.

def my_list[T](arr: Iterable[T]) -> list[T]:
   # do something...
   return list(arr)

my_integer_list = my_list[int]()  # TypeError: 'function' object is not subscriptable

몇 가지 우회 방법이 있습니다.

  1. 호출 가능 객체 클래스를 만드는 방법:
class my_list[T]:
     def __call__(self, arr: Iterable[T]) -> list[T]:
         # do something...
         return list(arr)

my_string_list = my_list[str]([])
  1. 추가로 사용되지 않는 인자를 포함하여 PEP 747의 TypeForm을 사용합니다:
from typing import TypeForm

def my_list(*arr: Iterable[T], typ: TypeForm[T]) -> list[T]:
   # do something...
   return list(arr)

my_string_list = my_list([], str)

보시다시피 이 해결책은 추가 인자로 인해 복잡성이 증가합니다. 또한 사용자가 새로운 개념인 TypeForm을 이해해야 합니다.

  1. 할당에 어노테이션을 추가합니다.
my_integer_list: list[int] = my_list()

이 해결책은 반환 타입이 반복되고 더 장황하며, 반환 타입이 변경될 경우 여러 위치에서 타입을 갱신해야 하므로 최적이 아닙니다. 또한 특수화된 값을 다른 호출에 전달하려는 의도일 때 불필요하고 주의를 분산시키는 장황함을 추가합니다.

결론적으로 현재의 우회 방법은 언어의 나머지 부분과 일관된 구문에 비해 지나치게 복잡하거나 장황합니다.

제네릭 특수화

앞의 예와 마찬가지로 현재 서로 다른 특수화된 사용 사례에 대한 제네릭 별칭을 만들 수 있습니다.

NameNumberDict = dict[str, int]
NameNumberDict(one=1, two=2, three="3")  # Invalid: Literal["3"] is not of type int``

현재 함수에서는 이것이 가능하지 않지만, 허용된다면 코드베이스의 특정 부분에서 연산을 쉽게 특수화할 수 있습니다.

def constrained_addition[T](a: T, b: T) -> T: ...

# where we work exclusively with ints
int_addition = constrained_addition[int]
int_addition(2, 4+8j)  # Invalid: complex is not of type int

알 수 없는 타입

현재 특정 상황에서는 제네릭 함수의 타입 매개변수를 추론할 수 없습니다.

이 예제에서는 현재 T를 의미 있게 추론할 수 없으므로, 추가 할당 없이는 x가 타입이 지정되지 않습니다.

def factory[T](func: Callable[[T], Any]) -> Foo[T]: ...

reveal_type(factory(lambda x: "Hello World" * x)) # type is Foo[Unknown]

그러나 함수 객체를 첨자화할 수 있다면 더 구체적인 타입을 지정할 수 있습니다.

reveal_type(factory[int](lambda x: "Hello World" * x))  # type is Foo[int]

해결할 수 없는 타입 매개변수

현재 특수화되지 않은 리터럴을 사용하면 다음과 유사한 상황에서 타입을 결정할 수 없습니다.

def foo[T](x: list[T]) -> T: ...
reveal_type(foo([]))  # type checker cannot infer T (yet again)
reveal_type(foo[int]([]))  # type is int

또한 특정 타입을 사전에 함수에 전달해야 하는 경우 해당 타입을 지정할 수 있으면 유용합니다.

words = ["hello", "world"]
foo[int](words)  # Invalid: list[str] is incompatible with list[int]

첨자화를 허용하면 기존에 그렇지 않았던 경우에도 함수와 메서드가 제네릭 클래스와 일관되게 됩니다. 제안된 모든 변경 사항은 호출 가능 객체인 제네릭 클래스를 사용하여 구현할 수 있지만, 문법적 설탕이 있으면 매우 유용합니다.

이로 인해 함수를 특수화하여 새로운 팩토리로 사용해도 괜찮습니다.

make_int_list = make_list[int]
reveal_type(make_int_list())  # type is list[int]

단형화 및 구체화

이 제안은 monomorphisationreified types의 가능성도 열어 줍니다.

이를 통해 일화적으로 여러 차례 요청되었던 기능을 제공할 수 있습니다.

이 기능은 이 PEP에서 제안하는 것이 아니지만, 향후 구현될 수 있다는 점에 유의하십시오.

이러한 기능의 구문은 다음과 비슷할 수 있습니다.

def foo[T]():
   return T.__value__

assert foo[int]() is int

근거

이 제안은 기존 클래스 구문의 자연스러운 형태와 이미 비슷하게 보이고 느껴지는 구문을 허용함으로써 타입 시스템의 일관성을 향상합니다.

승인된다면 이 구문은 PEP 747s TypeForm를 학습해야 할 필요성을 줄이고, 안전하게 타입이 지정된 Python의 장황함과 인지 부하를 줄입니다.

사양

이 PEP에서 “Function objects”는 FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodTypeMethodWrapperType을 가리키는 용어로 사용됩니다.

MethodType의 경우 다음과 같이 작성할 수 있어야 합니다.

class Foo:
    def make_list[T](self, *args: T) -> list[T]: ...

Foo().make_list[int]()

그리고 FunctionType과 유사하게 작동하도록 해야 합니다.

BuiltinFunctionType의 경우, 내장 제네릭 함수(예: maxmin)가 Python에서 정의된 함수처럼 작동하도록 합니다. 내장 함수는 가능한 한 Python으로 구현된 함수와 유사하게 동작해야 합니다.

BuiltinMethodTypeBuiltinFunctionType과 같은 타입입니다.

MethodWrapperType(예: object().__str__의 타입)은 제네릭 매직 메서드에 유용합니다.

함수 객체는 런타임에 첨자 지정을 허용하도록 __getitem__을 구현하고, __origin__을 호출 가능 객체로, __args__를 전달된 타입으로 설정한 types.GenericAlias의 인스턴스를 반환해야 합니다.

타입 검사기는 함수에 대한 첨자 표기를 지원하고, 함수 첨자 표기에 전달되는 매개변수가 제네릭 호출 가능 객체 클래스와 동일한 규칙을 따라야 함을 이해해야 합니다.

__orig_class__ 설정

현재 __orig_class__GenericAlias.__call__에서 호출된 클래스를 생성한 GenericAlias의 인스턴스로 설정되는 속성입니다. 예를 들어 다음과 같습니다.

class Foo[T]: ...

assert Foo[int]().__orig_class__ == Foo[int]

현재 __orig_class__는 무조건 설정되지만, 생성된 인스턴스에서 발생할 수 있는 소거를 방지하려면 __origin__이 함수 객체의 인스턴스인 경우 이 속성을 설정하지 않아야 합니다.

이 변경이 없으면 다음 코드 조각은 런타임에 실패합니다. __orig_class__Foo[int]가 아니라 bar[str]가 되기 때문입니다.

def bar[U]():
    return Foo[int]()

assert bar[str]().__orig_class__  == Foo[int]

@typing.overload와의 상호 작용

이 PEP를 통해 타입 검사기는 타입 변수에 기반한 오버로딩을 수행할 수 있습니다.

@overload
def serializer_for[T: str]() -> StringSerializer: ...
@overload
def serializer_for[T: list]() -> ListSerializer: ...

def serializer_for():
    ...

오버로드를 결정하려면 다른 단계에 앞서 새로운 단계가 필요합니다. 이 단계에서 결정자는 첨자 지정이 성공할 수 있는 오버로드만 일치시킵니다.

@overload
def make[*Ts]() -> float: ...
@overload
def make[T]() -> int: ...

make[int]  # matches first and second overload
make[int, str]  # matches only first

TypeVarTuples로 매개변수화된 함수

현재 타입 검사기는 제네릭 매개변수에서 여러 TypeVarTuples를 사용하는 것을 허용하지 않지만, 다음과 같은 함수를 작성하는 것은 현재 유효합니다.

def foo[*T, *U](bar: Bar[*T], baz: Baz[*U]): ...
def spam[*T](bar: Bar[*T]): ...

이 PEP는 PEP 646에 정의된 것과 같은 이유로 foo와 같은 함수에 대한 첨자 지정을 허용하지 않습니다. 현재 구문으로는 타입 변수를 모호하지 않게 결정할 수 없기 때문입니다.

foo[int, str, bool, complex](Bar(), Baz())  # Invalid: cannot determine which parameters are passed to *T and *U. Explicitly parameterise the instances individually
spam[int, str, bool, complex](Bar())        # OK

바인딩 규칙

메서드 첨자 지정은 (classmethodsstaticmethods를 포함하여) 둘러싼 클래스의 타입 매개변수가 아니라 해당 함수의 타입 매개변수만 허용해야 합니다. 첨자 지정은 PEP 696에 지정된 규칙을 따라야 하며, 메서드는 속성에 접근할 때 타입 매개변수를 바인딩해야 합니다.

class C[T]:
    def method[U](self, x: T, y: U): ...
    @classmethod
    def cls[U](cls, x: T, y: U): ...

C[int].method[str](0, "")  # OK
C[int].cls[str](0, "")     # OK
C.cls[int, str](0, "")     # Invalid: too many type parameters
C.cls[str](0, "")          # OK, U will be matched to str

하위 호환성

현재 이 클래스들은 서브클래싱할 수 없으므로, 이미 __getitem__을 구현하는 클래스와 관련된 하위 호환성 문제가 없습니다.

참조 구현

제안된 런타임 변경 사항은 여기에서 확인할 수 있습니다 https://github.com/Gobot1234/cpython/tree/function-subscript

감사의 말

이 PEP에 의견을 보내 주신 Alex Waygood과 Jelle Zijlstra, 그리고 동기를 부여하는 몇 가지 예를 제공해 주신 Guido에게 감사드립니다.