python:dataclasses --- 数据类

python:dataclasses --- 数据类

  • 模块内容
  • 初始化后处理
  • 仅初始化变量
  • 实例
  • 继承
  • __init__() 中仅限关键字字段的重新排序
  • 默认工厂函数
  • 可变的默认值

这个模块提供了一个装饰器和一些函数,用于自动添加生成的 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, weakref_slot=False)
这个函数是 decorator ,用于将生成的 special method 添加到类中,如下所述。

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: 如果为真值(默认),将生成一个 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 (默认值),则根据 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的hash)。

frozen: 如为真值 (默认值为 False),则对字段赋值将会产生异常。 这模拟了只读的冻结实例。 如果在类中定义了 setattr() 或 delattr() 则将会引发 TypeError。 参见下文的讨论。

match_args: 如果为真值 (默认值为 True),则将根据传给生成的 init() 方法的形参列表来创建 match_args 元组 (即使没有生成 init() 也会创建,见上文)。 如果为假值,或者如果 match_args 已在类中定义,则将不生成 match_args

3.10 新版功能.

slots: 如果为真值 (默认值为 False),则将生成 slots 属性并将返回一个新类而非原来的类。 如果 slots 已在类中定义,则会引发 TypeError。

3.11 新版功能.

fields 可以选择使用普通的 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 :如果为true(默认值),则该字段作为参数包含在生成的 init() 方法中。

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

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

设置 hash=False 但 compare=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.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§ == {‘x’: 10, ‘y’: 20}

c = C([Point(0, 0), Point(10, 4)])
assert asdict© == {‘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§ == (10, 20)
assert astuple© == ([(0, 0), (10, 4)],)
要创建一个浅拷贝,可以使用以下方法:

tuple(getattr(obj, field.name) for field in dataclasses.fields(obj))
如果obj不是一个数据类实例, astuple() 引发 TypeError 。

此函数不是严格要求的,因为用于任何创建带有 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() 形参必须以关键字形式来指定。

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

初始化后处理

生成的 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
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() 将返回 i 和 j 的 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
最后的字段列表依次是 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, and 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 = []
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

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

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

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

你可能感兴趣的:(python,开发语言)