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, tuple, dict 등과 같은 일반적인 생성자에서 특히 유용합니다.

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]

단형화와 구체화

이 제안은 단형화와 구체화된 타입으로 가는 길도 열어 줍니다.

이를 통해 그동안 여러 차례 요청되어 온 것으로 알려진 기능을 제공할 수 있습니다.

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

이러한 기능의 문법은 대략 다음과 같은 형태가 될 수 있습니다:

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

assert foo[int]() is int

근거

이 제안은 기존 클래스 문법의 자연스러운 연장처럼 이미 보이고 느껴지는 문법을 허용함으로써 타입 시스템의 일관성을 향상합니다.

이 문법이 채택되면 PEP 747의 TypeForm에 대해 배워야 할 필요성이 줄어들고, 안전하게 타입이 지정된 Python의 장황함과 인지 부하도 줄어듭니다.

명세

이 PEP에서 “함수 객체”는 FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType 및 MethodWrapperType을 가리키는 데 사용됩니다.

MethodType의 경우 다음과 같이 작성할 수 있어야 하며:

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

Foo().make_list[int]()

FunctionType과 유사하게 동작해야 합니다.

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

BuiltinMethodType은 BuiltinFunctionType과 같은 타입입니다.

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

TypeVarTuple로 매개변수화된 함수

현재 타입 검사기는 제네릭 매개변수에 여러 개의 TypeVarTuple을 사용하는 것을 허용하지 않습니다. 그러나 현재 다음과 같은 함수를 작성하는 것은 유효합니다:

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

이 PEP는 foo와 같은 함수의 서브스크립션을 허용하지 않습니다. PEP 646에 정의된 것과 같은 이유로, 현재 문법으로는 타입 변수를 모호함 없이 해석할 수 없기 때문입니다.

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

바인딩 규칙

메서드 서브스크립션(classmethods와 staticmethods를 포함)은 해당 함수의 타입 매개변수만 허용해야 하며, 둘러싼 클래스의 타입 매개변수는 허용하지 않아야 합니다. 서브스크립션은 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에게 감사드립니다.