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

Python 개선 제안 한국어 번역

PEP 585 – 표준 컬렉션의 타입 힌트 제너릭

Author:
Łukasz Langa <lukasz at python.org>
Discussions-To:
Typing-SIG list
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
03-Mar-2019
Python-Version:
3.9
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. The up-to-date, canonical documentation can now be found at Generic Alias Type__class_getitem__() 문서.

×

See PEP 1 for how to propose changes.

초록

PEP 484, 526, 544, 560 및 563에 정의된 정적 타이핑은 기존 Python 런타임 위에 점진적으로 구축되었으며 기존 구문과 런타임 동작의 제약을 받았습니다. 이로 인해 제너릭 때문에 typing 모듈에 중복된 컬렉션 계층이 존재하게 되었습니다(예를 들어 typing.List와 내장 list가 있습니다).

이 PEP는 현재 typing 모듈에서 사용할 수 있는 모든 표준 컬렉션에 제너릭 구문을 지원하도록 제안합니다.

근거 및 목표

이 변경으로 typing 모듈의 병렬 타입 계층이 필요하지 않게 되어 사용자가 프로그램에 어노테이션을 작성하기 쉬워지고 교사가 Python을 가르치기 쉬워집니다.

용어

Generic (명사) – 매개변수화할 수 있는 타입이며, 일반적으로 컨테이너입니다. parametric type 또는 generic type이라고도 합니다. 예: dict.

parameterized generic – 컨테이너 요소에 예상되는 타입이 제공된 제너릭의 특정 인스턴스입니다. parameterized type이라고도 합니다. 예: dict[str, int].

하위 호환성

타입 검사기와 린터를 포함한 도구는 표준 컬렉션을 제너릭으로 인식하도록 조정되어야 합니다.

소스 수준에서는 새로 설명된 기능을 사용하려면 Python 3.9가 필요합니다. 타입 어노테이션으로 제한된 사용 사례에서는 “annotations” future-import를 사용하는 Python 파일이 표준 컬렉션을 비롯한 내장 타입을 매개변수화할 수 있습니다(Python 3.7부터 사용할 수 있습니다). 다시 말해, 이는 외부 도구가 이러한 사용이 유효하다는 점을 이해하는지에 달려 있습니다.

구현

Python 3.7부터 from __future__ import annotations를 사용하면 함수 및 변수 어노테이션에서 표준 컬렉션을 직접 매개변수화할 수 있습니다. 예:

from __future__ import annotations

def find(haystack: dict[str, list[int]]) -> int:
    ...

외부 도구인 Mypy가 표준 컬렉션을 제네릭으로 인식하지 못하므로 PEP 585 이전에는 이 구문의 유용성이 제한적입니다. 또한 타입 별칭이나 캐스팅과 같은 typing의 일부 기능을 사용하려면 어노테이션 외부의 런타임 컨텍스트에 타입을 배치해야 합니다. 이러한 기능은 타입 어노테이션보다 상대적으로 덜 일반적이지만, 모든 컨텍스트에서 동일한 타입 구문을 사용할 수 있도록 하는 것이 중요합니다. 따라서 Python 3.9부터 다음 컬렉션은 __class_getitem__()을 사용하여 포함된 타입을 매개변수화하는 제너릭이 됩니다.

  • tuple # typing.Tuple
  • list # typing.List
  • dict # typing.Dict
  • set # typing.Set
  • frozenset # typing.FrozenSet
  • type # typing.Type
  • collections.deque
  • collections.defaultdict
  • collections.OrderedDict
  • collections.Counter
  • collections.ChainMap
  • collections.abc.Awaitable
  • collections.abc.Coroutine
  • collections.abc.AsyncIterable
  • collections.abc.AsyncIterator
  • collections.abc.AsyncGenerator
  • collections.abc.Iterable
  • collections.abc.Iterator
  • collections.abc.Generator
  • collections.abc.Reversible
  • collections.abc.Container
  • collections.abc.Collection
  • collections.abc.Callable
  • collections.abc.Set # typing.AbstractSet
  • collections.abc.MutableSet
  • collections.abc.Mapping
  • collections.abc.MutableMapping
  • collections.abc.Sequence
  • collections.abc.MutableSequence
  • collections.abc.ByteString
  • collections.abc.MappingView
  • collections.abc.KeysView
  • collections.abc.ItemsView
  • collections.abc.ValuesView
  • contextlib.AbstractContextManager # typing.ContextManager
  • contextlib.AbstractAsyncContextManager # typing.AsyncContextManager
  • re.Pattern # typing.Pattern, typing.re.Pattern
  • re.Match # typing.Match, typing.re.Match

typing에서 가져오는 것은 사용 중단 예정입니다. 관련 PEP 563 및 타이핑이 런타임에 미치는 영향을 최소화하려는 취지에 따라, 이 사용 중단에서는 DeprecationWarning이 발생하지 않습니다. 대신, 검사 대상 프로그램의 대상 버전이 Python 3.9 이상으로 지정된 경우 타입 검사기는 이러한 사용 중단 예정 기능의 사용에 대해 경고할 수 있습니다. 이러한 경고를 프로젝트 전체 기준으로 억제할 수 있도록 설정하는 것이 좋습니다.

사용 중단 예정인 기능은 결국 typing 모듈에서 제거될 수 있습니다. 제거는 2025년 10월로 예정된 Python 3.9의 수명 종료 이후에만 이루어집니다.

제네릭의 매개변수는 런타임에 사용할 수 있습니다.

제네릭 타입을 런타임에 보존하면 API 생성이나 런타임 타입 검사에 사용할 수 있는 타입 인트로스펙션이 가능해집니다. 이러한 사용은 이미 실제 환경에 존재합니다.

현재의 typing 모듈과 마찬가지로, 이전 절에 나열된 매개변수화된 제네릭 타입은 모두 런타임에 타입 매개변수를 보존합니다.:

>>> list[str]
list[str]
>>> tuple[int, ...]
tuple[int, ...]
>>> ChainMap[str, list[str]]
collections.ChainMap[str, list[str]]

이는 모든 메서드 호출과 특성 접근을 원본 타입으로 전달하는 얇은 프록시 타입을 사용하여 구현되며, 다음과 같은 예외가 있습니다.

  • __repr__은 매개변수화된 타입을 표시합니다.
  • __origin__ 특성은 매개변수화되지 않은 제네릭 클래스를 가리킵니다.
  • __args__특성은 튜플입니다(길이는 1) 원래 __class_getitem__에 전달된 제네릭 타입의
  • __parameters__특성은 __args__에서 발견된 고유한 타입 변수의 튜플을 지연 계산한 것입니다(비어 있을 수도 있음).
  • __getitem__dict[str][str]과 같은 실수를 방지하기 위해 예외를 발생시킵니다. 그러나 예를 들어 dict[str, T][int]을 허용하며, 이 경우 dict[str, int]을 반환합니다.

이 설계에서는 다음과 같은 매개변수화된 컬렉션의 인스턴스를 생성할 수 있습니다.:

>>> l = list[str]()
[]
>>> list is list[str]
False
>>> list == list[str]
False
>>> list[str] == list[str]
True
>>> list[str] == list[int]
False
>>> isinstance([1, 2, 3], list[str])
TypeError: isinstance() arg 2 cannot be a parameterized generic
>>> issubclass(list, list[str])
TypeError: issubclass() arg 2 cannot be a parameterized generic
>>> isinstance(list[str], types.GenericAlias)
True

기본 타입으로 생성한 객체와 매개변수화된 타입으로 생성한 객체는 정확히 동일합니다. 매개변수화된 타입으로 생성한 인스턴스에는 제네릭 매개변수가 보존되지 않습니다. 즉, 객체 생성 중에 제네릭 타입이 타입 매개변수를 삭제합니다.

이에 따른 한 가지 중요한 결과는 인터프리터가 매개변수화된 타입으로 생성된 컬렉션에 대한 연산의 타입을 검사하려고 하지 않는다는 것입니다. 이는 다음 두 경우 사이에 대칭성을 제공합니다.:

l: list[str] = []

및:

l = list[str]()

Python 코드에서 프록시 타입에 접근할 수 있도록 types 모듈에서 GenericAlias라는 이름으로 내보냅니다.

GenericAlias 인스턴스를 피클링하거나 얕게 또는 깊게 복사해도 타입, 원본, 특성 및 매개변수가 보존됩니다.

하위 호환성

향후 표준 컬렉션은 동일한 동작을 구현해야 합니다.

참조 구현

개념 증명 또는 프로토타입 implementation이 존재합니다.

거부된 대안

아무것도 하지 않습니다.

현재 상태를 유지하면 Python 프로그래머는 표준 컬렉션에 대해 typing 모듈에서 가져오는 항목을 관리해야 하므로, 가장 단순한 어노테이션을 제외한 모든 어노테이션을 유지 관리하기가 번거로워집니다. 병렬 타입이 존재한다는 점은 초보자에게 혼란을 줍니다(listList가 모두 있는 이유는 무엇입니까?).

위의 문제는 런타임 기능과 제네릭 타입 어노테이션으로 사용할 수 있는 기능을 공유하는 사용자 정의 제네릭 클래스에는 존재하지 않습니다. 사용자 클래스에서 표준 컬렉션을 타입 힌팅에 사용하기 어렵게 만든 것은 타입 기능의 도입과 사용성을 저해했습니다.

제네릭 타입 삭제

나열된 표준 컬렉션에 __class_getitem__을 제네릭 타입을 보존하지 않는 방식으로 구현하는 것이 더 쉬울 수 있습니다. 즉,:

>>> list[str]
<class 'list'>
>>> tuple[int, ...]
<class 'tuple'>
>>> collections.ChainMap[str, list[str]]
<class 'collections.ChainMap'>

이는 하위 호환성을 깨뜨리므로 문제가 됩니다. typing 모듈에서 이러한 타입에 해당하는 현재 타입은 제네릭 타입을 보존합니다.:

>>> from typing import List, Tuple, ChainMap
>>> List[str]
typing.List[str]
>>> Tuple[int, ...]
typing.Tuple[int, ...]
>>> ChainMap[str, List[str]]
typing.ChainMap[str, typing.List[str]]

“Implementation” 절에서 언급했듯이, 런타임에 제네릭 타입을 보존하면 API 생성이나 런타임 타입 검사에 사용할 수 있는 타입의 런타임 인트로스펙션이 가능해집니다. 이러한 사용 방식은 이미 실제 환경에 존재합니다.

또한 첨자 연산을 항등 함수로 구현하면 Python이 초보자에게 덜 친숙해집니다. 예를 들어 사용자가 실수로 리스트 객체 대신 리스트 타입을 함수에 전달하고, 해당 함수가 전달받은 객체에 첨자 연산을 수행한다고 하면, 이제 코드는 오류를 발생시키지 않습니다.

현재:

>>> l = list
>>> l[-1]
TypeError: 'type' object is not subscriptable

__class_getitem__을 항등 함수로 사용하는 경우:

>>> l = list
>>> l[-1]
list

여기서 첨자 연산이 성공하면 사용자를 혼란스럽게 하는 예외가 나중에 발생할 가능성이 큽니다.

매개변수화된 타입의 인스턴스화 금지

__origin____args__를 보존하는 프록시 타입은 주로 런타임 인트로스펙션에 유용하므로, 매개변수화된 타입의 인스턴스화를 금지했을 수도 있습니다.

실제로 매개변수화된 타입의 인스턴스화를 금지하는 것이 오늘날 typing 모듈이 내장 컬렉션과 병렬 관계에 있는 타입에 대해 수행하는 방식입니다(다른 매개변수화된 타입의 인스턴스화는 허용됩니다).

이러한 결정을 내린 본래 이유는 해당 내장 컬렉션에 사용할 수 있는 특수 구문에 비해 객체 생성이 최대 100배 느려지는 불필요한 매개변수화를 억제하기 위해서였습니다.

이러한 근거만으로는 내장 타입을 예외적으로 처리하는 것을 허용하기에 충분하지 않습니다. 표준 라이브러리의 컬렉션에 대응하는 타입을 포함하여 다른 모든 매개변수화된 타입은 인스턴스화할 수 있습니다. 또한 Python에서는 list()를 사용하여 리스트를 인스턴스화할 수 있으며, 일부 내장 컬렉션은 인스턴스화를 위한 특수 구문을 제공하지 않습니다.

제네릭을 무시하고 isinstance(obj, list[str])가 검사를 수행하도록 만들기

이 PEP의 이전 버전에서는 list[str]과 같은 매개변수화된 제네릭을 isinstance()issubclass()에서 list와 같은 매개변수화되지 않은 변형과 동등하게 취급할 것을 제안했습니다. 이는 list[str]()이 일반 리스트를 생성하는 방식과 대칭을 이룹니다.

이 설계는 매개변수화된 제네릭을 사용한 isinstance()issubclass() 검사가 요소별 런타임 타입 검사처럼 보이게 되므로 거부되었습니다. 이러한 검사의 결과는 다음과 같이 예상과 다를 수 있습니다.:

>>> isinstance([1, 2, 3], list[str])
True

객체가 제공된 제네릭 타입과 일치하지 않지만, isinstance()은 객체가 리스트인지 여부만 확인하므로 여전히 True를 반환합니다.

라이브러리가 매개변수화된 제네릭을 받고 기본 타입을 사용하여 isinstance()검사를 수행하려는 경우, 매개변수화된 제네릭의 __origin__ 특성을 사용하여 해당 타입을 가져올 수 있습니다.

isinstance(obj, list[str])가 런타임 타입 검사를 수행하도록 만들기

이 기능을 사용하려면 컬렉션을 순회해야 하며, 일부 컬렉션에서는 이 작업이 파괴적입니다. 이 기능은 유용했겠지만, 복합 타입, 중첩 타입 검사, 타입 변수, 문자열 정방향 참조 등을 처리하는 타입 검사기를 Python 내부에 구현하는 것은 이 PEP의 범위를 벗어납니다.

GenericAlias대신 GenericType으로 타입 이름 짓기

이 타입에 대해 다른 이름을 고려했지만, GenericAlias가 더 낫다고 결정했습니다 – 이들은 실제 타입이 아니라, 추가 메타데이터가 첨부된 해당 컨테이너 타입의 별칭입니다.

초기 초안에 대한 참고

이 PEP의 초기 버전은 표준 컬렉션의 제네릭을 넘어서는 문제들을 논의했습니다. 명확성을 위해 그 관련 없는 주제들은 제거되었습니다.

감사의 말

Python에 대한 작업과, 특히 이 PEP의 구현에 대해 Guido van Rossum에게 감사드립니다.