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() 装饰器会检查类以查找 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)
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 (默认值),则根据 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

    如果 eqfrozen 都是 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 版新加入.

fields 可以选择使用普通的 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 的值,这通常是预期的行为。如果字段用于比较,则应在 hash 中考虑该字段。不鼓励将此值设置为 None 以外的任何值。

    设置 hash=Falsecompare=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.xC.y 将不设置。

class dataclasses.Field

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

  • name :字段的名字。

  • type :字段的类型。

  • default, default_factory, init, repr, hash, compare, metadatakw_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}]}

要创建一个浅拷贝,可以使用以下方法:

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 会被用作 typeinit, repr, eq, order, unsafe_hash, frozen, match_args, kw_onlyslots 等值与它们在 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__() 形参必须以关键字形式来指定。

在这个例子中,字段 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 的一个子类。

初始化后处理

生成的 __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 字段的警告。

类变量

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

仅初始化变量

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

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

@dataclass
class C:
    i: int
    j: int = None
    database: InitVar[DatabaseType] = 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() 将返回 ijField 对象,但不包括 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.y, Base.w, and D.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 = []
    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, dictset 的默认形参时将会引发 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.