替换数据类对象中的属性

33

我希望像namedtuple._replace()一样更改dataclass实例的属性,即创建一个修改过的原始对象副本:

from dataclasses import dataclass
from collections import namedtuple

U = namedtuple("U", "x")

@dataclass
class V:
    x: int

u = U(x=1)
u_ = u._replace(x=-1)
v = V(x=1)

print(u)
print(u_)
print(v)

这将返回:

U(x=1)
U(x=-1)
V(x=1)

如何在dataclass对象中模仿这个功能?

5个回答

54

dataclasses 模块有一个用于实例字段替换的辅助函数 (文档)

from dataclasses import replace

使用方法与collections.namedtuple不同,后者的功能是由生成类型上的一个方法提供的 (附注: namedtuple._replace被记录为公共API,但作者对名称中的下划线表示“遗憾”,请参见答案末尾的链接)。

>>> from dataclasses import dataclass, replace
>>> @dataclass
... class V:
...     x: int
...     y: int
...     
>>> v = V(1, 2)
>>> v_ = replace(v, y=42)
>>> v
V(x=1, y=2)
>>> v_
V(x=1, y=42)

如需了解更多设计背景,请参阅PyCon 2018演讲-Dataclasses:代码生成器中的终极代码生成器。该演讲深入讨论了replace API以及namedtupledataclasses之间的其他设计差异,并展示了一些性能比较结果。


1
似乎有人发现了数据类中 init 和后初始化钩子的问题,但他们并没有重新审视设计并解决复杂性,而是选择通过增加复杂性来解决它。真正的故事是,如果你在某种方式下使用数据类,而它们不被视为完全无逻辑的容器,那么你正在错误地使用它们,并需要其他工具。例如,数据类的 deepcopy 应该绝对零风险,除了对每个成员属性进行简单的深度复制,因此用户不会遇到任何意外情况。 - ely
8
当涉及到(伪)不可变对象时,如冻结的数据类时,“replace”非常有用。在函数式编程中,它们非常常见,你不会改变原始对象,而是返回一个所有字段都相等,除了你使用“replace”修改的字段不同的新对象。 - hugovdberg
2
我有点希望将replacec作为“方法”而不是函数,因为这似乎嵌入了在调用代码中假定某些内容是数据类的假设。 - Att Righ
@ely,我不同意你的观点,认为数据类必须是完全无逻辑的容器。例如,考虑一个冻结的数据类来建模二维三角形,在这种情况下,您可能需要一种计算面积的方法,并验证三角形是否退化(没有共线顶点)。在初始化后钩子中放置验证比这个位置更好吗? - wim
1
@wim 和 @hugovdberg 很久没回来了。Wim,像这样的验证逻辑永远不应该作为容器本身的内部内容。创建一个帮助函数 validate_triangle —— 不要用一些简单的记录对象去膨胀其自我处理的职责。如果你因为某种(可疑的)面向对象的原因需要它,那么请使用类。几乎 任何 地方都比将该验证逻辑作为获取此类数据记录对象的任何用户可能认为是基本数据记录的晦涩实例创建的一部分更好。这让人想起了 @property,它也经常是一个糟糕的选择并被过度使用。 - ely
显示剩余3条评论

1

仅使用replace将具有对先前可变对象的引用指针,因此数据类的两个实例将共享状态

因此,请尝试像这样做:

@dataclasses.dataclass(frozen=True)
class MyDataClass:
    mutable_object: list
    val: int
    
    def copy(self, **changes):
        return dataclasses.replace(deepcopy(self), **changes)

data = MyDataClass([], 1)
data2 = data.copy(val=2)
assert data.mutable_object != data2.mutable_object

你能再解释一下你所说的可变对象的引用指针是什么意思吗?这个什么时候很重要? - undefined

0

我知道这个问题是关于dataclass的,但如果你使用的是attr.s,那么你可以使用attr.evolve代替dataclasses.replace

import attr

@attr.s(frozen=True)
class Foo:
    x = attr.ib()
    y = attr.ib()

foo = Foo(1, 2)
bar = attr.evolve(foo, y=3)

-1

dataclass 只是语法糖,用于自动创建一个特殊的 __init__ 方法和一系列基于类型注释属性的其他“样板”方法。

一旦类被创建,它就像任何其他类一样,其属性可以被覆盖并且实例可以被复制,例如:

import copy

v_ = copy.deepcopy(v)
v_.x = -1

根据属性的不同,您可能只需要使用copy.copy


3
在数据类中进行字段替换时,使用复制/深度复制是不正确的做法。在某些复杂情况下(例如初始化/后初始化钩子),数据可能无法正确处理。更好的方法是使用dataclasses.replace()函数。 - wim
1
不,这只是巧合。我是 [python-dataclasses] 标签的追随者(目前唯一的追随者)。昨天我参加了一个有关它的 PyCon 演讲,才注意到 replace 方法的存在。 - wim
@wim稍后再次回顾一下,我认为在生产系统中使用这个功能后,我对replace的不同意见更加强烈了。我在你的答案中添加了一些评论,以便提供不同的看法。我完全尊重你的观点不同,但我想强调一个异议意见,因为有些用户可能会像我一样感到,这可以告诉他们如何使用基于约定的dataclass限制来避免replace的坏代码味道。 - ely
在需要创建可存储在集合内或用作字典键的可哈希实例时,冻结数据类非常常见。然而,在这种情况下,建议先复制再设置属性的方法根本行不通。 - wim
Python中的冻结数据类只是一个基本上混淆的概念。它仍然可以具有可变属性,例如列表等。将这样的东西用于字典键是一个非常糟糕的想法。 - ely

-2
@dataclass()
class Point:
    x: float = dataclasses.Field(repr=True, default=0.00, default_factory=float, init=True, hash=True, compare=True,
                                 metadata={'x_axis': "X Axis", 'ext_name': "Point X Axis"})
    y: float = dataclasses.Field(repr=True, default=0.00, default_factory=float, init=True, hash=True, compare=True,
                                 metadata={'y_axis': "Y Axis", 'ext_name': "Point Y Axis"})

Point1 = Point(13.5, 455.25)
Point2 = dataclasses.replace(Point1, y=255.25)

print(Point1, Point2)

3
欢迎来到StackOverflow!您能否在您的答案中添加一些文本来解释它是如何解决问题的,也许还可以指出它如何补充已经提供的其他答案? - joanis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接