"dataclasses" --- 数据类
************************

**源码：** Lib/dataclasses.py

======================================================================

这个模块提供了一个装饰器和一些函数，用于自动添加生成的 *special
method*，例如 "__init__()" 和 "__repr__()" 到用户定义的类。 它最初描述
于 **PEP 557** 。

在这些生成的方法中使用的成员变量是使用 **PEP 526** 类型标注来定义的。
例如以下代码：

   from dataclasses import dataclass

   @dataclass
   class InventoryItem:
       """Class for keeping track of an item in inventory."""
       name: str
       unit_price: float
       quantity_on_hand: int = 0

       def total_cost(self) -> float:
           return self.unit_price * self.quantity_on_hand

将在添加的内容中包括如下所示的 "__init__()":

   def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
       self.name = name
       self.unit_price = unit_price
       self.quantity_on_hand = quantity_on_hand

请注意，此方法会自动添加到类中：而不是在如上所示的 "InventoryItem" 定
义中被直接指定。

3.7 新版功能.


模块级装饰器、类和函数
======================

@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

   这个函数是 *decorator* ，用于将生成的 *special method* 添加到类中，
   如下所述。

   "dataclass()" 装饰器会检查类以查找 "field" —— "field" 被定义为具有
   *类型标注* 的类变量。除了下面描述的两个例外，在 "dataclass()" 中没
   有什么东西会去检查这些变量标注成了何种类型。

   这些字段在所有生成的方法中的顺序，都是它们在类定义中出现的顺序。

   "dataclass()" 装饰器将向类中添加如下的各种 dunder 方法。如果所添加
   的方法已存在于类中，则行为将取决于下面所列出的形参。该装饰器会返回
   调用它的类；不会创建新的类。

   如果 "dataclass()" 仅用作没有参数的简单装饰器，它将使用它的函数签名
   中的默认值。也就是说，这三种 "dataclass()" 用法是等价的：

      @dataclass
      class C:
          ...

      @dataclass()
      class C:
          ...

      @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
      class C:
         ...

   "dataclass()" 的参数有：

   * "init": 如果为真值（默认），将生成一个 "__init__()" 方法。

     如果类已定义 "__init__()" ，则忽略此参数。

   * "repr" ：如果为真值（默认），将生成一个 "__repr__()" 方法。 生成
     的 repr 字符串将具有类名以及每个字段的名称和 repr ，按照它们在类
     中定义的顺序。不包括标记为从 repr 中排除的字段。 例如：
     "InventoryItem(name='widget', unit_price=3.0,
     quantity_on_hand=10)"。

     如果类已定义 "__repr__()" ，则忽略此参数。

   * "eq" ：如果为true（默认值），将生成 "__eq__()" 方法。此方法将类作
     为其字段的元组按顺序比较。比较中的两个实例必须是相同的类型。

     如果类已定义 "__eq__()" ，则忽略此参数。

   * "order" ：如果为真值（默认为 "False" ），则 "__lt__()" 、
     "__le__()" 、 "__gt__()" 和 "__ge__()" 方法将生成。 这将类作为其
     字段的元组按顺序比较。比较中的两个实例必须是相同的类型。如果
     "order" 为真值并且 "eq" 为假值 ，则引发 "ValueError" 。

     如果类已经定义了 "__lt__()" 、 "__le__()" 、 "__gt__()" 或者
     "__ge__()" 中的任意一个，将引发 "TypeError" 。

   * "unsafe_hash" ：如果为 "False" （默认值），则根据 "eq" 和
     "frozen" 的设置方式生成 "__hash__()" 方法。

     "__hash__()" 由内置的 "hash()" 使用，当对象被添加到散列集合（如字
     典和集合）时。有一个 "__hash__()" 意味着类的实例是不可变的。可变
     性是一个复杂的属性，取决于程序员的意图， "__eq__()" 的存在性和行
     为，以及 "dataclass()" 装饰器中 "eq" 和 "frozen" 标志的值。

     默认情况下， "dataclass()" 不会隐式添加 "__hash__()" 方法，除非这
     样做是安全的。 它也不会添加或更改现有的明确定义的 "__hash__()" 方
     法。 设置类属性 "__hash__ = None" 对 Python 具有特定含义，如
     "__hash__()" 文档中所述。

     如果 "__hash__()" 没有显式定义，或者它被设为 "None"，则
     "dataclass()" *可能* 会添加一个隐式 "__hash__()" 方法。 虽然并不
     推荐，但你可以用 "unsafe_hash=True" 来强制 "dataclass()" 创建一个
     "__hash__()" 方法。 如果你的类在逻辑上不可变但却仍然可被修改那么
     可能就是这种情况。 这是一个特殊用例并且应当被仔细地考虑。

     以下是隐式创建 "__hash__()" 方法的规则。请注意，你不能在数据类中
     都使用显式的 "__hash__()" 方法并设置 "unsafe_hash=True" ；这将导
     致 "TypeError" 。

     如果 "eq" 和 "frozen" 都是 true，默认情况下 "dataclass()" 将为你
     生成一个 "__hash__()" 方法。如果 "eq" 为 true 且 "frozen" 为
     false ，则 "__hash__()" 将被设置为 "None" ，标记它不可用（因为它
     是可变的）。如果 "eq" 为 false ，则 "__hash__()" 将保持不变，这意
     味着将使用超类的 "__hash__()" 方法（如果超类是 "object" ，这意味
     着它将回到基于id的hash）。

   * "frozen": 如为真值 (默认值为 "False")，则对字段赋值将会产生异常。
     这模拟了只读的冻结实例。 如果在类中定义了 "__setattr__()" 或
     "__delattr__()" 则将会引发 "TypeError"。 参见下文的讨论。

   可以用普通的 Python 语法为各个 "field" 指定默认值：

      @dataclass
      class C:
          a: int       # 'a' has no default value
          b: int = 0   # assign a default value for 'b'

   在这个例子中， "a" 和 "b" 都将包含在添加的 "__init__()" 方法中，它
   们将被定义为:

      def __init__(self, a: int, b: int = 0):

   如果在具有默认值的字段之后存在没有默认值的字段，将会引发
   "TypeError"。无论此情况是发生在单个类中还是作为类继承的结果，都是如
   此。

dataclasses.field(*, default=MISSING, default_factory=MISSING, repr=True, hash=None, init=True, compare=True, metadata=None)

   大多数时候，对于简单常见的用途，前述的功能已经足够了。而有些功能需
   要字段提供额外的信息来启用。为了满足这种对附加信息的需求，你可以通
   过调用提供的 "field()" 函数来替换字段默认值。例如：

      @dataclass
      class C:
          mylist: list[int] = field(default_factory=list)

      c = C()
      c.mylist += [1, 2, 3]

   如上所示， "MISSING" 值是一个 sentinel 对象，用于检测是否提供了
   "default" 和 "default_factory" 参数。 使用此 sentinel 是因为 "None"
   是 "default" 的有效值。没有代码应该直接使用 "MISSING" 值。

   "field()" 的形参有：

   * "default"：如果提供，这将是该字段的默认值。设计这个形参是因为
     "field()" 调用将会占据原来用来提供默认值的位置。

   * "default_factory"：如果提供，它必须是一个需要零个参数的可调用对象
     ，当该字段需要一个默认值时，它将被调用。这能解决当默认值是可变对
     象时会带来的问题，如下所述。同时指定 "default" 和
     "default_factory" 将产生错误。

   * "init" ：如果为true（默认值），则该字段作为参数包含在生成的
     "__init__()" 方法中。

   * "repr" ：如果为true（默认值），则该字段包含在生成的 "__repr__()"
     方法返回的字符串中。

   * "compare" ：如果为true（默认值），则该字段包含在生成的相等性和比
     较方法中（ "__eq__()" ， "__gt__()" 等等）。

   * "hash" ：这可以是布尔值或 "None" 。如果为true，则此字段包含在生成
     的 "__hash__()" 方法中。如果为 "None" （默认值），请使用
     "compare" 的值，这通常是预期的行为。如果字段用于比较，则应在 hash
     中考虑该字段。不鼓励将此值设置为 "None" 以外的任何值。

     设置 "hash=False" 但 "compare=True" 的一个合理情况是，一个计算哈
     希值的代价很高的字段是检验等价性需要的，且还有其他字段可以用于计
     算类型的哈希值。可以从哈希值中排除该字段，但仍令它用于比较。

   * "metadata"：可以是映射或 None。None 被视为一个空的字典。这个值将
     被包装在 "MappingProxyType()" 中，使其只读，并暴露在 "Field" 对象
     上。数据类不使用它——它是作为第三方扩展机制提供的。多个第三方可以
     各自拥有自己的键，以用作元数据中的命名空间。

   如果通过调用 "field()" 指定字段的默认值，则该字段对应的类属性的值将
   最终被替换为指定的 "default" 值。如果没有提供 "default"，那么将删除
   该字段对应的类属性。目的是在 "dataclass()" 装饰器运行之后，类属性将
   包含字段的默认值，和直接指定了默认值一样。例如，在运行如下代码之后
   ：

      @dataclass
      class C:
          x: int
          y: int = field(repr=False)
          z: int = field(repr=False, default=10)
          t: int = 20

   类属性 "C.z" 将是 "10"，类属性 "C.t" 将是 "20"，类属性 "C.x" 和
   "C.y" 将不设置。

class dataclasses.Field

   "Field" 对象描述每个已定义的字段。这些对象在内部被创建，并由
   "fields()" 模块级方法返回（见下）。用户永远不应该直接实例化 "Field"
   对象。它的下列属性的含义是由文档规定的：

      * "name"：字段的名称。

      * "type"：字段的类型。

      * "default" 、 "default_factory" 、 "init" 、 "repr" 、 "hash"
        、 "compare" 以及 "metadata" 与具有和 "field()" 声明中相同的意
        义和值。

   可能存在其他属性，但它们是私有的。用户不应检查或依赖于这些属性。

dataclasses.fields(class_or_instance)

   返回一个能描述此数据类所包含的字段的元组，元组的每一项都是 "Field"
   对象。接受数据类或数据类的实例。如果没有传递一个数据类或实例将引发
   "TypeError"。不返回 "ClassVar" 或 "InitVar" 等伪字段。

dataclasses.asdict(obj, *, dict_factory=dict)

   将数据类 "obj" 转换为一个字典（使用工厂函数 "dict_factory"）。每个
   数据类被转换为以 "name: value" 键值对来储存其字段的字典。数据类、字
   典、列表和元组的内容会被递归地访问。其它对象用 "copy.deepcopy()" 来
   复制。

   在嵌套的数据类上使用 "asdict()" 的例子：

      @dataclass
      class Point:
           x: int
           y: int

      @dataclass
      class C:
           mylist: list[Point]

      p = Point(10, 20)
      assert asdict(p) == {'x': 10, 'y': 20}

      c = C([Point(0, 0), Point(10, 4)])
      assert asdict(c) == {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}

   要创建一个浅拷贝，可以使用以下的变通方法：

      dict((field.name, getattr(obj, field.name)) for field in fields(obj))

   如果 "obj" 不是一个数据类实例，"asdict()" 引发 "TypeError"。

dataclasses.astuple(obj, *, tuple_factory=tuple)

   将数据类 "obj" 转换为一个元组（使用工厂函数 "tuple_factory"）。每个
   数据类被转换为其字段的值的元组。数据类、字典、列表和元组的内容会被
   递归地访问。其它对象用 "copy.deepcopy()" 来复制。

   继续前一个例子：

      assert astuple(p) == (10, 20)
      assert astuple(c) == ([(0, 0), (10, 4)],)

   要创建一个浅拷贝，可以使用以下的变通方法：

      tuple(getattr(obj, field.name) for field in dataclasses.fields(obj))

   如果 "obj" 不是一个数据类实例，"astuple()" 引发 "TypeError"。

dataclasses.make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

   创建一个名为 "cls_name" 的新数据类，字段为 "fields" 中定义的字段，
   基类为 "bases" 中给出的基类，并使用 "namespace" 中给出的命名空间进
   行初始化。 "fields" 是一个可迭代的元素，每个元素都是 "name" 、
   "(name, type)" 或 "(name, type, Field)" 。 如果只提供``name`` ，
   "type" 为 "typing.Any" 。 "init" 、 "repr" 、 "eq" 、 "order" 、
   "unsafe_hash" 和 "frozen" 的值与它们在  "dataclass()" 中的含义相同
   。

   此函数不是必需的，因为任何用于创建带有 "__annotations__" 的新类的
   Python 机制都可以进一步用 "dataclass()" 函数将创建的类转换为数据类
   。提供此函数是为了方便。例如：

      C = make_dataclass('C',
                         [('x', int),
                           'y',
                          ('z', int, field(default=5))],
                         namespace={'add_one': lambda self: self.x + 1})

   等价于：

      @dataclass
      class C:
          x: int
          y: 'typing.Any'
          z: int = 5

          def add_one(self):
              return self.x + 1

dataclasses.replace(obj, /, **changes)

   创建一个与 "obj" 类型相同的新对象，将字段替换为 "changes" 里的值。
   如果 "obj" 不是数据类，则抛出 "TypeError" 。如果 "changes" 里的值没
   有指定要替换的字段名，则抛出 "TypeError"。

   新返回的对象通过调用数据类的 "__init__()" 方法创建。这确保了如果存
   在 "__post_init__()" ，其也被调用。

   如果存在没有默认值的仅初始化变量，必须在调用 "replace()" 时指定，以
   便它们可以传递给 "__init__()" 和 "__post_init__()" 。

   "changes" 包含任何定义为 "init=False" 的字段是错误的。在这种情况下
   会引发 "ValueError" 。

   提前提醒，  字段在 "replace()" 被调用时的行为是，当它们被初始化时，
   不是从源对象直接复制，而是在 "__post_init__()" 中初始化。除非保持审
   慎，否则 "init=False" 字段大概很少能被正确地使用。如果有使用
   "init=False" 的字段，那么使用另外的类构造器，或自定义 "replace()"
   方法（或类似名称的方法）来复制实例，可能会更好。

dataclasses.is_dataclass(obj)

   如果其形参为数据类，或其实例，返回 "True"，否则返回 "False"。

   如果你需要知道一个类是否是一个数据类的实例（而不是一个数据类本身）
   ，那么再添加一个 "not isinstance(obj, type)" 检查：

      def is_dataclass_instance(obj):
          return is_dataclass(obj) and not isinstance(obj, type)


初始化后处理
============

生成的 "__init__()" 代码将调用一个名为 "__post_init__()" 的方法，如果
在类上已经定义了 "__post_init__()" 。它通常被称为
"self.__post_init__()" 。但是，如果定义了任何 "InitVar" 字段，它们也将
按照它们在类中定义的顺序传递给 "__post_init__()" 。 如果没有
"__init__()" 方法生成，那么 "__post_init__()" 将不会被自动调用。

在其他用途中，这允许初始化依赖于一个或多个其他字段的字段值。例如:

   @dataclass
   class C:
       a: float
       b: float
       c: float = field(init=False)

       def __post_init__(self):
           self.c = self.a + self.b

由 "dataclass()" 所生成的 "__init__()" 方法不会调用基类的 "__init__()"
方法。 如果基类有需要被调用的 "__init__()" 方法，通常是在
"__post_init__()" 方法中调用此方法:

   @dataclass
   class Rectangle:
       height: float
       width: float

   @dataclass
   class Square(Rectangle):
       side: float

       def __post_init__(self):
           super().__init__(self.side, self.side)

但是请注意，一般来说 dataclass 生成的 "__init__()" 方法不需要被调用，
因为派生的 dataclass 将负责初始化任何自身为 dataclass 的基类的所有字段
。

有关将参数传递给 "__post_init__()" 的方法，请参阅下面有关仅初始化变量
的段落。另请参阅关于 "replace()" 处理 "init=False" 字段的警告。


类变量
======

两个地方 "dataclass()" 实际检查字段类型的之一是确定字段是否是如 **PEP
526** 所定义的类变量。它通过检查字段的类型是否为 "typing.ClassVar" 来
完成此操作。如果一个字段是一个 "ClassVar" ，它将被排除在考虑范围之外，
并被数据类机制忽略。这样的 "ClassVar" 伪字段不会由模块级的 "fields()"
函数返回。


仅初始化变量
============

另一个 "dataclass()" 检查类型注解地方是为了确定一个字段是否是一个仅初
始化变量。它通过查看字段的类型是否为 "dataclasses.InitVar" 类型来实现
。如果一个字段是一个 "InitVar" ，它被认为是一个称为仅初始化字段的伪字
段。因为它不是一个真正的字段，所以它不会被模块级的 "fields()" 函数返回
。仅初始化字段作为参数添加到生成的 "__init__()" 方法中，并传递给可选的
"__post_init__()" 方法。数据类不会使用它们。

例如，假设在创建类时没有为某个字段提供值，初始化时将从数据库中取值:

   @dataclass
   class C:
       i: int
       j: int = None
       database: InitVar[DatabaseType] = None

       def __post_init__(self, database):
           if self.j is None and database is not None:
               self.j = database.lookup('j')

   c = C(10, database=my_database)

在这种情况下， "fields()" 将返回 "i" 和 "j" 的 "Field" 对象，但不包括
"database" 。


冻结的实例
==========

无法创建真正不可变的 Python 对象。但是，通过将 "frozen=True" 传递给
"dataclass()" 装饰器，你可以模拟不变性。在这种情况下，数据类将向类添加
"__setattr__()" 和 "__delattr__()" 方法。 些方法在调用时会引发
"FrozenInstanceError" 。

使用 "frozen=True" 时会有很小的性能损失： "__ init__()" 不能使用简单的
赋值来初始化字段，并必须使用 "object.__setattr__()"。


继承
====

当数据类由 "dataclass()" 装饰器创建时，它会按反向 MRO 顺序（即，从
"object" 开始）查看它的所有基类，并且将找到的每个数据类的字段添加到一
个有序映射中。添加完所有基类字段后，它会将自己的字段添加到这个有序映射
中。所有生成的方法都将使用这个有序映射。字段会遵守它们被插入的顺序，因
此派生类会重写基类。一个例子：

   @dataclass
   class Base:
       x: Any = 15.0
       y: int = 0

   @dataclass
   class C(Base):
       z: int = 10
       x: int = 15

最后的字段列表依次是 "x" 、 "y" 、 "z" 。 "x" 的最终类型是 "int" ，如
类 "C" 中所指定的那样。

为 "C" 生成的 "__init__()" 方法看起来像:

   def __init__(self, x: int = 15, y: int = 0, z: int = 10):


默认工厂函数
============

   如果一个 "field()" 指定了一个 "default_factory" ，当需要该字段的默
   认值时，将使用零参数调用它。例如，要创建列表的新实例，请使用:

      mylist: list = field(default_factory=list)

   如果一个字段被排除在 "__init__()" 之外（使用 "init=False" ）并且字
   段也指定 "default_factory" ，则默认的工厂函数将始终从生成的
   "__init__()" 函数调用。发生这种情况是因为没有其他方法可以为字段提供
   初始值。


可变的默认值
============

   Python 在类属性中存储默认成员变量值。思考这个例子，不使用数据类:

      class C:
          x = []
          def add(self, element):
              self.x.append(element)

      o1 = C()
      o2 = C()
      o1.add(1)
      o2.add(2)
      assert o1.x == [1, 2]
      assert o1.x is o2.x

   请注意，类 "C" 的两个实例共享相同的类变量 "x" ，如预期的那样。

   使用数据类，*如果* 此代码有效：

      @dataclass
      class D:
          x: List = []
          def add(self, element):
              self.x += element

   它生成的代码类似于:

      class D:
          x = []
          def __init__(self, x=x):
              self.x = x
          def add(self, element):
              self.x += element

      assert D().x is D().x

   这与使用类 "C" 的原始示例具有相同的问题。也就是说，在创建类实例时没
   有为 "x" 指定值的类 "D" 的两个实例将共享相同的 "x" 副本。由于数据类
   只使用普通的 Python 类创建，因此它们也会共享此行为。数据类没有通用
   的方法来检测这种情况。相反，如果数据类检测到类型为 "list" 、 "dict"
   或 "set" 的默认参数，则会引发 "TypeError" 。这是一个部分解决方案，
   但它可以防止许多常见错误。

   使用默认工厂函数是一种创建可变类型新实例的方法，并将其作为字段的默
   认值:

      @dataclass
      class D:
          x: list = field(default_factory=list)

      assert D().x is not D().x


异常
====

exception dataclasses.FrozenInstanceError

   在使用 "frozen=True" 定义的数据类上调用隐式定义的 "__setattr__()"
   或 "__delattr__()" 时引发。 这是 "AttributeError" 的一个子类。
