"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 版新加入.

   "field"s 可以选择使用普通的 Python 语法指定默认值:

      @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" 的一个可能原因是，如果一个计
     算 hash 的代价很高的字段是检验等价性需要的，但还有其他字段可以计
     算类型的 hash 。 即使从 hash 中排除某个字段，它仍将用于比较。

   * "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)

   如果其形参为 dataclass 或其实例则返回 "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" 字段的警告。


类变量
======

One of the few places where "dataclass()" actually inspects the type
of a field is to determine if a field is a class variable as defined
in **PEP 526**.  It does this by checking if the type of the field is
"typing.ClassVar".  If a field is a "ClassVar", it is excluded from
consideration as a field and is ignored by the dataclass mechanisms.
Such "ClassVar" pseudo-fields are not returned by the module-level
"fields()" function.


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

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


Descriptor-typed fields
=======================

Fields that are assigned descriptor objects as their default value
have the following special behaviors:

* The value for the field passed to the dataclass's "__init__" method
  is passed to the descriptor's "__set__" method rather than
  overwriting the descriptor object.

* Similarly, when getting or setting the field, the descriptor's
  "__get__" or "__set__" method is called rather than returning or
  overwriting the descriptor object.

* To determine whether a field contains a default value, "dataclasses"
  will call the descriptor's "__get__" method using its class access
  form (i.e. "descriptor.__get__(obj=None, type=cls)".  If the
  descriptor returns a value in this case, it will be used as the
  field's default. On the other hand, if the descriptor raises
  "AttributeError" in this situation, no default value will be
  provided for the field.

   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

Note that if a field is annotated with a descriptor type, but is not
assigned a descriptor object as its default value, the field will act
like a normal field.
