PEP 712 – 向 dataclasses.field 添加“converter”参数

猫勺猫勺 03-18 131 阅读 0 评论

抽象

PEP 557 已添加到 Python stdlib 中。添加了 PEP 681 以帮助类型检查器理解 几个常见的类似数据类的库,例如 attrs、Pydantic 和 object 关系映射器 (ORM) 包,例如 SQLAlchemy 和 Django。

其他库相对于标准库提供的共同功能 实现是库转换 初始化时间为每个字段的预期类型,使用 用户提供的转换功能。

因此,此 PEP 向 (以及对 和 的必要更改) 添加一个参数,以指定要用于 将每个字段的输入值转换为要存储在 数据类。converter

赋予动机

没有现成的、标准的方式或第三方 类似 DataClass 的库,支持类型可检查的参数转换 道路。若要解决此限制,库作者/用户必须选择 自:

  • 选择加入自定义 Mypy 插件。这些插件帮助 Mypy 了解 转换语义,但不是其他工具。

  • 将转换责任转移到数据类的调用方 构造 函数。这可能会使构造某些数据类变得不必要 冗长和重复。

  • 提供声明“更宽”参数类型的自定义,以及 在设置适当的属性时转换它们。这不仅重复 转换器 和 之间的类型注解,但也选择 用户提供的许多功能。__init____init__

  • 提供自定义但不有意义的类型注释 对于需要转换的参数类型。__init__

这些选择都不是理想的。

理由

添加参数转换语义是有用和有益的,足以使大多数 类似 DataClass 的库提供支持。将此功能添加到标准中 图书馆意味着更多的用户能够选择加入这些好处,而不需要 第三方库。此外,第三方库能够提供线索 type-checkers 通过添加对 的支持进入自己的转换语义,这意味着这些库的用户 也有好处。

规范

新参数

此规范引入了一个名为函数的新参数。如果提供,则表示单个参数 callable 用于在分配给关联属性时转换所有值。converter

对于冻结的数据类,转换器仅在设置属性时在 -synthesized 内使用。对于非冻结数据类,转换器 用于所有属性赋值(例如 ),其中包括 默认值的赋值。dataclass__init__obj.attr = value

读取属性时不使用转换器,因为属性应该已经存在 已经皈依。

添加此参数还意味着以下更改:

  • 将向 添加一个属性。converter

  • converter将被添加到 的 支持的字段说明符参数列表。

def str_or_none(x: Any) -> str | None:
  return str(x) if x is not None else None@dataclasses.dataclassclass InventoryItem:
    # `converter` as a type (including a GenericAlias).
    id: int = dataclasses.field(converter=int)
    skus: tuple[int, ...] = dataclasses.field(converter=tuple[int, ...])
    # `converter` as a callable.
    vendor: str | None = dataclasses.field(converter=str_or_none))
    names: tuple[str, ...] = dataclasses.field(
      converter=lambda names: tuple(map(str.lower, names))
    )  # Note that lambdas are supported, but discouraged as they are untyped.

    # The default value is also converted; therefore the following is not a
    # type error.
    stock_image_path: pathlib.PurePosixPath = dataclasses.field(
      converter=pathlib.PurePosixPath, default="assets/unknown.png"
    )

    # Default value conversion extends to `default_factory`;
    # therefore the following is also not a type error.
    shelves: tuple = dataclasses.field(
      converter=tuple, default_factory=list
    )item1 = InventoryItem(
  "1",
  [234, 765],
  None,
  ["PYTHON PLUSHIE", "FLUFFY SNAKE"])# item1's repr would be (with added newlines for readability):#   InventoryItem(#     id=1,#     skus=(234, 765),#     vendor=None,#     names=('PYTHON PLUSHIE', 'FLUFFY SNAKE'),#     stock_image_path=PurePosixPath('assets/unknown.png'),#     shelves=()#   )# Attribute assignment also participates in conversion.item1.skus = [555]# item1's skus attribute is now (555,).

对打字的影响

A 必须是接受单个位置参数的可调用对象,并且 与此位置参数对应的参数类型提供类型 与字段关联的合成参数。converter__init__

换句话说,为 converter 参数提供的参数必须是 与 where 兼容 是 的输入类型 转换器 和 是转换器的输出类型。Callable[[T], X]TX

类型检查和

因为默认值是使用 无条件转换的,如果 参数 for 与 或 一起提供,默认值的类型(参数 if 则,否则应检查 ) 的返回值 将单个参数的类型用于可调用对象。converterconverterdefaultdefault_factorydefaultdefault_factoryconverter

转换器返回类型

可调用对象的返回类型必须是与 字段的声明类型。这完全包括字段的类型,但也可以是 更专用的类型(例如,对于批注为 的字段返回 a 的转换器,或者为一个 返回 的转换器返回一个 字段注释为 )。list[int]listintint | str

允许的参数类型的间接

这个 PEP 引入的一个缺点是知道什么是参数类型 在 DataClass' 和 Attribute Assignment 期间不允许 从读取数据类中可以立即看出。定义了允许的类型 通过转换器。__init__

从源代码读取代码时确实如此,但是与键入相关的助手例如 作为和 IDE 中的“IntelliSense”应该可以很容易地知道 确切地说,允许哪些类型,而无需读取任何源代码。typing.reveal_type

向后兼容性

这些更改不会引入任何兼容性问题,因为它们 仅引入选择加入的新功能。

安全隐患

这些更改没有直接的安全问题。

如何教这个

解释新参数和行为的文档和示例将是 添加到文档站点的相关部分(主要在 上),并从“新增功能”文档链接

添加的文档/示例还将涵盖“常见陷阱”: 转换器的用户可能会遇到。这些陷阱包括:

  • 需要处理 /sentinel 值。None

  • 需要处理已为正确类型的值。

  • 避免使用转换器的 lambda,因为合成参数的类型将变为 .__init__Any

  • 忘记转换用户定义的正文中的值 冻结的数据类。__init__

  • 忘记转换用户定义的正文中的值 非冻结数据类。__setattr__

此外,还应涵盖可能令人困惑的模式匹配语义:

@dataclassclass Point:
    x: int = field(converter=int)
    y: intmatch Point(x="0", y=0):
    case Point(x="0", y=0):  # Won't be matched
        ...
    case Point():  # Will be matched
        ...
    case _:
        ...

但是,值得注意的是,此行为适用于任何进行转换的类型 在其初始值设定项中,类型检查器应该能够捕捉到这个陷阱:

match int("0"):
  case int("0"):  # Won't be matched
      ...
  case _:  # Will be matched
      ...

参考实现

attrs 库已经包含一个参数,该参数表现出相同的转换器语义(在 初始值设定项和 ON 属性设置)。 装饰。converter@define

CPython 支持是在作者分支中的分支上实现的。

被拒绝的想法

只需将“转换器”添加到

在 Typing-SIG 上简要讨论了将此添加隔离的想法,其中有人建议 将其扩大到更普遍的范围。

此外,添加此功能以确保任何人都可以收获 无需额外库即可获得优势。

不转换默认值

转换和不转换默认值各有利弊。 保留默认值允许类型检查器和数据类作者 默认值的类型应与字段的类型匹配。然而 转换默认值有三大优点:

  1. 一致性。无条件转换分配给 属性,涉及用户必须记住的“特殊规则”较少。

  2. 更简单的默认值。允许默认值的类型与 用户提供的值意味着数据类作者可以获得与 他们的来电者。

  3. 与 attrs 的兼容性。Attrs 无条件地使用转换器来 转换默认值。

使用字段类型自动转换

一个想法可能是允许指定字段的类型(例如 或 ) 用作所提供每个参数的转换器。Pydantic 的数据转换具有语义 似乎与这种方法类似。strint

这适用于相当简单的类型,但会导致预期的歧义 复杂类型(如泛型)的行为。例如,因为它是 如果转换器应该简单地将可迭代对象转换为元组,则模棱两可, 或者,如果另外应该将每个元素类型转换为 。或 for ,这是不可调用的。tuple[int, ...]intint | None

从转换器的返回类型推导属性类型

另一个想法是允许用户省略属性的类型注释 如果提供参数。虽然这会 减少此 PEP 引入的常见重复(例如), 目前尚不清楚如何在维护当前数据类的同时最好地支持这一点 语义(即,为诸如 合成 ,或 )。这是因为没有 在 Python 中(今天)中,一种简单的方法来获取仅注释属性的穿插 按定义顺序使用未批注的属性。fieldconverterx: str = field(converter=str)__init__dataclasses.fields

可以应用哨兵注释(例如), 但是,这打破了类型注释的基本假设。x: FromConverter = ...

最后,如果所有字段(包括没有转换器的字段)都是可行的 被分配给 ,这意味着类自己的命名空间 持有订单,但这会交易 type+converter 的重复 重复现场分配。最终结果是没有重复的收益或损失, 但随着数据类语义的复杂性增加。dataclasses.field

这个 PEP 并不意味着它不能或不应该这样做。只是事实并非如此 包含在此 PEP 中。

版权

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

The End 微信扫一扫

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

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

上一篇 下一篇

相关阅读

发表评论

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

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

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