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

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月2日
Python 版本:
3.11
发布历史:
2021年4月24日, 2021年12月13日, 2022年2月22日
决议:
Python-Dev 消息

目录

重要

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

×

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

摘要

PEP 557 将数据类引入了 Python 标准库。一些流行的库具有与数据类相似的行为,但这些行为无法使用标准类型注释进行描述。此类项目包括 attrs、pydantic 以及 SQLAlchemy 和 Django 等对象关系映射器 (ORM) 包。

大多数类型检查器、代码检查器和语言服务器都完全支持数据类。本提案旨在概括此功能,并为第三方库提供一种方法,以指示某些装饰器函数、类和元类提供类似于数据类的行为。

这些行为包括

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

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

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

动机

目前没有现成的标准方法让具有类似数据类语义的库向类型检查器声明其行为。为了解决这个限制,许多这些库已经开发了 Mypy 自定义插件,但这些插件不适用于其他类型检查器、linter 或语言服务器。对于库作者来说,维护这些插件的成本也很高,并且它们要求 Python 开发人员了解这些插件的存在并下载和配置它们。

基本原理

本提案的目的不是支持每个具有数据类相似语义的库的所有功能,而是为了使这些库最常用的功能能够以与静态类型检查兼容的方式使用。如果用户重视这些库,也重视静态类型检查,他们可能需要避免使用某些功能或对其使用方式进行微小调整。对于 Mypy 自定义插件来说,情况已经如此,它们不支持每个数据类相似库的所有功能。

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

此外,我们未来将考虑为多个第三方库采用但数据类不支持的功能添加 dataclass_transform 支持。

规范

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

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

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

  • eq, order, frozen, initunsafe_hash 是标准库数据类中支持的参数,其含义在 PEP 557 中定义。
  • kw_onlymatch_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),因此如果我们描述标准库数据类行为,我们将提供元组参数 (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 是一个可选参数,它提供一个运行时回调,返回字段的默认值。如果 defaultdefault_factory 都未指定,则该字段被假定为没有默认值,并且在实例化类时必须提供一个值。
  • factorydefault_factory 的别名。stdlib 数据类使用名称 default_factory,但 attrs 在许多情况下使用名称 factory,因此此别名对于支持 attrs 是必需的。
  • kw_only 是一个可选的布尔参数,指示该字段是否应标记为仅限关键字。如果为 True,则该字段将是仅限关键字的。如果为 False,则不会是仅限关键字的。如果未指定,则使用被 dataclass_transform 装饰的对象上的 kw_only 参数的值,或者如果该值未指定,则使用 dataclass_transform 上的 kw_only_default 的值。
  • alias 是一个可选的字符串参数,它为字段提供一个替代名称。此替代名称在合成的 __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 装饰的函数装饰,都被假定为行为与标准库 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
    
  • 字段排序和继承被假定遵循 PEP 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 源文件将是理解实现的一个很好的起点。

attrs 和 pydantic 库正在使用 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__ 方法调用,将提供的值转换为其他所需值。这很难支持,因为合成的 __init__ 方法中的参数类型需要接受未转换的值,但结果字段的类型是根据转换器的输出确定的。

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

可能没有好的方法来支持这一点,因为没有足够的信息来推导输入参数的类型。一个可能的解决方案是添加对 converter 字段说明符参数的支持,然后对 __init__ 方法中的相应参数使用 Any 类型。


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

最后修改: 2025-02-01 07:28:42 GMT