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 装饰器会检查类以查找 fieldfield 定义为具有类型注解的类变量。除了下面描述的两个例外情况外,@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 (默认值),则会根据 eqfrozen 的设置方式生成 __hash__() 方法。

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

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

    如果 __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__(),则会引发 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

警告

在使用 slots=True 时,将参数传递给基类 __init_subclass__() 将导致 TypeError。 要么使用不带参数的 __init_subclass__,要么使用默认值作为解决方法。 有关完整详细信息,请参阅 gh-91126

在 3.10 版本中添加。

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

  • weakref_slot:如果为真(默认为 False),则添加一个名为“__weakref__”的 slot,这是使实例 可弱引用 所必需的。如果未同时指定 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)

对于常见和简单的用例,不需要其他功能。然而,有些数据类特性需要每个字段的额外信息。为了满足对额外信息的需求,你可以将默认字段值替换为对提供的 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:如果为 true(默认值),则此字段将作为参数包含在生成的 __init__() 方法中。

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

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

    hash=Falsecompare=True 的一个可能原因是,如果某个字段计算哈希值的开销很大,而该字段是等式测试所必需的,并且还有其他字段会影响类型的哈希值。即使一个字段从哈希中排除,它仍然用于比较。

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

  • metadata:这可以是映射或 NoneNone 被视为一个空字典。此值被包装在 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.xC.y 将不会设置。

class dataclasses.Field

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

  • name:字段的名称。

  • type:字段的类型。

  • defaultdefault_factoryinitreprhashcomparemetadatakw_only 具有与 field() 函数中相同的含义和值。

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

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)

创建一个新的数据类,其名称为 *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__ 属性将设置为该值。默认情况下,它设置为调用者的模块名称。

此函数不是严格必需的,因为任何使用 __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()(或类似名称的)方法来处理实例复制。

泛型函数 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_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

__init__() 方法由 @dataclass 生成,不会调用基类的 __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__() 方法。数据类不会以其他方式使用它们。

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

@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 对象。但是,通过将 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

最终的字段列表按顺序为 xyzx 的最终类型是 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 的原始示例存在相同的问题。也就是说,类 D 的两个实例在创建类实例时没有为 x 指定值,它们将共享 x 的相同副本。因为数据类只是使用普通的 Python 类创建,它们也共享此行为。数据类没有通用的方法来检测这种情况。相反,如果 @dataclass 装饰器检测到不可哈希的默认参数,则会引发 ValueError。假设如果一个值是不可哈希的,那么它是可变的。这是一个部分解决方案,但它可以防止许多常见错误。

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

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

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

在 3.11 版本中更改: 现在,不再查找并禁止 listdictset 类型的对象,而是不允许将不可哈希的对象作为默认值。不可哈希性用于近似可变性。

描述符类型的字段

被分配了 描述符对象 作为其默认值的字段具有以下特殊行为

  • 传递给数据类的 __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

请注意,如果一个字段使用描述符类型进行注释,但没有为其默认值分配描述符对象,则该字段的行为将类似于普通字段。