PEP 764 – 인라인 타입 지정 딕셔너리
- Author:
- Victorien Plot <contact at vctrn.dev>
- Sponsor:
- Eric Traut <erictr at microsoft.com>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Topic:
- Typing
- Created:
- 25-Oct-2024
- Python-Version:
- 3.15
- Post-History:
- 29-Jan-2025
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
PEP 589는 타입 지정 딕셔너리를 만들기 위한 클래스 기반 문법과 함수형 문법을 정의합니다. 두 경우 모두 클래스를 정의하거나 값에 대입해야 합니다. 이 때문에 상황에 따라 불필요한 보일러플레이트가 늘어날 수 있으며, 타입 지정 딕셔너리를 한 번만 사용한다면 더욱 그렇습니다.
이 PEP는 TypedDict 타입에 첨자를 붙이는 새로운 인라인 문법을 추가할 것을 제안합니다.:
from typing import TypedDict
def get_movie() -> TypedDict[{'name': str, 'year': int}]:
return {
'name': 'Blade Runner',
'year': 1982,
}
동기
Python 딕셔너리는 이 언어의 필수적인 자료 구조입니다. 함수가 구조화된 데이터를 반환하거나 받을 때 딕셔너리를 사용하는 경우가 많습니다. 그러나 TypedDict 클래스를 정의하는 일은 번거로울 수 있습니다:
- 타입 지정 딕셔너리에는 이름이 필요하지만, 그 이름이 별 의미가 없을 수도 있습니다.
- 중첩된 딕셔너리에는 두 개 이상의 클래스 정의가 필요합니다.
중첩된 구조화 데이터를 반환하는 간단한 함수를 예로 들어 보겠습니다.:
from typing import TypedDict
class ProductionCompany(TypedDict):
name: str
location: str
class Movie(TypedDict):
name: str
year: int
production: ProductionCompany
def get_movie() -> Movie:
return {
'name': 'Blade Runner',
'year': 1982,
'production': {
'name': 'Warner Bros.',
'location': 'California',
}
}
근거
새로운 인라인 문법을 사용하면 이러한 문제를 해결할 수 있습니다.:
def get_movie() -> TypedDict[{'name': str, 'year': int, 'production': TypedDict[{'name': str, 'location': str}]}]:
...
함수형 문법이나 클래스 기반 문법을 사용할 수도 있으므로 유용성은 덜하지만, 인라인 타입 지정 딕셔너리를 별칭으로서 변수에 대입할 수도 있습니다.:
InlineTD = TypedDict[{'name': str}]
def get_movie() -> InlineTD:
...
명세
TypedDict 특수 형식에 첨자를 붙일 수 있게 되며, 이 특수 형식은 반드시 dict여야 하는 단일 타입 인자를 받습니다. 이 인자는 함수형 문법과 같은 의미 체계를 따릅니다(딕셔너리 키는 필드 이름을 나타내는 문자열이고, 값은 유효한 어노테이션 표현식입니다). 중괄호 생성자 안에 key: value 쌍을 쉼표로 구분해 나열한 형태({k: <type>})만 허용되며, 이를 타입 인자로 직접 지정해야 합니다(즉, 앞서 dict 인스턴스를 대입해 둔 변수는 사용할 수 없습니다).
인라인 타입 지정 딕셔너리는 특정한 이름이 없다는 의미에서 익명이라고 부를 수 있습니다(런타임 동작 섹션을 참고하십시오).
중첩된 인라인 딕셔너리를 정의할 수 있습니다.:
Movie = TypedDict[{'name': str, 'production': TypedDict[{'location': str}]}]
# Note that the following is invalid as per the updated `type_expression` grammar:
Movie = TypedDict[{'name': str, 'production': {'location': str}}]
total 같은 클래스 인자는 지정할 수 없지만, 개별 필드에는 어떤 type qualifier든 사용할 수 있습니다.:
Movie = TypedDict[{'name': NotRequired[str], 'year': ReadOnly[int]}]
인라인 타입 지정 딕셔너리는 암묵적으로 total이며, 이는 모든 키가 반드시 존재해야 한다는 뜻입니다. 따라서 Required 타입 한정자를 사용하는 것은 불필요합니다.
타입 변수가 바깥 스코프에 바인딩되어 있다면 인라인 타입 지정 딕셔너리에서 타입 변수를 사용할 수 있습니다.:
class C[T]:
inline_td: TypedDict[{'name': T}] # OK, `T` is scoped to the class `C`.
reveal_type(C[int]().inline_td['name']) # Revealed type is 'int'
def fn[T](arg: T) -> TypedDict[{'name': T}]: ... # OK: `T` is scoped to the function `fn`.
reveal_type(fn('a')['name']) # Revealed type is 'str'
type InlineTD[T] = TypedDict[{'name': T}] # OK, `T` is scoped to the type alias.
T = TypeVar('T')
InlineTD = TypedDict[{'name': T}] # OK, same as the previous type alias, but using the old-style syntax.
def func():
InlineTD = TypedDict[{'name': T}] # Not OK: `T` refers to a type variable that is not bound to the scope of `func`.
인라인 타입 지정 딕셔너리는 확장할 수 있습니다.:
InlineTD = TypedDict[{'a': int}]
class SubTD(InlineTD):
pass
타이핑 명세 변경 사항
인라인 타입 지정 딕셔너리는 새로운 종류의 type expression을 추가합니다. 따라서 type_expression 토큰 생성 규칙은 인라인 문법을 포함하도록 갱신될 것입니다:
new-type_expression ::=type_expression| <TypedDict> '[' '{' (string: ':'annotation_expression',')* '}' ']' (where string is any string literal)
런타임 동작
인라인 타입 지정 딕셔너리를 만들면 새로운 클래스가 생성되므로, T1과 T2는 같은 타입입니다.:
from typing import TypedDict
T1 = TypedDict('T1', {'a': int})
T2 = TypedDict[{'a': int}]
인라인 타입 딕셔너리는 익명으로 쓰이도록 의도되었으므로, 그 __name__ 속성은 <inline TypedDict> 문자열 리터럴로 설정됩니다. 향후에는 이름 있는 클래스와 구별할 수 있도록 명시적인 클래스 속성이 추가될 수 있습니다.
TypedDict는 클래스로 문서화되어 있지만, 그것이 정의되는 방식은 구현 세부 사항입니다. TypedDict를 서브스크립트 가능하게 만들 수 있도록 구현을 조정해야 합니다.
하위 호환성
이 PEP는 하위 호환성을 깨뜨리는 변경 사항을 전혀 도입하지 않습니다.
보안 영향
이 PEP로 인해 발생하는 알려진 보안상의 영향은 없습니다.
교육 방법
새로운 인라인 문법은 typing 모듈 문서와 타이핑 명세 양쪽에 문서화될 것입니다.
복잡한 딕셔너리 구조를 사용할 때 모든 것을 한 줄에 정의하면 가독성이 떨어질 수 있습니다. 코드 포매터는 인라인 타입 딕셔너리를 여러 줄에 걸쳐 포매팅하여 도움을 줄 수 있습니다:
def edit_movie(
movie: TypedDict[{
'name': str,
'year': int,
'production': TypedDict[{
'location': str,
}],
}],
) -> None:
...
참조 구현
Mypy는 비슷한 문법을 실험적 기능으로 지원합니다:
def test_values() -> {"int": int, "str": str}:
return {"int": 42, "str": "test"}
이 PEP에 대한 지원은 이 풀 리퀘스트에서 추가되었습니다.
Pyright는 버전 1.1.387에서 새 문법에 대한 지원을 추가했습니다.
런타임 구현
필요한 변경 사항은 이 풀 리퀘스트를 통해 typing_extensions에 처음 구현되었습니다.
기각된 아이디어
어노테이션에서 함수형 문법 사용하기
대안인 함수형 문법을 어노테이션으로 직접 사용할 수 있습니다:
def get_movie() -> TypedDict('Movie', {'title': str}): ...
그러나 호출 표현식은 여러 가지 이유(처리 비용이 크고, 그 평가 방식이 표준화되어 있지 않음)로 현재 그러한 맥락에서 지원되지 않습니다.
또한 이 방식은 때로는 의미가 없는 이름을 요구합니다.
단일 타입 인자와 함께 dict 또는 typing.Dict 사용하기
같은 개념을 표현하기 위해 dict 또는 typing.Dict를 단일 타입 인자와 함께 재사용할 수 있습니다:
def get_movie() -> dict[{'title': str}]: ...
이렇게 하면 typing에서 TypedDict를 임포트할 필요가 없어지지만, 이 해결책에는 몇 가지 단점이 있습니다:
- 타입 검사기에게
dict는 두 개의 타입 변수를 가진 일반 클래스입니다. 매개변수화 오버로드를 표현할 방법이 없으므로,dict를 단일 타입 인자로 매개변수화할 수 있게 하려면 타입 검사기가 이를 특별히 처리해야 합니다. 반면에TypedDict는 이미 특수 형식입니다. - 향후 작업으로 인라인 타입 딕셔너리의 기능이 확장되더라도,
dict와 심볼을 공유함으로써 생기는 영향을 걱정할 필요가 없습니다. typing.Dict는 PEP 585에 의해 사용 중단되었습니다(제거가 계획되어 있지는 않습니다). 이를 새로운 타이핑 기능에 사용하면 사용자에게 혼란을 줄 것입니다(또한 코드 린터의 변경도 필요할 것입니다).
간단한 딕셔너리 사용하기
TypedDict 클래스에 첨자를 사용하는 대신, 일반 딕셔너리를 어노테이션으로 사용할 수 있습니다:
def get_movie() -> {'title': str}: ...
그러나 PEP 584는 딕셔너리에 합집합 연산자를 추가했고, PEP 604는 유니언 타입을 도입했습니다. 두 기능 모두 비트 단위 or (|) 연산자를 사용하므로, 다음 사용 사례는 특히 런타임 인트로스펙션에서 호환되지 않습니다:
# Dictionaries are merged:
def fn() -> {'a': int} | {'b': str}: ...
# Raises a type error at runtime:
def fn() -> {'a': int} | int: ...
다른 타입 딕셔너리 확장하기
다른 타입 딕셔너리를 확장할 수 있도록 여러 문법을 사용할 수 있습니다:
InlineBase = TypedDict[{'a': int}]
Inline = TypedDict[InlineBase, {'b': int}]
# or, by providing a slice:
Inline = TypedDict[{'b': int} : (InlineBase,)]
인라인 타입 딕셔너리는 기존 문법의 일부만 지원하도록 의도되었으므로, 늘어나는 복잡성을 고려하면 이 확장 메커니즘을 추가할 만큼 설득력이 충분하지 않습니다.
타입 시스템에 교차 타입이 추가된다면, 이 사용 사례를 다룰 수 있을 것입니다.
미해결 문제
인라인 타입 딕셔너리와 추가 항목
PEP 728은 닫힌 타입 딕셔너리라는 개념을 도입합니다. 이 PEP가 채택된다면, 인라인 타입 딕셔너리는 기본적으로 닫힌 상태가 됩니다. 즉, 이 PEP를 그에 맞게 갱신할 수 있도록 PEP 728을 먼저 처리해야 합니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.