PEP 728 – 带有键入额外项目的 TypedDict

猫勺猫勺 03-23 143 阅读 0 评论

抽象

PEP 提出了一种方法来限制使用参数的额外项目并使用特殊键键入它们。这解决了定义封闭式 TypedDict 类型或键入子集的需要 的键,这些键可能会在一段时间内出现,允许其他项目 指定类型。closed__extra_items__dict

赋予动机

类型可以注释每个已知的值类型 字典中的项。但是,由于结构子类型,TypedDict 可以具有 通过其类型不可见的额外项目。目前没有办法 限制 TypedDict 类型的 结构亚型。

定义封闭式 TypedDict 类型

TypedDict 的当前行为阻止用户定义封闭式 TypedDict 类型,当该类型不包含其他项时。

由于可能存在额外的项目,类型检查器无法推断出更多 TypedDict 和 TypedDict 上的精确返回类型。这可以 也可以通过定义封闭的 TypedDict 类型来解决。.items().values()

另一个可能的用例是使用检查启用类型缩小的合理方法:in

class Movie(TypedDict):
    name: str
    director: strclass Book(TypedDict):
    name: str
    author: strdef fun(entry: Movie | Book) -> None:
    if "author" in entry:
        reveal_type(entry)  # Revealed type is 'Movie | Book'

没有什么能阻止在结构上与 有密钥,在当前规范下,它将是 类型检查器缩小其类型范围不正确。dictMovieauthor

允许特定类型的额外项目

用于支持 API 接口或遗留代码库,其中只有一部分可能 密钥是已知的,显式期望其他密钥 某些值类型。

但是,类型规范对类型检查的构造更具限制性 TypedDict,阻止用户执行此操作:

class MovieBase(TypedDict):
    name: strdef fun(movie: MovieBase) -> None:
    # movie can have extra items that are not visible through MovieBase
    ...movie: MovieBase = {"name": "Blade Runner", "year": 1982}  # Not OK
    fun({"name": "Blade Runner", "year": 1982})  # Not OK

虽然在构造 TypedDict 时强制执行限制,但由于 结构子类型,TypedDict 可能具有不可见的额外项目 通过其类型。例如:

class Movie(MovieBase):
    year: int
    movie: Movie = {"name": "Blade Runner", "year": 1982}
    fun(movie)  # OK

不可能通过检查来确认额外项目的存在并在不破坏类型安全的情况下访问它们,即使它们 可能存在于以下任意结构亚型中:inMovieBase

def g(movie: MovieBase) -> None:
    if "year" in movie:
        reveal_type(movie["year"])  # Error: TypedDict 'MovieBase' has no key 'year'

已经实施了一些变通办法,以响应允许 额外的键,但没有一个是理想的。对于 mypy,禁止专门针对 TypedDict 上的未知键的类型检查错误。这牺牲了类型安全性 灵活性,并且它不提供指定 TypedDict 类型的方法 需要与特定类型兼容的其他密钥。--disable-error-code=typeddict-unknown-key

支持其他键

PEP 692 增加了一种精确注释单个关键字类型的方法 通过使用 TypedDict 和 表示的参数。然而 因为 TypedDict 不能被定义为接受任意的额外项目,所以它不是 可能允许在定义 TypedDict 时未知的其他关键字参数。**kwargsUnpack

鉴于在现有的 PEP 692 之前的类型注释的使用 代码库,在 TypedDict 上接受和键入额外的项目将很有价值,因此 旧的类型行为可以与新构造结合使用。**kwargsUnpack

理由

允许在 TypedDict 上添加额外类型的类型可以松散地 描述为 TypedDict 和 .strMapping[str, str]

TypeScript 中的索引签名实现了这一点:

type Foo = {
    a: string    
    [key: string]: string
}

该提案旨在支持类似的功能,而不引入一般 类型或语法更改的交集,为 现有类型一致性规则。

我们建议向 TypedDict 添加一个参数。与 类似,只允许使用文本或值。当在 TypedDict 类型定义中使用时,我们给出 dunder 属性具有特殊含义:允许额外的项目,并且 它们的类型应与 的值类型兼容。closedtotalTrueFalseclosed=True__extra_items____extra_items__

如果已设置,但没有键,则 TypedDict 被视为包含一个项目。closed=True__extra_items____extra_items__: Never

请注意,在同一个 TypedDict 上,类型定义将保留 如果不使用,则作为常规项目。__extra_items__closed=True

与索引签名不同,已知项的类型不需要 与 的值类型一致。__extra_items__

这种方法有一些优点:

  • 继承是自然而然的。 在 TypedDict 上定义 will 也可用于其子类。__extra_items__

  • 我们可以在类型规范中定义的类型一致性规则之上进行构建。 在类型上可以被视为伪项目 一致性。__extra_items__

  • 无需引入语法更改来指定 额外的项目。

  • 我们可以精确地输入额外的项目,而无需制作 已知项的并集。__extra_items__

  • 我们不会失去向后兼容性,因为仍然可以 用作常规密钥。__extra_items__

规范

该规范的结构与 PEP 589 并行,以突出对 原始的 TypedDict 规范。

如果指定,则额外项目将被视为非必需项目 在以下情况下允许使用相同类型的密钥 确定支持和不支持的操作。closed=True__extra_items__

使用 TypedDict 类型

假设在 TypedDict 类型定义中使用了它。closed=True

对于具有特殊键的 TypedDict 类型,在 构造,则每个未知项的值类型预计为非必需值 并与 的值类型兼容。例如:__extra_items____extra_items__

class Movie(TypedDict, closed=True):
    name: str
    __extra_items__: boola: Movie = {"name": "Blade Runner", "novel_adaptation": True}  # OKb: Movie = {
    "name": "Blade Runner",
    "year": 1982,  # Not OK. 'int' is incompatible with 'bool'}

在此示例中,并不意味着具有 必需的字符串键,其值类型为 。相反 它指定“name”以外的键的值类型为 和 非必需。__extra_items__: boolMovie"__extra_items__"boolbool

还支持替代内联语法:

Movie = TypedDict("Movie", {"name": str, "__extra_items__": bool}, closed=True)

允许访问额外的密钥。类型检查器必须从以下位置推断其值类型 值类型:__extra_items__

def f(movie: Movie) -> None:
    reveal_type(movie["name"])              # Revealed type is 'str'
    reveal_type(movie["novel_adaptation"])  # Revealed type is 'bool'

当 TypedDict 类型定义 without 时,默认为 ,并且假定该键是常规键:__extra_items__closed=TrueclosedFalse

class Movie(TypedDict):
    name: str
    __extra_items__: bool
a: Movie = {"name": "Blade Runner", "novel_adaptation": True}  # Not OK. Unexpected key 'novel_adaptation'
b: Movie = {
    "name": "Blade Runner",
    "__extra_items__": True,  # OK
}

对于此类非封闭式 TypedDict 类型,假定它们允许非必需的 在继承或类型期间值类型的额外项 一致性检查。但是,在施工过程中发现的额外钥匙仍应 被类型检查器拒绝。ReadOnly[object]

closed不通过子类继承:

class MovieBase(TypedDict, closed=True):
    name: str
    __extra_items__: ReadOnly[str | None]
class Movie(MovieBase):
    __extra_items__: str  # A regular key
a: Movie = {"name": "Blade Runner", "__extra_items__": None}  # Not OK. 'None' is incompatible with 'str'
b: Movie = {
    "name": "Blade Runner",
    "__extra_items__": "A required regular key",
    "other_extra_key": None,
}  # OK

这里,in 是定义在 where 上的常规键 它的值类型从 to 缩小到 ,in 是一个额外的键,其值类型必须是 与 上定义的值类型一致。"__extra_items__"aMovieReadOnly[str | None]str"other_extra_key"b"__extra_items__"MovieBase

与整体的互动

使用或与特殊项目一起使用是错误的。 并且对自身没有影响。Required[]NotRequired[]__extra_items__total=Falsetotal=True__extra_items__

无论 TypedDict 的整体性如何,额外的项目都不是必需的。 可用于物料的操作也应可用 到额外的项目:NotRequired

class Movie(TypedDict, closed=True):
    name: str
    __extra_items__: intdef f(movie: Movie) -> None:
    del movie["name"]  # Not OK
    del movie["year"]  # OK

与以下人员的互动

出于型式检查目的,额外的项目应该是 在常规参数中被视为等价参数,而现有规则 函数参数仍然适用:Unpack[TypedDict]

class Movie(TypedDict, closed=True):
    name: str
    __extra_items__: int
def f(**kwargs: Unpack[Movie]) -> None: ...
# Should be equivalent to
def f(*, name: str, **kwargs: int) -> None: ...

与 PEP 705 的交互

当特殊项目用 注释时, TypedDict 上的额外项具有只读项的属性。这 与 PEP 705 中指定的继承规则交互。__extra_items__ReadOnly[]

值得注意的是,如果 TypedDict 类型声明为只读,则 TypedDict 类型的子类可以重新声明 的值类型或 其他非额外项的值类型。__extra_items____extra_items__

因为非闭合的 TypedDict 类型隐式允许非必需的额外项 的 value type ,它的子类可以用更具体的类型覆盖特殊。ReadOnly[object]__extra_items__

更多细节将在后面的章节中讨论。

遗产

当 TypedDict 类型定义为(默认值)时,其行为和继承方式与常规键相同 愿意。常规键可以与特殊键共存,并且在子类化时应继承两者。closed=False__extra_items____extra_items____extra_items__

我们假设每当提到 本节的其余部分。closed=True__extra_items__

__extra_items__以与常规项目相同的方式继承。与其他键一样,键入规范和 PEP 705 中的相同规则也适用。我们解释 的上下文。key: value_type__extra_items__

我们需要重新解释以下规则来定义如何与之交互:__extra_items__

  • 不允许更改子类中父 TypedDict 类的字段类型。

首先,不允许更改子类中的值类型 除非它被声明为在超类中:__extra_items__ReadOnly

class Parent(TypedDict, closed=True):
__extra_items__: int | None
class Child(Parent, closed=True):
 __extra_items__: int  # Not OK. Like any other TypedDict item, __extra_items__'s type cannot be changed

其次,有效地定义任何未命名的值类型 接受 TypedDict 的项目,并将其标记为非必需项。因此,上述 限制适用于子类中定义的任何其他项。对于每个项目 添加到子类中,以下所有条件都应适用:__extra_items__: T

  • 如果为只读__extra_items__

  • 该项目可以是必需的,也可以是非必需的

  • 项的值类型与T

  • 如果不是只读的__extra_items__

  • 该项目不是必需的

  • 项的值类型与T

  • T与物料的值类型一致

  • 如果未重新声明,则子类将按原样继承它。__extra_items__

例如:

class MovieBase(TypedDict, closed=True):
    name: str
    __extra_items__: int | None
class AdaptedMovie(MovieBase):  # Not OK. 'bool' is not consistent with 'int | None'
    adapted_from_novel: bool
class MovieRequiredYear(MovieBase):  # Not OK. Required key 'year' is not known to 'Parent'
    year: int | None
class MovieNotRequiredYear(MovieBase):  # Not OK. 'int | None' is not consistent with 'int'
    year: NotRequired[int]
class MovieWithYear(MovieBase):  # OK
    year: NotRequired[int | None]

由于这种性质,一个重要的副作用允许我们定义一个 TypedDict 不允许其他项目的类型:

class MovieFinal(TypedDict, closed=True):
    name: str
    __extra_items__: Never

在这里,注释指定 除了已知的键之外,不能有其他键。 由于其潜在的通用用途,这相当于:__extra_items__typing.NeverMovieFinal

class MovieFinal(TypedDict, closed=True):    
        name: str

默认情况下,我们隐式假设该字段 如果指定了 only。__extra_items__: Neverclosed=True

类型一致性

除了显式定义项的键集之外,一个 具有该项的 TypedDict 类型被视为具有 满足以下条件的无限项集:S__extra_items__: T

  • 如果为只读__extra_items__

  • 键的值类型与T

  • 键不在 .S

  • 如果不是只读的__extra_items__

  • 密钥是非必需的

  • 键的值类型与T

  • T与键的值类型一致

  • 键不在 .S

出于类型检查目的,let be a non-required 伪项 每当“对于每个......item/key“在 PEP 705 的现有类型一致性规则中说明, 我们修改如下:__extra_items__

如果 TypedDict 类型与 TypedDict 一致,则为 在结构上与 兼容。当且仅当所有 满足以下要求:ABAB

对于 中的每个项目,都有相应的键,除非项目 in 是只读的,不是必需的,并且属于 top value 类型 ().[编辑:否则,如果 在 ''A'' 中找不到同名的相应键, “__extra_items__”被视为相应的键。BABReadOnly[NotRequired[object]]

对于 中的每个项目,如果具有相应的键 [编辑:或 “__extra_items__”],则对应的值类型是一致的 值类型为 。BAAB

对于 中的每个非只读项,其值类型与 中的相应值类型。[编辑:如果对应的键 在“A”中找不到同名,“__extra_items__”是 考虑了相应的键。BA

对于 中的每个必需键,在 中都需要相应的键。 对于 中的每个非必需键,如果项目在 中不是只读的, 中不需要相应的键。[编辑:如果在 ''A'', “__extra_items__” 被认为是非必需的,因为 对应的键。BABBA

以下示例演示了这些检查的实际应用。

__extra_items__对类型的其他项目施加各种限制 一致性检查:

class Movie(TypedDict, closed=True):
    name: str
    __extra_items__: int | None
class MovieDetails(TypedDict, closed=True):
    name: str
    year: NotRequired[int]
    __extra_items__: int | None
details: MovieDetails = {"name": "Kill Bill Vol. 1", "year": 2003}
movie: Movie = details  # Not OK. While 'int' is consistent with 'int | None',
               # 'int | None' is not consistent with 'int'
class MovieWithYear(TypedDict, closed=True):
    name: str
    year: int | None
    __extra_items__: int | None
details: MovieWithYear = {"name": "Kill Bill Vol. 1", "year": 2003}
movie: Movie = details  # Not OK. 'year' is not required in 'Movie',
               # so it shouldn't be required in 'MovieWithYear' either

因为 中没有“year”,所以被认为是 对应的键。 被要求违反了“对于每个 必需的键,相应的键在“中是必需的。Movie__extra_items__"year"BA

当在 TypedDict 类型中定义为只读时,可以 对于具有比 的值类型更窄的项:__extra_items____extra_items__

class Movie(TypedDict, closed=True):
    name: str
    __extra_items__: ReadOnly[str | int]
class MovieDetails(TypedDict, closed=True):
    name: str
    year: NotRequired[int]
    __extra_items__: int
details: MovieDetails = {"name": "Kill Bill Vol. 2", "year": 2004}movie:
Movie = details  # OK. 'int' is consistent with 'str | int'.

这的行为方式与 PEP 705 指定的方式相同,如果 是在 中定义的项目。year: ReadOnly[str | int]Movie

__extra_items__由于伪项目遵循与其他项目相同的规则,因此 当两个 TypedDict 都包含 时,自然会强制执行此检查:__extra_items__

class MovieExtraInt(TypedDict, closed=True):
    name: str
    __extra_items__: int
class MovieExtraStr(TypedDict, closed=True):
    name: str
    __extra_items__: str
extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
extra_str: MovieExtraStr = {"name": "No Country for Old Men", "description": ""}
extra_int = extra_str  # Not OK. 'str' is inconsistent with 'int' for item '__extra_items__'
extra_str = extra_int  # Not OK. 'int' is inconsistent with 'str' for item '__extra_items__'

非闭合 TypedDict 类型隐式允许非必需的额外值键 类型。这允许应用类型一致性规则 在此类型和封闭的 TypedDict 类型之间:ReadOnly[object]

class MovieNotClosed(TypedDict):
    name: str
extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
not_closed: MovieNotClosed = {"name": "No Country for Old Men"}
extra_int = not_closed  # Not OK. 'ReadOnly[object]' implicitly on 'MovieNotClosed' is not consistent with 'int' for item '__extra_items__'
not_closed = extra_int  # OK

与构造函数的交互

允许额外类型项的 TypedDicts 也允许任意关键字 通过调用类对象构造时,此类型的参数:T

class OpenMovie(TypedDict):
    name: str
OpenMovie(name="No Country for Old Men")  # OK
OpenMovie(name="No Country for Old Men", year=2007)  # Not OK. Unrecognized key
class ExtraMovie(TypedDict, closed=True):
    name: str
    __extra_items__: int
ExtraMovie(name="No Country for Old Men")  # OK
ExtraMovie(name="No Country for Old Men", year=2007)  # OK
ExtraMovie(
    name="No Country for Old Men",
    language="English",
)  # Not OK. Wrong type for extra key
# This implies '__extra_items__: Never',
# so extra keyword arguments produce an error
class ClosedMovie(TypedDict, closed=True):
    name: str
ClosedMovie(name="No Country for Old Men")  # OK
ClosedMovie(
    name="No Country for Old Men",
    year=2007,
)  # Not OK. Extra items not allowed

与Mapping的交互[KT, VT]

TypedDict 类型可以与类型一致,但只要 TypedDict 上的值类型并集即可 类型与 一致。它是此规则从键入的扩展 规范:Mapping[KT, VT]Mapping[str, object]VT

具有所有值的 TypedDict 与 不一致,因为由于结构子类型化,可能存在其他非 - 值在类型中不可见。 可以使用 和 中的intMapping[str, int]intvalues()items()Mapping

例如:

class MovieExtraStr(TypedDict, closed=True):
    name: str
    __extra_items__: str
extra_str: MovieExtraStr = {"name": "Blade Runner", "summary": ""}
str_mapping: Mapping[str, str] = extra_str  # OK
int_mapping: Mapping[str, int] = extra_int  # Not OK. 'int | str' is not consistent with 'int'
int_str_mapping: Mapping[str, int | str] = extra_int  # OK

此外,类型检查器应该能够推断出此类 TypedDict 类型的精确返回类型:values()items()

def fun(movie: MovieExtraStr) -> None:
    reveal_type(movie.items())  # Revealed type is 'dict_items[str, str]'
    reveal_type(movie.values())  # Revealed type is 'dict_values[str, str]'

与dict[KT, VT]的交互

请注意,因为 在封闭的 TypedDict 类型上存在 禁止在其结构子类型中添加其他必需的密钥,我们可以确定 如果 TypedDict 类型及其结构子类型有任何必需的 静态分析期间的关键。__extra_items__

TypedDict 类型与 TypedDict 类型满足以下条件:dict[str, VT]

VT与物料的值类型一致

项的值类型与VT

该项目不是只读的。

该项目不是必需的。

例如:

class IntDict(TypedDict, closed=True):
    __extra_items__: int
class IntDictWithNum(IntDict):
    num: NotRequired[int]
def f(x: IntDict) -> None:
    v: dict[str, int] = x  # OK
    v.clear()  # OK
not_required_num: IntDictWithNum = {"num": 1, "bar": 2}
regular_dict: dict[str, int] = not_required_num  # OK
f(not_required_num)  # OK

在这种情况下,允许使用以前在 TypedDict 上不可用的方法:

not_required_num.clear()  # OK
reveal_type(not_required_num.popitem())  # OK. Revealed type is tuple[str, int]

但是,不一定与 TypedDict 类型一致, 因为这样的 dict 可以是 dict 的子类型:dict[str, VT]

class CustomDict(dict[str, int]):
    ...
not_a_regular_dict: CustomDict = {"num": 1}
int_dict: IntDict = not_a_regular_dict  # Not OK

如何教这个

拼写的选择旨在实现这一点 与较短的替代方案相比,新用户更容易理解该功能。"__extra_items__""__extra__"

有关此内容的详细信息应记录在类型规范和文档中。

向后兼容性

因为如果不是,则保持为常规键 指定后,现有代码库不会因此更改而中断。__extra_items__closed=True

如果提案被接受,则 、 和 不应 在指定时,在相同的 TypedDict 类型上定义的 include。__required_keys____optional_keys____readonly_keys____mutable_keys__"__extra_items__"closed=True

请注意,作为关键字参数不会与关键字发生冲突 参数 替代使用允许的功能语法定义键 像 ,因为它是预定的 在 Python 3.13 中删除。closedTD = TypedDict("TD", foo=str, bar=int)

因为这是一项类型检查功能,所以可以将其提供给较旧的 版本,只要类型检查器支持它。

被拒绝的想法

 允许在不指定类型的情况下添加额外项目

extra=True最初被提议用于定义一个接受额外 无论类型如何,项目,例如如何工作:total=True

class TypedDict(extra=True):
    pass

因为它没有提供指定额外项类型的方法,所以类型 检查器需要假设额外项目的类型是 ,其中 损害类型安全。此外,TypedDict 的当前行为已经 允许在运行时中存在非类型的额外项,因为结构 子类型。 在目前的提案中也发挥了类似的作用。Anyclosed=True

配套

在讨论 PEP 期间,有人强烈反对增加 另一个地方,类型作为值传递,而不是来自某些的注释 类型检查器的作者。虽然这种设计可能可行,但有 还有几个部分可解决的问题需要考虑。

  • 前向参考的可用性 与函数语法一样,使用带引号的类型或类型别名将是 当 SomeType 是前向引用时是必需的。这已经是一项要求 对于函数语法,因此实现可以重用该部分 的逻辑,但这仍然是提案没有的额外工作 有。closed=True

  • 关于使用类型作为值的顾虑 函数语法中不允许作为值类型的任何内容都不应该 被允许作为额外的论据。虽然类型检查器可能能够 要重用此检查,它仍然需要以某种方式对 基于类的语法。

  • 如何教学 值得注意的是,由于它是一种直觉,因此经常被提出来 用例的解决方案,因此学习起来可能比学习的更少更简单 显而易见的解决方案。但是,更常见的用例只需要 ,前面提到的其他缺点超过了 需要教特殊键的用法。extra=typeclosed=True

支持具有交叉点的额外项目

在 Python 的类型系统中支持交叉点需要非常小心 考虑,社区可能需要很长时间才能达成 就合理的设计达成共识。

理想情况下,TypedDict 中的额外项目不应被 交叉点,也不一定需要通过 交叉 口。

而且,和 之间的交集不是 等同于具有建议的特殊 item,作为所有已知项的值类型,需要满足 is-subtype-of 关系,值类型为 。Mapping[...]TypedDict__extra_items__TypedDictMapping[...]

要求已知项的类型兼容性

__extra_items__限制 TypedDict 类型。因此,任何已知项的值类型不一定是 与 的类型一致,而 的类型为 不一定与所有已知项的值类型一致。__extra_items____extra_items__

这与 TypeScript 的索引签名语法不同,后者要求所有属性的类型都与字符串索引的类型匹配。 例如:

interface MovieWithExtraNumber {
        name: string // Property 'name' of type 'string' is not assignable to 'string' index type 'number'.    
        [index: string]: number
}interface MovieWithExtraNumberOrString {
    name: string // OK    
    [index: string]: number | string
}

这是 TypeScript 的问题跟踪器中讨论的已知限制, 建议应该有一种方法来排除定义的键 从索引签名中,这样就可以定义类似 的类型。MovieWithExtraNumber

版权

本文档位于公共领域或 CC0-1.0-通用许可证,以更宽松者为准。

The End 微信扫一扫

文章声明:以上内容(如有图片或视频在内)除非注明,否则均为腾龙猫勺儿原创文章,转载或复制请以超链接形式并注明出处。

本文作者:猫勺本文链接:https://www.jo6.cn/post/73.html

上一篇 下一篇

相关阅读

发表评论

访客 访客
快捷回复: 表情:
评论列表 (暂无评论,143人围观)

还没有评论,来说两句吧...

取消
微信二维码
微信二维码
支付宝二维码