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
装饰器将向类添加各种“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, weakref_slot=False) class C: ...
@dataclass
的参数是init:如果为 true(默认值),将生成
__init__()
方法。如果该类已定义
__init__()
,则忽略此参数。repr:如果为 true(默认值),将生成
__repr__()
方法。生成的 repr 字符串将包含类名以及每个字段的名称和 repr,其顺序与在类中定义的顺序相同。标记为从 repr 中排除的字段不包括在内。例如:InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)
。如果该类已定义
__repr__()
,则忽略此参数。eq:如果为 true(默认值),将生成
__eq__()
方法。此方法将类按其字段的元组进行比较,按顺序进行比较。比较中的两个实例必须是相同类型。如果该类已定义
__eq__()
,则忽略此参数。order:如果为 true(默认值为
False
),将生成__lt__()
、__le__()
、__gt__()
和__ge__()
方法。这些方法将类按其字段的元组进行比较,按顺序进行比较。比较中的两个实例必须是相同类型。如果 order 为 true,而 eq 为 false,则会引发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 的哈希)。frozen:如果为真(默认值为
False
),则对字段进行赋值将生成一个异常。这模拟了只读冻结实例。如果在类中定义了__setattr__()
或__delattr__()
,则会引发TypeError
。请参见下面的讨论。match_args:如果为真(默认值为
True
),则__match_args__
元组将从生成__init__()
方法的参数列表中创建(即使未生成__init__()
,请参见上文)。如果为假,或者如果在类中已定义__match_args__
,则不会生成__match_args__
。
在版本 3.10 中添加。
kw_only:如果为真(默认值为
False
),则所有字段都将标记为仅关键字。如果一个字段被标记为仅关键字,则唯一的效果是,当调用__init__()
时,必须使用关键字指定从仅关键字字段生成的__init__()
参数。对数据类的任何其他方面没有影响。有关详细信息,请参见 parameter 词汇表条目。另请参见KW_ONLY
部分。
在版本 3.10 中添加。
slots:如果为真(默认值为
False
),将生成__slots__
属性,并将返回新类,而不是原始类。如果在类中已定义__slots__
,则会引发TypeError
。在使用slots=True
的数据类中调用无参数super()
将导致引发以下异常:TypeError: super(type, obj): obj must be an instance or subtype of type
。双参数super()
是有效的解决方法。有关详细信息,请参阅 gh-90562。
weakref_slot:如果为真(默认值为
False
),则添加名为 “__weakref__” 的 slot,这是使实例可弱引用的必要条件。指定weakref_slot=True
而又不指定slots=True
是一个错误。
在 3.11 版中添加。
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:如果为真(默认),此字段将作为生成
__init__()
方法的参数包含在内。repr:如果为真(默认),此字段将包含在生成
__repr__()
方法返回的字符串中。hash:这可以是布尔值或
None
。如果为 true,则此字段将包含在生成的__hash__()
方法中。如果为None
(默认值),则使用 compare 的值:这通常是预期的行为。如果字段用于比较,则应在哈希中考虑该字段。不建议将此值设置为None
以外的任何值。设置
hash=False
但compare=True
的一个可能原因是,如果字段计算哈希值很昂贵,该字段需要用于相等性测试,并且还有其他字段有助于类型的哈希值。即使字段从哈希中排除,它仍将用于比较。compare:如果为 true(默认值),则此字段将包含在生成的相等性和比较方法中(
__eq__()
、__gt__()
等)。metadata:这可以是映射或
None
。None
被视为一个空字典。此值包装在MappingProxyType()
中以使其只读,并在Field
对象上公开。数据类根本不使用它,它是作为第三方扩展机制提供的。多个第三方都可以有自己的键,用作元数据中的命名空间。kw_only:如果为 true,则此字段将标记为仅关键字。在计算生成的
__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}]}
要创建浅拷贝,可以使用以下解决方法
{field.name: getattr(obj, field.name) for field in fields(obj)}
asdict()
如果 obj 不是数据类实例,则引发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))
astuple()
如果 obj 不是数据类实例,则引发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)¶
使用名称 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__
属性将设置为该值。默认情况下,它设置为调用者的模块名称。此函数不是严格必需的,因为任何使用
__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()
时,请预先了解init=False
字段的工作方式。它们不会从源对象复制,而是在__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_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
检查类型注释的地方是确定字段是否是仅初始化变量。它通过查看字段的类型是否为 dataclasses.InitVar
类型来实现。如果字段是 InitVar
,则它被视为称为仅初始化字段的伪字段。由于它不是真正的字段,因此不会由模块级 fields()
函数返回。仅初始化字段作为参数添加到生成的 __init__()
方法中,并传递给可选的 __post_init__()
方法。它们不会被 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)
冻结实例¶
不可能创建真正的不可变 Python 对象。但是,通过将 frozen=True
传递给 @dataclass
装饰器,你可以模拟不可变性。在这种情况下,dataclasses 将向类添加 __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
的原始示例具有相同的问题。也就是说,类 D
的两个实例在创建类实例时未为 x
指定值,它们将共享 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
请注意,如果字段用描述符类型注释,但未将其分配为描述符对象作为其默认值,则该字段将像普通字段一样作用。