数据类 — 数据类 — Python 文档

来自菜鸟教程
Python/docs/3.9/library/dataclasses
跳转至:导航、​搜索

dataclasses — 数据类

源代码: :source:`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)

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

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)
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 为真而 eq 为假,则会引发 ValueError

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

  • unsafe_hash:如果是False(默认),则根据eqfrozen的设置方式生成__hash__()方法。

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

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

    如果 __hash__() 未明确定义,或者设置为 None,则 dataclass() 可以 添加隐式 __hash__() ] 方法。 虽然不推荐,但您可以强制 dataclass() 使用 unsafe_hash=True 创建 __hash__() 方法。 如果您的类在逻辑上是不可变的,但仍然可以改变,则可能就是这种情况。 这是一个专门的用例,应该仔细考虑。

    以下是管理 __hash__() 方法的隐式创建的规则。 请注意,您不能在数据类中同时使用显式 __hash__() 方法并设置 unsafe_hash=True; 这将导致 TypeError

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

  • frozen:如果为true(默认为False),分配给字段会产生异常。 这模拟只读冻结实例。 如果在类中定义了 __setattr__()__delattr__(),则会引发 TypeError。 请参阅下面的讨论。

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, repr=True, hash=None, init=True, compare=True, metadata=None)

对于常见和简单的用例,不需要其他功能。 但是,有些数据类功能需要额外的每个字段信息。 为了满足对附加信息的这种需求,您可以通过调用提供的 field() 函数来替换默认字段值。 例如:

@dataclass
class C:
    mylist: list[int] = field(default_factory=list)

c = C()
c.mylist += [1, 2, 3]

如上图,MISSING值是一个哨兵对象,用于检测是否提供了defaultdefault_factory参数。 使用此标记是因为 Nonedefault 的有效值。 任何代码都不应直接使用 MISSING 值。

field() 的参数是:

  • default:如果提供,这将是该字段的默认值。 这是必需的,因为 field() 调用本身替换了默认值的正常位置。

  • default_factory:如果提供,它必须是一个零参数可调用的,当该字段需要默认值时将被调用。 除其他用途外,这可用于指定具有可变默认值的字段,如下所述。 同时指定defaultdefault_factory是错误的。

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

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

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

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

    设置 hash=False 但设置 compare=True 的一个可能原因是,如果一个字段计算哈希值的成本很高,则需要该字段进行相等性测试,并且还有其他字段会影响类型的哈希值。 即使某个字段从哈希中排除,它仍将用于比较。

  • metadata:这可以是映射或无。 None 被视为空字典。 该值包含在 MappingProxyType() 中以使其只读,并在 Field 对象上公开。 数据类根本不使用它,而是作为第三方扩展机制提供的。 多个第三方可以各自拥有自己的密钥,用作元数据中的命名空间。

如果字段的默认值是通过调用 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.z10,类属性 C.t20,类属性 C.xC.y 不会被设置。

class dataclasses.Field

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

  • name:字段名称。

  • type:字段类型。

  • defaultdefault_factoryinitreprhashcomparemetadatafield() 声明中相同的含义和值。


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

dataclasses.fields(class_or_instance)
返回定义此数据类字段的 Field 对象的元组。 接受数据类或数据类的实例。 如果未传递数据类或实例,则引发 TypeError。 不返回 ClassVarInitVar 的伪字段。
dataclasses.asdict(instance, *, dict_factory=dict)

将数据类 instance 转换为字典(通过使用工厂函数 dict_factory)。 每个数据类都转换为其字段的字典,作为 name: value 对。 数据类、字典、列表和元组被递归到。 例如:

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

如果 instance 不是数据类实例,则引发 TypeError

dataclasses.astuple(instance, *, tuple_factory=tuple)

将数据类 instance 转换为元组(通过使用工厂函数 tuple_factory)。 每个数据类都被转换为其字段值的元组。 数据类、字典、列表和元组被递归到。

继续前面的例子:

assert astuple(p) == (10, 20)
assert astuple(c) == ([(0, 0), (10, 4)],)

如果 instance 不是数据类实例,则引发 TypeError

dataclasses.make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

创建一个名为 cls_name 的新数据类、fields 中定义的字段、bases 中给出的基类,并使用 namespace 中给出的命名空间进行初始化. fields 是一个可迭代对象,其元素分别为 name(name, type)(name, type, Field)。 如果仅提供 name,则 typing.Any 用于 typeinitrepreqorderunsafe_hashfrozen的意义与他们在 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(instance, /, **changes)

创建与 instance 类型相同的新对象,用 changes 中的值替换字段。 如果 instance 不是数据类,则引发 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(class_or_instance)

如果其参数是数据类或其实例,则返回 True,否则返回 False

如果你需要知道一个类是否是一个数据类的实例(而不是一个数据类本身),那么为 not isinstance(obj, type) 添加进一步的检查:

def is_dataclass_instance(obj):
    return is_dataclass(obj) and not isinstance(obj, type)


初始化后处理

如果在类上定义了 __post_init__(),则生成的 __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)

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

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


类变量

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


仅初始化变量

dataclass() 检查类型注释的另一个地方是确定一个字段是否是一个 init-only 变量。 它通过查看字段的类型是否为 dataclasses.InitVar 类型来执行此操作。 如果字段是 InitVar,则将其视为伪字段,称为 init-only 字段。 由于它不是真正的字段,因此模块级 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):

默认工厂函数

如果 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

使用数据类,if 此代码有效:

@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 的原始示例存在相同的问题。 也就是说,创建类实例时未指定 x 值的类 D 的两个实例将共享 x 的相同副本。 因为数据类只使用普通的 Python 类创建,所以它们也共享这种行为。 数据类没有通用的方法来检测这种情况。 相反,如果数据类检测到类型为 listdictset 的默认参数,则会引发 TypeError。 这是一个部分解决方案,但它确实可以防止许多常见错误。

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

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

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

例外

exception dataclasses.FrozenInstanceError
在使用 frozen=True 定义的数据类上调用隐式定义的 __setattr__()__delattr__() 时引发。 它是 AttributeError 的子类。