"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, match_args=True, kw_only=False, slots=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,
                 match_args=True, kw_only=False, slots=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"。 参见下文的讨论。

   * "match_args": 如果为真值 (默认值为 "True")，则将根据传给生成的
     "__init__()" 方法的形参列表来创建 "__match_args__" 元组 (即使没有
     生成 "__init__()" 也会创建，见上文)。 如果为假值，或者如果
     "__match_args__" 已在类中定义，则将不生成 "__match_args__"。

      3.10 版新加入.

   * "kw_only": 如果为真值 (默认值为 "False")，则所有字段都将被标记为
     仅限关键字。 如果一个字段被标记为仅限关键字，则其唯一的影响是根据
     仅限关键字的字段生成的 "__init__()" 形参必须使用调用 "__init__()"
     时传入的关键字来指定。 对于 dataclass 的任何其它方面都没有影响。
     请参阅 *parameter* 术语表条目了解详情。 另请参阅 "KW_ONLY" 一节。

      3.10 版新加入.

   * "slots": 如果为真值 (默认值为 "False")，则将生成 "__slots__" 属性
     并将返回一个新类而非原来的类。 如果 "__slots__" 已在类中定义，则
     会引发 "TypeError"。

      3.10 版新加入.

   可以用普通的 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, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING)

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

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

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

   如上所示，"MISSING" 值是一个哨兵对象，用于检测一些形参是否由用户提
   供。使用它是因为 "None" 对于一些形参来说是有效的用户值。任何代码都
   不应该直接使用 "MISSING" 值。

   "field()" 的形参有：

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

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

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

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

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

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

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

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

   * "kw_only": 如果为真值，则此字段将被标记为仅限关键字。 这将在当计
     算出所生成的 "__init__()" 方法的形参时被使用。

      3.10 版新加入.

   如果通过调用 "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" 和 "kw_only" 具有与 "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, match_args=True, kw_only=False, slots=False)

   新建一个名为 "cls_name" 的数据类，其字段在 "fields" 中定义，基类在
   "bases" 中给出，并使用在 "namespace" 中给出的命名空间进行初始化。
   "fields" 是一个可迭代对象，其中的每个元素均为 "name", "(name,
   type)" 或 "(name, type, Field)"。 如果只提供了 "name"，则
   "typing.Any" 会被用作 "type"。 "init", "repr", "eq", "order",
   "unsafe_hash", "frozen", "match_args", "kw_only" 和  "slots" 等值与
   它们在 "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"。

   提前提醒 "init=False" 字段在 "replace()" 被调用时的行为。如果它们被
   初始化的话，它们不是从源对象复制的，而是在 "__post_init__()" 中初始
   化。除非保持审慎，否则 "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)

dataclasses.MISSING

   一个指明“没有提供 default 或 default_factory”的监视值。

dataclasses.KW_ONLY

   一个用作类型标注的监视值。 任何在伪字段之后的类型为 "KW_ONLY" 的字
   段会被标记为仅限关键字字段。 请注意在其他情况下 "KW_ONLY" 类型的伪
   字段会被完全忽略。 这包括此类字段的名称。 根据惯例，名称 "_" 会被用
   作 "KW_ONLY" 字段。 仅限关键字字段指明当类被实例化时 "__init__()"
   形参必须以关键字形式来指定。

   在这个例子中，字段 "y" 和 "z" 将被标记为仅限关键字字段:

      @dataclass
      class Point:
        x: float
        _: KW_ONLY
        y: float
        z: float

      p = Point(0, y=1.5, z=2.0)

   在单个数据类中，指定一个以上 "KW_ONLY" 类型的字段将导致错误。

   3.10 版新加入.

exception dataclasses.FrozenInstanceError

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


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

生成的 "__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()" 函数返回。


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

Another place where "dataclass()" inspects a type annotation is to
determine if a field is an init-only variable.  It does this by seeing
if the type of a field is of type "dataclasses.InitVar".  If a field
is an "InitVar", it is considered a pseudo-field called an init-only
field.  As it is not a true field, it is not returned by the module-
level "fields()" function.  Init-only fields are added as parameters
to the generated "__init__()" method, and are passed to the optional
"__post_init__()" method.  They are not otherwise used by dataclasses.

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

   @dataclass
   class C:
       i: int
       j: int | None = None
       database: InitVar[DatabaseType | None] = 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):


"__init__()" 中仅限关键字字段的重新排序
=======================================

在计算出 "__init__()" 所需要的形参之后，任何仅限关键字形参会被移至所有
常规（非仅限关键字）形参的后面。 这是 Python 中实现仅限关键字形参所要
求的：它们必须位于非仅限关键字形参之后。

在这个例子中，"Base.y", "Base.w", and "D.t" 是仅限关键字字段，而
"Base.x" 和 "D.z" 是常规字段:

   @dataclass
   class Base:
       x: Any = 15.0
       _: KW_ONLY
       y: int = 0
       w: int = 1

   @dataclass
   class D(Base):
       z: int = 10
       t: int = field(kw_only=True, default=0)

为 "D" 生成的 "__init__()" 方法看起来像是这样:

   def __init__(self, x: Any = 15.0, z: int = 10, *, y: int = 0, w: int = 1, t: int = 0):

请注意形参原来在字段列表中出现的位置已被重新排序：前面是来自常规字段的
形参而后面是来自仅限关键字字段的形参。

仅限关键字形参的相对顺序会在重新排序的 "__init__()" 形参列表中保持原样
。


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

   如果一个 "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" 类的原始示例具有相同的问题。 也就是说，当在创建类实例
   的时候 "D" 类的两个实例没有为 "x" 指定值则将共享同一个 "x" 的副本。
   因为数据类只是使用普通的 Python 类创建方式所以它们也会共享此行为。
   数据类没有任何通用方式来检测这种情况。 相反地，"dataclass()" 装饰器
   在检测到类型为 "list", "dict" 或 "set" 的默认形参时将会引发
   "TypeError"。 这是一个部分解决方案，但它确实能防止许多常见错误。

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

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

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


字段标注描述器类型
==================

当字段被 描述器对象 赋值为默认值时会遵循以下行为:

* 字段的值被传递到数据类的 "__init__" 方法时，会传递给描述器的
  "__set__" 方法，而不会覆盖掉描述器对象。

* 相似的是，当我们获取或设置字段的值时，不会覆盖或返回描述器对象，而是
  会调用描述器的 "__get__" 或 "__set__" 方法后返回。

* 检测字段是否存在默认值时，"dataclasses``会通过类方法的形式去调用描述
  器的``__get__``方法(即为``descriptor.__get__(obj=None, type=cls)")。
  如果描述器有返回值，则返回值为字段的默认值，若调用描述器时抛出
  "AttributeError" 错误，则该字段无默认值。

   class IntConversionDescriptor:
     def __init__(self, *, default):
       self._default = default

     def __set_name__(self, owner, name):
       self._name = "_" + name

     def __get__(self, obj, type):
       if obj is None:
         return self._default

       return getattr(obj, self._name, self._default)

     def __set__(self, obj, value):
       setattr(obj, self._name, int(value))

   @dataclass
   class InventoryItem:
     quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)

   i = InventoryItem()
   print(i.quantity_on_hand)   # 100
   i.quantity_on_hand = 2.5    # calls __set__ with 2.5
   print(i.quantity_on_hand)   # 2

如果一个字段被描述器标注类型，但默认值并不是描述器对象，那么该字段就只
能像平常的字段一样工作。
