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

Python 增强提案

PEP 589 – TypedDict: 具有固定键集的字典的类型提示

作者:
Jukka Lehtosalo <jukka.lehtosalo at iki.fi>
发起人:
Guido van Rossum <guido at python.org>
BDFL 委托
Guido van Rossum <guido at python.org>
讨论至:
Typing-SIG 邮件列表
状态:
最终版
类型:
标准跟踪
主题:
类型标注
创建日期:
2019年3月20日
Python 版本:
3.8
发布历史:

决议:
Typing-SIG 消息

目录

重要

本 PEP 是一份历史文档:请参阅类型化字典typing.TypedDict以获取最新规范和文档。规范的类型化规范在类型化规范站点维护;运行时类型化行为在 CPython 文档中描述。

×

有关如何提议更改类型规范的信息,请参阅类型规范更新过程

摘要

PEP 484定义了统一字典的类型Dict[K, V],其中每个值都具有相同的类型,并且支持任意键值。它没有正确支持常见模式,即字典值的类型取决于键的字符串值。本 PEP 提出了一个类型构造函数typing.TypedDict,以支持字典对象具有一组特定字符串键,每个键具有特定类型值的用例。

这是一个PEP 484不允许我们满意地进行注解的示例

movie = {'name': 'Blade Runner',
         'year': 1982}

本 PEP 建议添加一个新的类型构造函数,名为TypedDict,以允许精确地表示movie的类型

from typing import TypedDict

class Movie(TypedDict):
    name: str
    year: int

现在类型检查器应该接受这段代码

movie: Movie = {'name': 'Blade Runner',
                'year': 1982}

动机

在 Python 程序中,使用(可能嵌套的)带有字符串键的字典(而不是用户定义的类)来表示对象或结构化数据是一种常见模式。表示 JSON 对象也许是典型的用例,而且这非常流行,以至于 Python 内置了 JSON 库。本 PEP 提出了一种允许更有效地对这类代码进行类型检查的方法。

更一般地说,仅使用 Python 原生类型(如字典、字符串和列表)来表示纯数据对象具有一定的吸引力。即使不使用 JSON,它们也易于序列化和反序列化。它们无需额外努力即可轻松支持各种有用的操作,包括美观打印(通过str()pprint模块)、迭代和相等性比较。

PEP 484未能妥善支持上述用例。让我们考虑一个字典对象,它恰好有两个有效的字符串键,'name'的值类型为str'year'的值类型为intPEP 484类型Dict[str, Any]是合适的,但它过于宽松,因为可以使用任意字符串键,并且任意值都有效。同样,Dict[str, Union[str, int]]也过于通用,因为键'name'的值可以是int,并且允许任意字符串键。此外,诸如d['name']的下标表达式(假设d是这种类型的字典)的类型将是Union[str, int],这过于宽泛。

数据类是解决此用例的最新替代方案,但在数据类可用之前,仍有大量现有代码,特别是在大型现有代码库中,类型提示和检查已被证明是有益的。与字典对象不同,数据类不直接支持 JSON 序列化,尽管有一个第三方包实现了它[1]

规范

TypedDict 类型表示具有一组特定字符串键的字典对象,以及每个有效键的特定值类型。每个字符串键可以是必需的(它必须存在)或非必需的(它不需要存在)。

本 PEP 提出了两种定义 TypedDict 类型的方法。第一种使用基于类的语法。第二种是替代的基于赋值的语法,为了向后兼容性而提供,以允许该功能可以移植到旧版本的 Python。其原理类似于PEP 484为何支持 Python 2.7 的基于注释的注解语法:类型提示对于大型现有代码库特别有用,而这些代码库通常需要在旧版本的 Python 上运行。这两个语法选项与typing.NamedTuple支持的语法变体并行。其他提议的功能包括 TypedDict 继承和完整性(指定键是否必需)。

本 PEP 还概述了类型检查器如何支持涉及 TypedDict 对象的类型检查操作。与PEP 484类似,此讨论故意有些模糊,以允许尝试各种不同的类型检查方法。特别是,类型兼容性应基于结构兼容性:更具体的 TypedDict 类型可以与更小(更通用)的 TypedDict 类型兼容。

基于类的语法

可以使用基于类的定义语法来定义 TypedDict 类型,其中typing.TypedDict是唯一的基类

from typing import TypedDict

class Movie(TypedDict):
    name: str
    year: int

Movie是一个 TypedDict 类型,包含两个项:'name'(类型为str)和'year'(类型为int)。

类型检查器应验证基于类的 TypedDict 定义的主体符合以下规则

  • 类体应只包含形式为key: value_type的项定义行,可选地前面有一个文档字符串。项定义的语法与属性注解相同,但不能有初始化器,并且键名实际上指的是键的字符串值而不是属性名。
  • 类型注释不能与基于类的语法一起使用,以与基于类的NamedTuple语法保持一致。(请注意,支持类型注释不足以向后兼容 Python 2.7,因为类定义可能具有total关键字参数,如下所述,这在 Python 2.7 中不是有效语法。)相反,本 PEP 提供了一个替代的、基于赋值的语法以实现向后兼容性,这将在替代语法中讨论。
  • 字符串字面量前向引用在值类型中是有效的。
  • 不允许使用方法,因为 TypedDict 对象的运行时类型将始终只是dict(它从不是dict的子类)。
  • 不允许指定元类。

可以通过在主体中只包含pass来创建空 TypedDict(如果有文档字符串,则可以省略pass

class EmptyDict(TypedDict):
    pass

使用 TypedDict 类型

以下是类型Movie如何使用的示例

movie: Movie = {'name': 'Blade Runner',
                'year': 1982}

通常需要显式的Movie类型注解,否则类型检查器可能会假定为普通的字典类型,以实现向后兼容性。当类型检查器可以推断出构造的字典对象应该是一个 TypedDict 时,可以省略显式注解。一个典型的例子是作为函数参数的字典对象。在此示例中,类型检查器应推断字典参数应被理解为 TypedDict

def record_movie(movie: Movie) -> None: ...

record_movie({'name': 'Blade Runner', 'year': 1982})

类型检查器应将字典显示视为 TypedDict 的另一个示例是在赋值给先前声明的 TypedDict 类型的变量时

movie: Movie
...
movie = {'name': 'Blade Runner', 'year': 1982}

movie的操作可以由静态类型检查器检查

movie['director'] = 'Ridley Scott'  # Error: invalid key 'director'
movie['year'] = '1982'  # Error: invalid value type ("int" expected)

下面的代码应该被拒绝,因为'title'不是一个有效的键,并且'name'键缺失

movie2: Movie = {'title': 'Blade Runner',
                 'year': 1982}

创建的 TypedDict 类型对象不是一个真正的类对象。以下是类型检查器预期允许的类型对象的唯一用法

  • 它可以用在类型注解中以及任何任意类型提示有效的上下文中,例如在类型别名中和作为强制转换的目标类型。
  • 它可以作为可调用对象使用,其关键字参数对应于 TypedDict 项目。不允许非关键字参数。示例
    m = Movie(name='Blade Runner', year=1982)
    

    被调用时,TypedDict 类型对象在运行时返回一个普通的字典对象

    print(type(m))  # <class 'dict'>
    
  • 它可以用作基类,但仅在定义派生 TypedDict 时。这将在下面详细讨论。

特别是,TypedDict 类型对象不能用于isinstance()测试,例如isinstance(d, Movie)。原因是目前不支持检查字典项值的类型,因为isinstance()不适用于许多PEP 484类型,包括常见的类型如List[str]。这对于以下情况是必需的

class Strings(TypedDict):
    items: List[str]

print(isinstance({'items': [1]}, Strings))    # Should be False
print(isinstance({'items': ['x']}, Strings))  # Should be True

上述用例不受支持。这与isinstance()不支持List[str]是一致的。

继承

使用基于类的语法,TypedDict 类型可以从一个或多个 TypedDict 类型继承。在这种情况下,不应包含TypedDict基类。示例

class BookBasedMovie(Movie):
    based_on: str

现在BookBasedMovie拥有键nameyearbased_on。它等价于这个定义,因为 TypedDict 类型使用结构兼容性

class BookBasedMovie(TypedDict):
    name: str
    year: int
    based_on: str

这是一个多重继承的例子

class X(TypedDict):
    x: int

class Y(TypedDict):
    y: str

class XYZ(X, Y):
    z: bool

TypedDict XYZ有三个项:x(类型int)、y(类型str)和z(类型bool)。

TypedDict 不能同时继承 TypedDict 类型和非 TypedDict 基类。

关于 TypedDict 类继承的额外说明

  • 不允许在子类中更改父 TypedDict 类的字段类型。示例
    class X(TypedDict):
       x: str
    
    class Y(X):
       x: int  # Type check error: cannot overwrite TypedDict field "x"
    

    在上面概述的示例中,TypedDict 类注解为键x返回类型str

    print(Y.__annotations__)  # {'x': <class 'str'>}
    
  • 多重继承不允许同名字段存在冲突类型
    class X(TypedDict):
       x: int
    
    class Y(TypedDict):
       x: str
    
    class XYZ(X, Y):  # Type check error: cannot overwrite TypedDict field "x" while merging
       xyz: bool
    

总和性

默认情况下,所有键都必须存在于 TypedDict 中。可以通过指定 完整性 来覆盖此设置。以下是使用基于类的语法实现此目的的方法

class Movie(TypedDict, total=False):
    name: str
    year: int

这意味着Movie TypedDict 可以省略任何键。因此,以下是有效的

m: Movie = {}
m2: Movie = {'year': 2015}

类型检查器只支持字面值FalseTrue作为total参数的值。True是默认值,它使类体中定义的所有项都成为必需项。

完整性标志仅适用于 TypedDict 定义主体中定义的项。继承的项不受影响,而是使用它们定义所在的 TypedDict 类型的完整性。这使得在单个 TypedDict 类型中同时包含必需和非必需键成为可能。

替代语法

本 PEP 还提出了一个替代语法,可以向后移植到较旧的 Python 版本,例如 3.5 和 2.7,这些版本不支持PEP 526中引入的变量定义语法。它类似于定义命名元组的传统语法

Movie = TypedDict('Movie', {'name': str, 'year': int})

也可以使用替代语法指定总和性

Movie = TypedDict('Movie',
                  {'name': str, 'year': int},
                  total=False)

语义等同于基于类的语法。然而,这种语法不支持继承,并且无法在单个类型中同时拥有必需和非必需字段。这样做的动机是尽可能简化向后兼容的语法,同时涵盖最常见的用例。

类型检查器只接受字典显示表达式作为TypedDict的第二个参数。特别是,为了简化实现,不需要支持引用字典对象的变量。

类型一致性

非正式地说,类型一致性 是 is-subtype-of 关系的一种泛化,以支持Any类型。它在PEP 483中更正式地定义。本节介绍了支持 TypedDict 类型的类型一致性所需的新且非平凡的规则。

首先,任何 TypedDict 类型都与Mapping[str, object]一致。其次,TypedDict 类型A与 TypedDict B一致,当且仅当A在结构上与B兼容。这当且仅当以下两个条件都满足

  • 对于B中的每个键,A具有相应的键,并且A中相应的值类型与B中的值类型一致。对于B中的每个键,B中的值类型也与A中相应的值类型一致。
  • 对于B中的每个必需键,A中相应的键也是必需的。对于B中的每个非必需键,A中相应的键也是非必需的。

讨论

  • 值类型的行为是不变的,因为 TypedDict 对象是可变的。这与可变容器类型(例如ListDict)类似。以下是相关示例
    class A(TypedDict):
        x: Optional[int]
    
    class B(TypedDict):
        x: int
    
    def f(a: A) -> None:
        a['x'] = None
    
    b: B = {'x': 0}
    f(b)  # Type check error: 'B' not compatible with 'A'
    b['x'] + 1  # Runtime error: None + 1
    
  • 具有必需键的 TypedDict 类型与相同键为非必需键的 TypedDict 类型不一致,因为后者允许删除键。以下是相关示例
    class A(TypedDict, total=False):
        x: int
    
    class B(TypedDict):
        x: int
    
    def f(a: A) -> None:
        del a['x']
    
    b: B = {'x': 0}
    f(b)  # Type check error: 'B' not compatible with 'A'
    b['x'] + 1  # Runtime KeyError: 'x'
    
  • 不含键'x'的 TypedDict 类型A与具有非必需键'x'的 TypedDict 类型不一致,因为在运行时,键'x'可能存在并具有不兼容的类型(由于结构子类型,可能无法通过A看到)。示例
    class A(TypedDict, total=False):
        x: int
        y: int
    
    class B(TypedDict, total=False):
        x: int
    
    class C(TypedDict, total=False):
        x: int
        y: str
    
    def f(a: A) -> None:
        a['y'] = 1
    
    def g(b: B) -> None:
        f(b)  # Type check error: 'B' incompatible with 'A'
    
    c: C = {'x': 0, 'y': 'foo'}
    g(c)
    c['y'] + 'bar'  # Runtime error: int + str
    
  • TypedDict 与任何Dict[...]类型不一致,因为字典类型允许破坏性操作,包括clear()。它们还允许设置任意键,这会损害类型安全。示例
    class A(TypedDict):
        x: int
    
    class B(A):
        y: str
    
    def f(d: Dict[str, int]) -> None:
        d['y'] = 0
    
    def g(a: A) -> None:
        f(a)  # Type check error: 'A' incompatible with Dict[str, int]
    
    b: B = {'x': 0, 'y': 'foo'}
    g(b)
    b['y'] + 'bar'  # Runtime error: int + str
    
  • 具有所有int值的 TypedDict 与Mapping[str, int]不一致,因为可能存在通过该类型不可见的额外非int值,原因在于结构子类型。例如,这些值可以通过Mapping中的values()items()方法访问。示例
    class A(TypedDict):
        x: int
    
    class B(TypedDict):
        x: int
        y: str
    
    def sum_values(m: Mapping[str, int]) -> int:
        n = 0
        for v in m.values():
            n += v  # Runtime error
        return n
    
    def f(a: A) -> None:
        sum_values(a)  # Error: 'A' incompatible with Mapping[str, int]
    
    b: B = {'x': 0, 'y': 'foo'}
    f(b)
    

支持和不支持的操作

类型检查器应支持对 TypedDict 对象的大多数dict操作的受限形式。指导原则是,如果操作可能违反运行时类型安全,则不涉及Any类型的操作应被类型检查器拒绝。以下是一些最重要的要防止的类型安全违规

  1. 缺少必需的键。
  2. 值具有无效类型。
  3. 添加了 TypedDict 类型中未定义的键。

通常应拒绝不是字面量的键,因为其值在类型检查期间是未知的,因此可能导致上述某些违规。(Final 值和 Literal 类型的使用将此推广到涵盖最终名称和字面量类型。)

使用未知存在的键应报告为错误,即使这不一定会在运行时产生类型错误。这些通常是错误,如果结构子类型隐藏了某些项目的类型,则它们可能会插入无效类型的值。例如,如果'x'不是d(假定为 TypedDict 类型)的有效键,则d['x'] = 1应生成类型检查错误。

TypedDict 对象构造中包含的多余键也应该被捕获。在此示例中,director键未在Movie中定义,并预期会从类型检查器生成错误

m: Movie = dict(
    name='Alien',
    year=1979,
    director='Ridley Scott')  # error: Unexpected key 'director'

类型检查器应拒绝以下对 TypedDict 对象的操作,因为它们不安全,即使它们对普通字典是有效的

  • 带有任意str键(而不是字符串字面量或其他具有已知字符串值的表达式)的操作通常应被拒绝。这涉及破坏性操作(如设置项)和只读操作(如下标表达式)。作为上述规则的一个例外,d.get(e)e in d应允许用于 TypedDict 对象,对于类型为str的任意表达式e。这样做的动机是它们是安全的,并且对于自省 TypedDict 对象很有用。如果e的字符串值无法静态确定,则d.get(e)的静态类型应为object
  • clear()不安全,因为它可能删除必需的键,其中一些可能由于结构子类型而不可直接见。popitem()同样不安全,即使所有已知键都不是必需的(total=False)。
  • del obj['key']应该被拒绝,除非'key'是一个非必需键。

类型检查器可能允许使用d['x']读取项,即使键'x'不是必需的,而不是要求使用d.get('x')或显式的'x' in d检查。理由是全面跟踪键的存在性很难实现,并且禁止此操作可能需要对现有代码进行大量更改。

具体的类型检查规则由每个类型检查器决定。在某些情况下,如果替代方案是对惯用代码生成误报错误,则可能会接受潜在不安全的操作。

Final 值和 Literal 类型的使用

类型检查器应允许在 TypedDict 对象操作中使用具有字符串值的 final 名称(PEP 591)来代替字符串字面量。例如,这是有效的

YEAR: Final = 'year'

m: Movie = {'name': 'Alien', 'year': 1979}
years_since_epoch = m[YEAR] - 1970

同样,可以使用具有合适字面量类型(PEP 586)的表达式来代替字面量值

def get_value(movie: Movie,
              key: Literal['year', 'name']) -> Union[int, str]:
    return movie[key]

类型检查器仅支持实际的字符串字面量,而不支持 final 名称或字面量类型,用于在 TypedDict 类型定义中指定键。此外,只能使用布尔字面量在 TypedDict 定义中指定完整性。这样做的动机是使类型声明自包含,并简化类型检查器的实现。

向后兼容性

为了保持向后兼容性,类型检查器不应推断 TypedDict 类型,除非程序员明确表示需要。如果不确定,应推断为普通字典类型。否则,一旦 TypedDict 支持添加到类型检查器中,现有代码(以前类型检查无错误)可能会开始生成错误,因为 TypedDict 类型比字典类型更具限制性。特别是,它们不是字典类型的子类型。

参考实现

mypy [2]类型检查器支持 TypedDict 类型。运行时组件的参考实现由typing_extensions [3]模块提供。最初的实现是在mypy_extensions [4]模块中。

被拒绝的替代方案

一些提出的想法被拒绝了。目前的功能集似乎涵盖了广泛的范围,并且不清楚提出的哪些扩展会比仅仅略微有用。本 PEP 定义了一个基线功能,将来可能会扩展。

这些原则上被拒绝,因为它们与本提案的精神不符

  • TypedDict 不可扩展,它只解决一个特定用例。TypedDict 对象在运行时是常规字典,TypedDict 不能与其他类似字典或映射的类一起使用,包括dict的子类。没有办法向 TypedDict 类型添加方法。这里的动机是简单性。
  • TypedDict 类型定义可能用于执行字典的运行时类型检查。例如,它们可以用于验证 JSON 对象是否符合 TypedDict 类型指定的模式。本 PEP 不包括此类功能,因为本提案的重点仅在于静态类型检查,并且如基于类的语法中讨论的,其他现有类型不支持此功能。此类功能可以通过第三方库使用typing_inspect [5]第三方模块等方式提供。
  • TypedDict 类型不能用于isinstance()issubclass()检查。原因与许多类型提示通常不支持运行时类型检查的原因类似。

这些功能被本 PEP 排除在外,但它们是未来可能添加的扩展

  • TypedDict 不支持为未明确定义的键提供 默认值类型。这将允许将任意键与 TypedDict 对象一起使用,并且只有明确枚举的键会与普通、统一的字典类型相比获得特殊处理。
  • 无法单独指定每个键是否必需。没有提议的语法足够清晰,我们预计对此的需求有限。
  • TypedDict 不能用于指定**kwargs参数的类型。这将允许限制允许的关键字参数及其类型。根据PEP 484,使用 TypedDict 类型作为**kwargs的类型意味着 TypedDict 作为任意关键字参数的 是有效的,但它不限制允许哪些关键字参数。为此,已经提出了语法**kwargs: Expand[T] [6]

致谢

David Foster 为 mypy 贡献了 TypedDict 类型的最初实现。至少作者(Jukka Lehtosalo)、Ivan Levkivskyi、Gareth T、Michael Lee、Dominik Miedzinski、Roy Williams 和 Max Moroz 贡献了对实现的改进。

参考资料


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

上次修改:2025-04-09 22:14:53 GMT