PEP 712 – 为 dataclasses.field 添加“converter”参数
- 作者:
- Joshua Cannon <joshdcannon at gmail.com>
- 发起人:
- Eric V. Smith <eric at trueblade.com>
- 讨论至:
- Discourse 帖子
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2023年1月1日
- Python 版本:
- 3.13
- 发布历史:
- 2022年12月27日, 2023年1月19日, 2023年4月23日
- 决议:
- Discourse 消息
拒绝通知
2024 年指导委员会拒绝的原因包括:
- 尽管一些支持者认为该功能对于减少对第三方包的依赖很有必要,但我们并未发现有强烈的共识认为该功能在标准库中是必需的。对于那些需要此类功能的人来说,我们认为现有的第三方库,如 attrs 和 Pydantic(PEP 中已引用),是可接受的替代方案。
- 在我们看来,这项功能似乎会增加标准库的“冗余”,使我们离数据类最适合的“简单”用例越来越远。
- 阅读 PEP 的“如何教授此内容”部分,让我们犹豫不决,因为其陷阱和难点非常显著,混淆和复杂性增加,抵消了任何潜在的好处。
- 该 PEP 似乎更侧重于帮助类型检查器,而不是使用该库的用户。
摘要
PEP 557 向 Python 标准库添加了 dataclasses。 PEP 681 添加了 dataclass_transform(),以帮助类型检查器理解几种常见的数据类类库,如 attrs、Pydantic 和对象关系映射 (ORM) 包,如 SQLAlchemy 和 Django。
标准库实现的一个常见功能是,允许库使用用户提供的转换函数,将初始化时提供的参数转换为每个字段预期的数据类型。
因此,本 PEP 向 dataclasses.field() 添加了一个 converter 参数(以及对 dataclasses.Field 和 dataclass_transform() 的必要更改),用于指定用于将每个字段的输入值转换为存储在数据类中的表示形式的函数。
动机
对于 dataclasses 或第三方数据类类库,没有现有的标准方法可以以类型可检查的方式支持参数转换。为了规避此限制,库作者/用户被迫选择:
- 选择自定义 Mypy 插件。这些插件可以帮助 Mypy 理解转换语义,但对其他工具则无效。
- 将转换责任转移给数据类构造函数的调用者。这会使构造某些数据类变得不必要地冗长和重复。
- 提供自定义
__init__,它声明“更宽泛”的参数类型并在设置相应属性时进行转换。这不仅会在转换器和__init__之间重复类型注解,还会使您的用户无法使用dataclasses提供的许多功能。 - 提供自定义
__init__,但不对需要转换的参数类型进行有意义的类型注解。
这些选择都不是理想的。
基本原理
添加参数转换语义非常有用且有益,以至于大多数数据类类库都提供支持。将此功能添加到标准库意味着更多用户可以获得这些好处,而无需第三方库。此外,第三方库可以通过在 dataclass_transform() 中添加支持来让类型检查器了解其自身的转换语义,这意味着这些库的用户也能从中受益。
规范
新增 converter 参数
本规范在 dataclasses.field() 函数中引入了一个名为 converter 的新参数。如果提供了该参数,它将代表一个单参数的可调用对象,用于在为相关属性赋值时转换所有值。
对于不可变 (frozen) 的数据类,converter 仅在 dataclass 生成的 __init__ 中设置属性时使用。对于可变 (non-frozen) 的数据类,converter 用于所有属性赋值(例如 obj.attr = value),包括默认值的赋值。
读取属性时不会使用 converter,因为属性应该已经过转换。
添加此参数还意味着以下更改:
- 将把
converter属性添加到dataclasses.Field。 converter将添加到dataclass_transform()支持的字段说明符参数列表中。
示例
def str_or_none(x: Any) -> str | None:
return str(x) if x is not None else None
@dataclasses.dataclass
class 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,).
对类型检查的影响
converter 必须是一个可调用对象,它接受一个位置参数,并且该位置参数对应的参数类型提供了与字段关联的已生成 __init__ 参数的类型。
换句话说,为 converter 参数提供的参数必须与 Callable[[T], X] 兼容,其中 T 是 converter 的输入类型,X 是 converter 的输出类型。
default 和 default_factory 的类型检查
由于默认值将使用 converter 无条件转换,因此,如果为 converter 参数提供了与 default 或 default_factory 之一一起使用的参数,则默认值的类型(如果提供了 default 参数,则为 default 参数的值,否则为 default_factory 的返回值)应使用 converter 可调用对象的单个参数的类型进行检查。
Converter 的返回类型
可调用对象的返回类型必须与字段声明的类型兼容。这包括字段类型本身,也可以是更专门化的类型(例如,对于类型注解为 list 的字段,converter 返回 list[int];或者对于类型注解为 int | str 的字段,converter 返回 int)。
允许的参数类型的间接性
此 PEP 引入的一个缺点是,从数据类的 __init__ 和属性赋值过程中允许的参数类型,无法通过阅读数据类直接得知。允许的类型由 converter 定义。
在阅读源代码时确实是这样,但是像 typing.reveal_type 和 IDE 中的“IntelliSense”等与类型相关的辅助工具,应该能够轻松地确切知道允许的类型,而无需阅读任何源代码。
向后兼容性
这些更改不会引入任何兼容性问题,因为它们只引入了选择加入的新功能。
安全隐患
这些更改没有直接的安全问题。
如何教授此内容
将把有关新参数和行为的文档和示例添加到文档站点相关部分(主要在 dataclasses 上),并链接到“最新消息”文档。
添加的文档/示例还将涵盖转换器用户可能遇到的“常见陷阱”。这些陷阱包括:
- 需要处理
None/哨兵值。 - 需要处理已是正确类型的.*
- 避免在 converters 中使用 lambda,因为已生成的
__init__参数的类型将变为Any。 - 在不可变数据类的用户定义
__init__中忘记转换值。 - 在可变数据类的用户定义
__setattr__的主体中忘记转换值。
此外,还应涵盖可能令人困惑的模式匹配语义。
@dataclass
class Point:
x: int = field(converter=int)
y: int
match 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 库 已包含一个 converter 参数,在使用 @define 类装饰器时,该参数具有与此相同的转换器语义(在初始化和属性设置时进行转换)。
CPython 的支持实现在 作者 fork 的一个分支上。
被拒绝的想法
仅将“converter”添加到 typing.dataclass_transform 的 field_specifiers 中
将此功能仅限于 dataclass_transform() 的想法 在 Typing-SIG 上进行过简短讨论,当时有人建议将其扩展到更通用的 dataclasses。
此外,将其添加到 dataclasses 中,可以确保任何人都可以从中受益,而无需其他库。
不转换默认值
在转换默认值和不转换默认值之间都存在优缺点。保留默认值不变,可以使类型检查器和数据类作者期望默认值的类型与字段的类型匹配。然而,转换默认值有三个主要的优点:
- 一致性。无条件地转换分配给属性的所有值,涉及的“特殊规则”更少,用户需要记住。
- 更简单的默认值。允许默认值的类型与用户提供的*值*相同,意味着数据类作者可以获得与其调用者相同的便利。
- 与 attrs 兼容。attrs 无条件地使用 converter 来转换默认值。
使用字段类型自动转换
一种可能的想法是允许字段指定的类型(例如 str 或 int)作为每个参数的 converter。 Pydantic 的数据转换具有与此方法相似的语义。
这对于相当简单的类型效果很好,但对于泛型等复杂类型,会导致行为预期上的歧义。例如,对于 tuple[int, ...],converter 是应该仅仅将可迭代对象转换为元组,还是应该额外地将每个元素类型转换为 int,这是不明确的。或者对于 int | None,这是不可调用的。
从 converter 的返回类型推断属性类型
另一种想法是允许用户在提供带有 converter 参数的 field 时省略属性的类型注解。虽然这可以减少此 PEP 引入的常见重复(例如 x: str = field(converter=str)),但尚不清楚如何最好地支持这一点,同时保持当前的数据类语义(即,属性顺序对于已生成 __init__ 或 dataclasses.fields 等很重要)。这是因为在 Python 中(目前)没有简单的方法可以将仅注解的属性与未注解的属性按其定义顺序交错。
可以应用一个哨兵注解(例如 x: FromConverter = ...),但这会破坏类型注解的一个基本假设。
最后,如果*所有*字段(包括没有 converter 的字段)都赋值给 dataclasses.field,则这是可行的,这意味着类自己的命名空间保留了顺序,但这会将类型+converter 的重复与字段赋值的重复进行权衡。最终结果是重复项的数量没有增减,但增加了数据类语义的复杂性。
本 PEP 并不建议不能或不应该这样做。只是这个 PEP 中没有包含。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源: https://github.com/python/peps/blob/main/peps/pep-0712.rst
最后修改: 2025-02-01 08:55:40 GMT