Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 681 – 数据类转换

作者:
Erik De Bonte <erikd at microsoft.com>, Eric Traut <erictr at microsoft.com>
赞助商:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
讨论地址:
Typing-SIG 线程
状态:
最终
类型:
标准轨道
主题:
类型提示
创建日期:
2021-12-02
Python 版本:
3.11
历史记录:
2021-04-24, 2021-12-13, 2022-02-22
决议:
Python-Dev 消息

目录

注意

此 PEP 是一份历史文档:请参阅 dataclass_transform 装饰器@typing.dataclass_transform 获取最新的规范和文档。规范的类型提示维护在 类型提示规范网站 上;运行时类型提示行为在 CPython 文档中描述。

×

有关如何提出更改类型提示规范的建议,请参阅 类型提示规范更新流程

摘要

PEP 557 将数据类引入 Python 标准库。一些流行的库具有类似于数据类的行为,但这些行为无法使用标准类型提示来描述。这些项目包括 attrs、pydantic 和对象关系映射器 (ORM) 包,例如 SQLAlchemy 和 Django。

大多数类型检查器、代码风格检查器和语言服务器都全面支持数据类。本提案旨在将此功能泛化,并提供一种方法让第三方库表明某些装饰器函数、类和元类提供类似于数据类的行为。

这些行为包括

  • 基于声明的数据字段合成 __init__ 方法。
  • 可选地合成 __eq__, __ne__, __lt__, __le__, __gt____ge__ 方法。
  • 支持“冻结”类,这是一种在静态类型检查期间强制不可变性的方法。
  • 支持“字段说明符”,它描述了静态类型检查器必须注意的各个字段的属性,例如是否为该字段提供默认值。

标准库数据类的完整行为在 Python 文档 中描述。

本提案不影响 CPython,除了在 typing.py 中添加 dataclass_transform 装饰器。

动机

目前没有标准方法可以让具有数据类语义的库向类型检查器声明其行为。为了解决此限制,Mypy 为许多这些库开发了自定义插件,但这些插件无法与其他类型检查器、代码风格检查器或语言服务器配合使用。它们对于库作者来说维护成本也较高,而且它们要求 Python 开发人员了解这些插件的存在,并在其环境中下载和配置它们。

理由

本提案的意图不是支持具有数据类语义的每个库的每个特性,而是让以与静态类型检查兼容的方式使用这些库中最常见的特性成为可能。如果用户重视这些库并重视静态类型检查,他们可能需要避免使用某些特性,或者对使用这些库的方式进行一些小的调整。对于 Mypy 自定义插件,这已经是事实了,因为它们并不支持每个数据类库的每个特性。

随着将来为数据类添加新特性,我们打算在适当的时候,为 dataclass_transform 添加对这些特性的支持。保持这两个特性集同步将使数据类用户更容易理解和使用 dataclass_transform,并简化类型检查器中数据类支持的维护。

此外,我们将来会考虑为 dataclass_transform 添加对已被多个第三方库采用但未被数据类支持的特性的支持。

规范

The dataclass_transform 装饰器

本规范在 typing 模块中引入了一个名为 dataclass_transform 的新装饰器函数。此装饰器可以应用于本身是装饰器的函数、类或元类。 dataclass_transform 的存在告诉静态类型检查器,被装饰的函数、类或元类执行运行时“魔法”,转换类,赋予它类似于数据类的行为。

如果将 dataclass_transform 应用于函数,则将假设使用被装饰的函数作为装饰器会应用类似于数据类的语义。如果该函数具有重载,则可以将 dataclass_transform 装饰器应用于该函数的实现,或者应用于一个(而不是多个)重载。当应用于重载时, dataclass_transform 装饰器仍然会影响对该函数的所有使用。

如果将 dataclass_transform 应用于类,则将假设类似于数据类的语义适用于直接或间接从被装饰的类派生的任何类,或者使用被装饰的类作为元类的任何类。被装饰的类及其基类的属性不被视为字段。

以下部分显示了每种方法的示例。每个示例都创建一个具有类似于数据类语义的 CustomerModel 类。为了简洁起见,省略了被装饰对象的实现,但我们假设它们以以下方式修改类

  • 它们使用在该类及其父类中声明的数据字段合成 __init__ 方法。
  • 它们合成 __eq____ne__ 方法。

支持此 PEP 的类型检查器将识别 CustomerModel 类可以使用合成的 __init__ 方法进行实例化

# Using positional arguments
c1 = CustomerModel(327, "John Smith")

# Using keyword arguments
c2 = CustomerModel(id=327, name="John Smith")

# These calls will generate runtime errors and should be flagged as
# errors by a static type checker.
c3 = CustomerModel()
c4 = CustomerModel(327, first_name="John")
c5 = CustomerModel(327, "John Smith", 0)

装饰器函数示例

_T = TypeVar("_T")

# The ``create_model`` decorator is defined by a library.
# This could be in a type stub or inline.
@typing.dataclass_transform()
def create_model(cls: Type[_T]) -> Type[_T]:
    cls.__init__ = ...
    cls.__eq__ = ...
    cls.__ne__ = ...
    return cls

# The ``create_model`` decorator can now be used to create new model
# classes, like this:
@create_model
class CustomerModel:
    id: int
    name: str

类示例

# The ``ModelBase`` class is defined by a library. This could be in
# a type stub or inline.
@typing.dataclass_transform()
class ModelBase: ...

# The ``ModelBase`` class can now be used to create new model
# subclasses, like this:
class CustomerModel(ModelBase):
    id: int
    name: str

元类示例

# The ``ModelMeta`` metaclass and ``ModelBase`` class are defined by
# a library. This could be in a type stub or inline.
@typing.dataclass_transform()
class ModelMeta(type): ...

class ModelBase(metaclass=ModelMeta): ...

# The ``ModelBase`` class can now be used to create new model
# subclasses, like this:
class CustomerModel(ModelBase):
    id: int
    name: str

装饰器函数和类/元类参数

提供类似于数据类功能的装饰器函数、类或元类可能会接受修改某些行为的参数。本规范定义了以下参数,如果静态类型检查器使用这些参数,则必须遵守这些参数。这些参数中的每一个都接受一个 bool 参数,并且必须能够对 bool 值 (TrueFalse) 进行静态评估。

  • eq, order, frozen, initunsafe_hash 是标准库数据类中支持的参数,其含义在 PEP 557 中定义。
  • kw_only, match_argsslots 是标准库数据类中支持的参数,首次在 Python 3.10 中引入。

dataclass_transform 参数

dataclass_transform 的参数允许对默认行为进行一些基本自定义

_T = TypeVar("_T")

def dataclass_transform(
    *,
    eq_default: bool = True,
    order_default: bool = False,
    kw_only_default: bool = False,
    field_specifiers: tuple[type | Callable[..., Any], ...] = (),
    **kwargs: Any,
) -> Callable[[_T], _T]: ...
  • eq_default 指示如果调用者省略 eq 参数,则假设该参数为 True 或 False。如果未指定, eq_default 将默认为 True(数据类的默认假设)。
  • order_default 指示如果调用者省略 order 参数,则假设该参数为 True 或 False。如果未指定, order_default 将默认为 False(数据类的默认假设)。
  • kw_only_default 指示如果调用者省略 kw_only 参数,则假设该参数为 True 或 False。如果未指定, kw_only_default 将默认为 False(数据类的默认假设)。
  • field_specifiers 指定一个静态列表,其中包含描述字段的受支持类。一些库还提供函数来分配字段规范实例,这些函数也可以在该元组中指定。如果未指定,field_specifiers 将默认为空元组(不支持字段规范)。标准数据类行为仅支持一种称为 Field 的字段规范类型,以及一个实例化此类的辅助函数 (field),因此,如果我们要描述 stdlib 数据类行为,我们将提供元组参数 (dataclasses.Field, dataclasses.field)
  • kwargs 允许将任意其他关键字参数传递给 dataclass_transform。这为类型检查器提供了支持实验参数的自由度,而无需等待 typing.py 中的更改。类型检查器应报告对任何无法识别的参数的错误。

将来,我们可能会根据需要向 dataclass_transform 添加其他参数来支持用户代码中的常见行为。这些添加将在达成 typing-sig 共识后进行,而不是通过额外的 PEP。

以下部分提供其他示例,展示了这些参数的使用方式。

装饰器函数示例

# Indicate that the ``create_model`` function assumes keyword-only
# parameters for the synthesized ``__init__`` method unless it is
# invoked with ``kw_only=False``. It always synthesizes order-related
# methods and provides no way to override this behavior.
@typing.dataclass_transform(kw_only_default=True, order_default=True)
def create_model(
    *,
    frozen: bool = False,
    kw_only: bool = True,
) -> Callable[[Type[_T]], Type[_T]]: ...

# Example of how this decorator would be used by code that imports
# from this library:
@create_model(frozen=True, kw_only=False)
class CustomerModel:
    id: int
    name: str

类示例

# Indicate that classes that derive from this class default to
# synthesizing comparison methods.
@typing.dataclass_transform(eq_default=True, order_default=True)
class ModelBase:
    def __init_subclass__(
        cls,
        *,
        init: bool = True,
        frozen: bool = False,
        eq: bool = True,
        order: bool = True,
    ):
        ...

# Example of how this class would be used by code that imports
# from this library:
class CustomerModel(
    ModelBase,
    init=False,
    frozen=True,
    eq=False,
    order=False,
):
    id: int
    name: str

元类示例

# Indicate that classes that use this metaclass default to
# synthesizing comparison methods.
@typing.dataclass_transform(eq_default=True, order_default=True)
class ModelMeta(type):
    def __new__(
        cls,
        name,
        bases,
        namespace,
        *,
        init: bool = True,
        frozen: bool = False,
        eq: bool = True,
        order: bool = True,
    ):
        ...

class ModelBase(metaclass=ModelMeta):
    ...

# Example of how this class would be used by code that imports
# from this library:
class CustomerModel(
    ModelBase,
    init=False,
    frozen=True,
    eq=False,
    order=False,
):
    id: int
    name: str

字段说明符

大多数支持数据类语义的库都提供一个或多个“字段规范”类型,这些类型允许类定义提供有关类中每个字段的更多元数据。例如,此元数据可以描述默认值,或者指示是否应将字段包含在合成的 __init__ 方法中。

在不需要其他元数据的情况下,可以省略字段规范。

@dataclass
class Employee:
    # Field with no specifier
    name: str

    # Field that uses field specifier class instance
    age: Optional[int] = field(default=None, init=False)

    # Field with type annotation and simple initializer to
    # describe default value
    is_paid_hourly: bool = True

    # Not a field (but rather a class variable) because type
    # annotation is not provided.
    office_number = "unassigned"

字段说明符参数

支持数据类语义和支持字段规范类的库通常使用通用参数名称来构造这些字段规范。此规范正式化了静态类型检查器必须理解的参数的名称和含义。这些标准化参数必须是关键字参数。

这些参数是 dataclasses.field 支持的那些参数的超集,排除了对类型检查没有影响的参数,例如 comparehash

字段规范类允许在其构造函数中使用其他参数,这些参数可以是位置参数,并且可以使用其他名称。

  • init 是一个可选的布尔值参数,指示是否应将字段包含在合成的 __init__ 方法中。如果未指定,init 默认为 True。字段规范函数可以使用隐式使用字面布尔值类型 (Literal[False]Literal[True]) 指定 init 值的重载。
  • default 是一个可选参数,它提供字段的默认值。
  • default_factory 是一个可选参数,它提供一个运行时回调,该回调返回字段的默认值。如果既未指定 default 也未指定 default_factory,则假设该字段没有默认值,并且在实例化类时必须提供一个值。
  • factorydefault_factory 的别名。stdlib 数据类使用名称 default_factory,但 attrs 在许多情况下使用名称 factory,因此此别名对于支持 attrs 来说是必要的。
  • kw_only 是一个可选的布尔值参数,指示是否应将字段标记为仅关键字参数。如果为 true,则该字段将仅为关键字参数。如果为 false,则它将不是仅关键字参数。如果未指定,将使用用 dataclass_transform 装饰的对象上的 kw_only 参数的值,或者如果该参数未指定,则将使用 dataclass_transform 上的 kw_only_default 值。
  • alias 是一个可选的 str 参数,它提供字段的备用名称。此备用名称用于合成的 __init__ 方法中。

指定多个 defaultdefault_factoryfactory 是错误的。

此示例演示了上述内容。

# Library code (within type stub or inline)
# In this library, passing a resolver means that init must be False,
# and the overload with Literal[False] enforces that.
@overload
def model_field(
        *,
        default: Optional[Any] = ...,
        resolver: Callable[[], Any],
        init: Literal[False] = False,
    ) -> Any: ...

@overload
def model_field(
        *,
        default: Optional[Any] = ...,
        resolver: None = None,
        init: bool = True,
    ) -> Any: ...

@typing.dataclass_transform(
    kw_only_default=True,
    field_specifiers=(model_field, ))
def create_model(
    *,
    init: bool = True,
) -> Callable[[Type[_T]], Type[_T]]: ...

# Code that imports this library:
@create_model(init=False)
class CustomerModel:
    id: int = model_field(resolver=lambda : 0)
    name: str

运行时行为

在运行时,dataclass_transform 装饰器的唯一作用是在装饰的函数或类上设置名为 __dataclass_transform__ 的属性,以支持自省。该属性的值应该是一个字典,它将 dataclass_transform 参数的名称映射到它们的值。

例如。

{
  "eq_default": True,
  "order_default": False,
  "kw_only_default": False,
  "field_specifiers": (),
  "kwargs": {}
}

数据类语义

除非在本 PEP 中另有说明,否则受 dataclass_transform 影响的类,无论是通过继承用 dataclass_transform 装饰的类,还是通过用用 dataclass_transform 装饰的函数装饰,都假定表现得像 stdlib dataclass 一样。

这包括但不限于以下语义。

  • 冻结的数据类不能继承自非冻结的数据类。用 dataclass_transform 装饰的类既不被认为是冻结的,也不被认为是非冻结的,从而允许冻结的类继承自它。同样,直接指定用 dataclass_transform 装饰的元类的类,既不被认为是冻结的,也不被认为是非冻结的。

    考虑这些类示例。

    # ModelBase is not considered either "frozen" or "non-frozen"
    # because it is decorated with ``dataclass_transform``
    @typing.dataclass_transform()
    class ModelBase(): ...
    
    # Vehicle is considered non-frozen because it does not specify
    # "frozen=True".
    class Vehicle(ModelBase):
        name: str
    
    # Car is a frozen class that derives from Vehicle, which is a
    # non-frozen class. This is an error.
    class Car(Vehicle, frozen=True):
        wheel_count: int
    

    以及这些类似的元类示例。

    @typing.dataclass_transform()
    class ModelMeta(type): ...
    
    # ModelBase is not considered either "frozen" or "non-frozen"
    # because it directly specifies ModelMeta as its metaclass.
    class ModelBase(metaclass=ModelMeta): ...
    
    # Vehicle is considered non-frozen because it does not specify
    # "frozen=True".
    class Vehicle(ModelBase):
        name: str
    
    # Car is a frozen class that derives from Vehicle, which is a
    # non-frozen class. This is an error.
    class Car(Vehicle, frozen=True):
        wheel_count: int
    
  • 假定字段排序和继承遵循 557 中指定的规则。这包括覆盖的影响(在子类中重新定义已在父类中定义的字段)。
  • PEP 557 指出 所有没有默认值的字段都必须出现在有默认值的字段之前。尽管 PEP 557 中没有明确说明,但在 init=False 时会忽略此规则,本规范在那种情况下也忽略此要求。同样,当仅关键字参数用于 __init__ 时,无需强制执行此排序,因此如果 kw_only 语义有效,则不会强制执行此规则。
  • dataclass 一样,如果方法合成会覆盖类中显式声明的方法,则会跳过方法合成。基类上的方法声明不会导致方法合成被跳过。

    例如,如果一个类显式声明一个 __init__ 方法,则不会为该类合成 __init__ 方法。

  • KW_ONLY 哨兵值的支持方式如 Python 文档bpo-43532 中所述。
  • ClassVar 属性不被认为是数据类字段,并且 被数据类机制忽略

未定义的行为

如果在单个函数(包括其重载)、单个类或类层次结构中找到多个 dataclass_transform 装饰器,则结果行为未定义。库作者应避免这些情况。

参考实现

Pyright 包含 dataclass_transform 的类型检查器支持的参考实现。Pyright 的 dataClasses.ts 源文件 将是理解实现的良好起点。

attrspydantic 库正在使用 dataclass_transform,并作为其用法的实际示例。

被拒绝的想法

auto_attribs 参数

attrs 库支持一个 auto_attribs 参数,指示是否应将用 PEP 526 变量注释装饰但没有赋值的类成员视为数据字段。

我们考虑过支持 auto_attribs 和相应的 auto_attribs_default 参数,但最终决定不这样做,因为它特定于 attrs。

Django 不支持仅使用类型注释声明字段,因此使用 dataclass_transform 的 Django 用户应该注意,他们应该始终提供分配的值。

cmp 参数

attrs 库支持一个布尔值参数 cmp,它等效于将 eqorder 都设置为 True。我们选择不支持 cmp 参数,因为它仅适用于 attrs。用户可以通过使用 eqorder 参数名称来模拟 cmp 行为。

自动字段名别名

attrs 库会对以单个下划线开头的字段名称进行 自动别名化,从相应的 __init__ 参数的名称中删除下划线。

本提案省略了该行为,因为它特定于 attrs。用户可以使用 alias 参数手动为这些字段设置别名。

替代字段排序算法

attrs 库当前支持两种在类中排序字段的方法。

  • 数据类顺序:与数据类使用相同的排序方式。这是较旧 API(例如 attr.s)的默认行为。
  • 方法解析顺序 (MRO):这是较新 API(例如 define、mutable、frozen)的默认行为。较旧的 API(例如 attr.s)可以通过指定 collect_by_mro=True 来选择此行为。

在某些菱形多重继承场景中,最终的字段排序可能会不同。

为了简单起见,此提案不支持除数据类使用之外的任何字段排序。

在子类中重新声明的字段

attrs 库在处理子类中重新声明的继承字段的方式上与标准库数据类不同。数据类规范保留原始顺序,但 attrs 根据子类定义了新的顺序。

为了简单起见,我们选择只支持数据类行为。依赖于 attrs 特定排序的 attrs 用户将不会在合成的 __init__ 方法中看到预期的参数顺序。

Django 主键和外键

Django 对 主键和外键应用了附加逻辑。例如,如果不存在被指定为主键的字段,它会自动添加一个 id 字段(以及 __init__ 参数)。

由于这在数据类库中并不广泛适用,因此此附加逻辑在本提案中没有得到处理,因此 Django 用户需要明确声明 id 字段。

类级默认值

SQLAlchemy 请求我们提供一种方法来指定转换后的类中所有字段的默认值为 None。通常所有 SQLAlchemy 字段都是可选的,而 None 表示该字段未设置。

我们选择不支持此功能,因为它特定于 SQLAlchemy。用户可以手动在这些字段上设置 default=None

描述符类型字段支持

我们考虑在 dataclass_transform 上添加一个布尔参数,以更好地支持带有描述符类型的字段,这在 SQLAlchemy 中很常见。启用后,合成 __init__ 方法上对应于描述符类型字段的每个参数的类型将是描述符的 __set__ 方法的值参数的类型,而不是描述符类型本身。类似地,在设置字段时,将期望 __set__ 值类型。并且,当获取字段的值时,其类型应该与 __get__ 的返回值类型匹配。

这个想法是基于这样的信念,即 dataclass 不正确地支持描述符类型字段。实际上它确实支持,但类型检查器(至少是 mypy 和 pyright)没有反映运行时行为,这导致了我们的误解。有关更多详细信息,请参阅 Pyright 问题

converter 字段说明符参数

attrs 库支持一个 converter 字段规范器参数,它是一个 Callable,由生成的 __init__ 方法调用,将提供的 value 转换为一些其他所需 value。这很难支持,因为合成 __init__ 方法中的参数类型需要接受未覆盖的值,但结果字段的类型将根据转换器的输出进行类型化。

此问题的一些方面在 Pyright 讨论 中有详细说明。

可能没有很好的方法来支持这一点,因为没有足够的信息来推导出输入参数的类型。一种可能的解决方案是添加对 converter 字段规范器参数的支持,但随后在 __init__ 方法中使用 Any 类型作为相应的参数。


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

最后修改时间:2024-06-11 22:12:09 GMT