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__()
方法,即使这样做可能不安全。否则,将根据 eq 和 frozen 的设置来生成__hash__()
方法。默认值为False
。内置的
hash()
在对象被添加到哈希集合(如字典和集合)时会使用__hash__()
。拥有__hash__()
意味着类的实例是不可变的。可变性是一个复杂的属性,取决于程序员的意图、__eq__()
的存在和行为,以及@dataclass
装饰器中 eq 和 frozen 标志的值。默认情况下,
@dataclass
不会隐式添加__hash__()
方法,除非这样做是安全的。它也不会添加或更改已显式定义的__hash__()
方法。如__hash__()
文档中所述,将类属性__hash__ = None
设置为对 Python 具有特定含义。如果
__hash__()
没有被显式定义,或者它被设置为None
,那么@dataclass
可能 会添加一个隐式的__hash__()
方法。虽然不推荐,但你可以使用unsafe_hash=True
来强制@dataclass
创建一个__hash__()
方法。这可能适用于你的类在逻辑上是不可变的,但仍然可以被修改的情况。这是一个特殊的用例,应仔细考虑。以下是隐式创建
__hash__()
方法的规则。请注意,你不能在数据类中既有显式的__hash__()
方法,又设置unsafe_hash=True
;这将导致TypeError
。如果 eq 和 frozen 均为真值,默认情况下
@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=True
时,向基类的__init_subclass__()
传递参数将导致TypeError
。解决方法是使用不带参数的__init_subclass__
或使用默认值。详情见 gh-91126。在 3.10 版本加入。
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'
在此示例中,
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, 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: 如果提供,它必须是一个零参数的可调用对象,当需要此字段的默认值时将被调用。除了其他用途,这可以用来指定具有可变默认值的字段,如下所述。同时指定 default 和 default_factory 是错误的。
init: 如果为真(默认值),此字段将作为参数包含在生成的
__init__()
方法中。repr: 如果为真(默认值),此字段将包含在生成的
__repr__()
方法返回的字符串中。hash: 这可以是一个布尔值或
None
。如果为真,此字段将包含在生成的__hash__()
方法中。如果为假,此字段将从生成的__hash__()
中排除。如果为None
(默认值),则使用 compare 的值:这通常是预期的行为,因为如果字段用于比较,它就应该包含在哈希中。不鼓励将此值设置为除None
之外的任何值。将
hash=False
但compare=True
的一个可能原因是,如果某个字段计算哈希值成本高昂,而该字段对于相等性测试是必需的,并且还有其他字段对类型的哈希值有贡献。即使一个字段被从哈希中排除,它仍将用于比较。compare: 如果为真(默认值),此字段将包含在生成的相等性和比较方法中(
__eq__()
,__gt__()
, 等)。metadata: 这可以是一个映射或
None
。None
被视为空字典。这个值被包装在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.x
和C.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
。不返回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}]}
要创建浅拷贝,可以使用以下变通方法:
{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.Any
。init、repr、eq、order、unsafe_hash、frozen、match_args、kw_only、slots 和 weakref_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__()
参数。在此示例中,字段
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
的子类。
初始化后处理¶
- 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)
冻结的实例¶
创建真正不可变的 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
, 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
和 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 = [] # 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
描述符类型的字段¶
被赋予描述符对象作为其默认值的字段具有以下特殊行为:
传递给数据类
__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
请注意,如果一个字段被注解为描述符类型,但没有被赋予一个描述符对象作为其默认值,该字段将像一个普通字段一样工作。