Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 764 – 内联类型字典

作者:
Victorien Plot <contact at vctrn.dev>
发起人:
Eric Traut <erictr at microsoft.com>
讨论至:
Discourse 帖子
状态:
草案
类型:
标准跟踪
主题:
类型标注
创建日期:
2024年10月25日
Python 版本:
3.15
发布历史:
2025年1月29日

目录

摘要

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,但任何类型修饰符都可以用于单个字段

Movie = TypedDict[{'name': NotRequired[str], 'year': ReadOnly[int]}]

内联类型字典默认是完整的,这意味着所有键都必须存在。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 生产规则将更新以包含内联语法

new-type_expression ::=  type_expression
                         | <TypedDict> '[' '{' (string: ':' annotation_expression ',')* '}' ']'
                               (where string is any string literal)

运行时行为

创建一个内联类型字典会产生一个新的类,因此 T1T2 属于同一类型

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}): ...

然而,目前在这样的上下文中,由于各种原因(处理开销大,评估不标准化),函数调用表达式是不受支持的。

这也需要一个有时不相关的名称。

使用带单个类型参数的 dicttyping.Dict

我们可以重用带单个类型参数的dicttyping.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 引入了联合类型。这两个特性都使用了按位或 (|) 运算符,使得以下用例不兼容,尤其是在运行时自省方面

# 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 728 需要首先处理,以便本 PEP 可以相应更新。


来源: https://github.com/python/peps/blob/main/peps/pep-0764.rst

最后修改: 2025-05-06 22:54:17 GMT