Replace attributes in Data Class objects

Question:

I’d like to replace the attributes of a dataclass instance, analogous to namedtuple._replace(), i.e. making an altered copy of the original object:

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)

This returns:

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

How can I mimic this functionality in dataclass objects?

Asked By: BayerSe

||

Answers:

dataclass is just syntactic sugar for the automatic creation of a special __init__ method and a host of other “boilerplate” methods based on type-annotated attributes.

Once the class is created, it is like any other, and its attributes can be overwritten and instances can be copied, e.g.

import copy

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

Depending on what the attributes are, you may only require copy.copy.

Answered By: ely

The dataclasses module has a helper function for field replacement on instances (docs)

from dataclasses import replace

Usage differs from collections.namedtuple, where the functionality was provided by a method on the generated type (Side note: namedtuple._replace is documented/public API, using an underscore on the name was called a "regret" by the author, see link at end of answer).

>>> 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)

For more background of the design, see the PyCon 2018 talk – Dataclasses: The code generator to end all code generators. The replace API is discussed in depth, along with other design differences between namedtuple and dataclasses, and some performance comparisons are shown.

Answered By: wim

I know the question is about dataclass, but if you’re using attr.s instead then you can use attr.evolve instead of 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)
Answered By: Florian Brucker
@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)
Answered By: adg08101

Just using replace will have reference pointer to previous mutable objects, hence two instances of a dataclass will share a state

So try something like this:

@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
Answered By: Yeldos Balgabekov