dataclasses --- 数据类

源代码: Lib/dataclasses.py


此模块提供了一个装饰器和一些函数,用于自动为用户自定义的类添加诸如 __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, weakref_slot=False)

这个函数是一个装饰器,用于为类添加生成的特殊方法,如下所述。

@dataclass 装饰器检查类以寻找 field。一个 field 被定义为具有类型注解的类变量。除了下面描述的两个例外,@dataclass 不会检查变量注解中指定的类型。

所有生成方法中的字段顺序,就是它们在类定义中出现的顺序。

@dataclass 装饰器会向类中添加各种“双下划线”方法,如下所述。如果类中已存在任何被添加的方法,其行为取决于参数,具体如下文所述。装饰器返回其被调用的同一个类;不会创建新的类。

如果 @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, weakref_slot=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: 若为真值 (默认),将生成一个 __eq__() 方法。此方法会按顺序将类实例作为其字段的元组来进行比较。参与比较的两个实例必须为相同的类型。

    如果该类已经定义了 __eq__(),则此形参将被忽略。

  • order:如果为真值(默认为 False),将会生成 __lt__()__le__()__gt__()__ge__() 方法。这些方法会按顺序将类实例作为其字段的元组来进行比较。参与比较的两个实例必须为相同的类型。如果 order 为真值而 eq 为假值,则会引发 ValueError

    如果该类已定义了 __lt__()__le__()__gt__()__ge__() 中的任何一个,则会引发 TypeError

  • unsafe_hash: 如果为 true,则强制 dataclasses 创建一个 __hash__() 方法,即使这样做可能不安全。否则,将根据 eqfrozen 的设置来生成 __hash__() 方法。默认值为 False

    内置的 hash() 在对象被添加到哈希集合(如字典和集合)时会使用 __hash__()。拥有 __hash__() 意味着类的实例是不可变的。可变性是一个复杂的属性,取决于程序员的意图、__eq__() 的存在和行为,以及 @dataclass 装饰器中 eqfrozen 标志的值。

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

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

    以下是隐式创建 __hash__() 方法的规则。请注意,你不能在数据类中既有显式的 __hash__() 方法,又设置 unsafe_hash=True;这将导致 TypeError

    如果 eqfrozen 均为真值,默认情况下 @dataclass 将为你生成一个 __hash__() 方法。如果 eq 为真值而 frozen 为假值,__hash__() 将被设为 None,标记其为不可哈希的(因为它确实是可变的)。如果 eq 为假值,__hash__() 将保持不变,意味着将使用超类的 __hash__() 方法(如果超类是 object,则会回退为基于 id 的哈希)。

  • frozen: 如果为真(默认为 False),对字段进行赋值会产生一个异常。这模拟了只读的冻结实例。请参阅下面的讨论

    如果在类中定义了 __setattr__()__delattr__() 并且 frozen 为真,则会引发 TypeError

  • match_args: 如果为真(默认为 True),则会从生成的 __init__() 方法的非仅关键字参数列表中创建 __match_args__ 元组(即使不生成 __init__(),见上文)。如果为假,或者如果类中已定义了 __match_args__,则不会生成 __match_args__

在 3.10 版本加入。

  • kw_only: 若为真值(默认值为 False),则所有字段都将被标记为仅限关键字。如果一个字段被标记为仅限关键字,其唯一效果是,在调用 __init__() 时,必须使用关键字来指定由该字段生成的 __init__() 参数。详见形参术语条目。另见 KW_ONLY 部分。

    仅关键字字段不包含在 __match_args__ 中。

在 3.10 版本加入。

  • slots: 若为真值(默认值为 False),则会生成 __slots__ 属性,并返回一个新类而不是原始类。如果类中已经定义了 __slots__,则会引发 TypeError

警告

当使用 slots=True 时,向基类的 __init_subclass__() 传递参数将导致 TypeError。解决方法是使用不带参数的 __init_subclass__ 或使用默认值。详情见 gh-91126

在 3.10 版本加入。

在 3.11 版本发生变更: 如果字段名已包含在基类的 __slots__ 中,它将不会包含在生成的 __slots__ 中,以防止覆盖它们。因此,不要使用 __slots__ 来检索数据类的字段名。请改用 fields()。为了能够确定继承的槽位,基类的 __slots__ 可以是任何可迭代对象,但*不能*是迭代器。

  • weakref_slot: 若为真值(默认值为 False),则添加一个名为 “__weakref__” 的槽,这是使实例可弱引用所必需的。在未指定 slots=True 的情况下指定 weakref_slot=True 是一个错误。

在 3.11 版本中新增。

field 可以选择性地指定一个默认值,使用正常的 Python 语法:

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

在此示例中,ab 都将包含在添加的 __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, doc=None)

对于常见和简单的用例,不需要其他功能。然而,有些数据类特性需要额外的逐字段信息。为了满足这种对额外信息的需求,你可以用对提供的 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: 如果提供,它必须是一个零参数的可调用对象,当需要此字段的默认值时将被调用。除了其他用途,这可以用来指定具有可变默认值的字段,如下所述。同时指定 defaultdefault_factory 是错误的。

  • init: 如果为真(默认值),此字段将作为参数包含在生成的 __init__() 方法中。

  • repr: 如果为真(默认值),此字段将包含在生成的 __repr__() 方法返回的字符串中。

  • hash: 这可以是一个布尔值或 None。如果为真,此字段将包含在生成的 __hash__() 方法中。如果为假,此字段将从生成的 __hash__() 中排除。如果为 None(默认值),则使用 compare 的值:这通常是预期的行为,因为如果字段用于比较,它就应该包含在哈希中。不鼓励将此值设置为除 None 之外的任何值。

    hash=Falsecompare=True 的一个可能原因是,如果某个字段计算哈希值成本高昂,而该字段对于相等性测试是必需的,并且还有其他字段对类型的哈希值有贡献。即使一个字段被从哈希中排除,它仍将用于比较。

  • compare: 如果为真(默认值),此字段将包含在生成的相等性和比较方法中(__eq__(), __gt__(), 等)。

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

  • kw_only: 如果为真,此字段将被标记为仅关键字。这在计算生成的 __init__() 方法的参数时使用。

    仅关键字字段也不包含在 __match_args__ 中。

在 3.10 版本加入。

  • doc: 该字段的可选文档字符串。

在 3.14 版本加入。

如果字段的默认值是通过调用 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.xC.y 将不会被设置。

class dataclasses.Field

Field 对象描述每个已定义的字段。这些对象在内部创建,并由模块级方法 fields() 返回(见下文)。用户不应直接实例化 Field 对象。其文档化的属性有:

  • name: 字段的名称。

  • type: 字段的类型。

  • default, default_factory, init, repr, hash, compare, metadata, 和 kw_only 的含义和值与它们在 field() 函数中的含义和值相同。

可能存在其他属性,但它们是私有的,不应被检查或依赖。

class dataclasses.InitVar

InitVar[T] 类型注解描述了仅限初始化的变量。用 InitVar 注解的字段被认为是伪字段,因此既不会被 fields() 函数返回,也不会以任何方式使用,除了将它们作为参数添加到 __init__() 和可选的 __post_init__()

dataclasses.fields(class_or_instance)

返回一个 Field 对象元组,该元组定义了此数据类的字段。接受数据类或数据类的实例。如果传入的不是数据类或其实例,则引发 TypeError。不返回 ClassVarInitVar 类型的伪字段。

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}]}

要创建浅拷贝,可以使用以下变通方法:

{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, weakref_slot=False, module=None, decorator=dataclass)

创建一个新的数据类,其名称为 cls_name,字段定义在 fields 中,基类在 bases 中给出,并使用 namespace 中给出的命名空间进行初始化。fields 是一个可迭代对象,其元素可以是 name(name, type)(name, type, Field)。如果只提供了 name,则 type 会使用 typing.Anyinitrepreqorderunsafe_hashfrozenmatch_argskw_onlyslotsweakref_slot 的值与它们在 @dataclass 中的含义相同。

如果定义了 module,数据类的 __module__ 属性将被设置为该值。默认情况下,它被设置为调用者的模块名。

decorator 参数是一个可调用对象,将用于创建数据类。它应将类对象作为第一个参数,并接受与 @dataclass 相同的关键字参数。默认情况下,使用 @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

3.14 版新增: 增加了 decorator 参数。

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()(或类似名称的)方法。

数据类实例也受通用函数 copy.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__() 参数。

在此示例中,字段 yz 将被标记为仅关键字字段:

@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 的子类。

初始化后处理

dataclasses.__post_init__()

当在类上定义时,它将被生成的 __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__() 方法中调用此方法:

class Rectangle:
    def __init__(self, height, width):
        self.height = height
        self.width = width

@dataclass
class Square(Rectangle):
    side: float

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

但是请注意,通常情况下,由数据类生成的 __init__() 方法不需要被调用,因为派生的数据类会负责初始化任何本身是数据类的基类的所有字段。

有关如何向 __post_init__() 传递参数的方法,请参阅下面关于仅初始化变量的部分。另请参阅关于 replace() 如何处理 init=False 字段的警告。

类变量

@dataclass 实际检查字段类型的少数地方之一是确定一个字段是否为 PEP 526 中定义的类变量。它通过检查字段的类型是否为 typing.ClassVar 来做到这一点。如果一个字段是 ClassVar,它将被排除在字段考虑范围之外,并被数据类机制忽略。这样的 ClassVar 伪字段不会被模块级的 fields() 函数返回。

仅限初始化的变量

@dataclass 检查类型注解的另一个地方是确定一个字段是否是仅初始化变量。它通过查看字段的类型是否为 InitVar 类型来实现。如果一个字段是 InitVar,它被认为是一个称为仅初始化字段的伪字段。由于它不是一个真正的字段,它不会被模块级的 fields() 函数返回。仅初始化字段被添加为生成的 __init__() 方法的参数,并传递给可选的 __post_init__() 方法。它们在其他方面不被数据类使用。

例如,假设一个字段将从数据库初始化,如果在创建类时没有提供值:

@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() 将为 ij 返回 Field 对象,但不会为 database 返回。

冻结的实例

创建真正不可变的 Python 对象是不可能的。但是,通过向 @dataclass 装饰器传递 frozen=True,你可以模拟不可变性。在这种情况下,数据类会向类中添加 __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, zx 的最终类型是 int,如类 C 中所指定。

C 生成的 __init__() 方法将如下所示:

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

__init__() 中仅限关键字参数的重新排序

在计算出 __init__() 所需的参数后,任何仅关键字参数都会被移动到所有常规(非仅关键字)参数之后。这是 Python 中实现仅关键字参数的要求:它们必须位于非仅关键字参数之后。

在此示例中,Base.yBase.wD.t 是仅关键字字段,而 Base.xD.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 = []      # This code raises ValueError
    def add(self, element):
        self.x.append(element)

它会生成类似这样的代码:

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

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

这与使用类 C 的原始示例有相同的问题。也就是说,在创建类实例时未指定 x 值的两个类 D 的实例将共享同一份 x 的副本。因为数据类仅使用正常的 Python 类创建,它们也共享此行为。数据类没有通用的方法来检测这种情况。相反,如果 @dataclass 装饰器检测到不可哈希的默认参数,它将引发 ValueError。其假设是,如果一个值是不可哈希的,那么它是可变的。这是一个部分解决方案,但它确实可以防止许多常见的错误。

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

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

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

在 3.11 版本发生变更: 现在不再是查找并禁止 list, dictset 类型的对象,而是不允许不可哈希对象作为默认值。不可哈希性被用来近似可变性。

描述符类型的字段

被赋予描述符对象作为其默认值的字段具有以下特殊行为:

  • 传递给数据类 __init__() 方法的字段值将被传递给描述符的 __set__() 方法,而不是覆盖描述符对象。

  • 类似地,当获取或设置字段时,会调用描述符的 __get__()__set__() 方法,而不是返回或覆盖描述符对象。

  • 为了确定一个字段是否包含默认值,@dataclass 将以其类访问形式调用描述符的 __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

请注意,如果一个字段被注解为描述符类型,但没有被赋予一个描述符对象作为其默认值,该字段将像一个普通字段一样工作。