Suppose I have a dataclass:
@dataclass(frozen=True)
class Foo:
id: str
name: str
I want this to be immutable (hence the frozen=True
), such that foo.id = bar
and foo.name = baz
fail. But, I want to be able to strip the id, like so:
foo = Foo(id=10, name="spam")
foo.strip_id()
foo
-> Foo(id=None, name="spam")
I have tried a few things, overriding setattr, but nothing worked. Is there an elegant solution to this? (I know I could write a method that returns a new frozen instance that is identical except that that id has been stripped, but that seems a bit hacky, and it would require me to do foo = foo.strip_id()
, since foo.strip_id()
would not actually change foo
)
Edit:
Although some commenters seem to disagree, I think there is a legitimate distinction between 'fully mutable, do what you want with it', and 'immutable, except in this particular, tightly controlled way'
Well, you can do it by directly modifying the modifying the attribute using __dict__
member of the instanceobject.__setattr__(...)
1, but why??? Asking specifically for immutable and then making it mutable is... indecisive. But if you must:
from dataclasses import dataclass
@dataclass(frozen=True)
class Foo:
id: str
name: str
def strip_id(self):
object.__setattr__(self, 'id', None)
foo=Foo(10, 'bar')
>>> foo
Foo(id=10, name='bar')
>>> foo.strip_id()
>>> foo
Foo(id=None, name='bar')
Any way of doing this is probably going to seem hacky... because it requires doing things that are fundamentally the opposite of the design.
If you're using this as a signal to other programmers that they should not modify the values, the way that is normally done in Python is by prefixing the variable name with a single underscore. If you want to do that, while also making the values accessible, Python has a builtin module called property
, where (from the documentation) "typical use is to define a managed attribute":
from dataclasses import dataclass
@dataclass
class Foo:
_name: str
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@name.deleter
def name(self):
self._name = None
Then you can use it like this:
>>> f=Foo()
>>> f.name = "bar"
>>> f.name
'bar'
>>> f._name
'bar'
>>> del f.name
>>> f.name
>>> f._name
The decorated methods hide the actual value of _name
behind name
to control how the user interacts with that value. You can use this to apply transformation rules or validation checks to data before it is stored or returned.
This doesn't quite accomplish the same thing as using @dataclass(frozen=True)
, and if you try declaring it as frozen, you'll get an error. Mixing frozen dataclasses with the property decorator is not straightforward and I have not seen a satisfying solution that is concise and intuitive. @Arne posted this answer, and I found this thread on GitHub, but neither approach is very inspiring; if I came across such things in code that I had to maintain, I would not be very happy (but I would be confused, and probably pretty irritated).
1: Modified as per the answer by @Arne, who observed that the internal use of a dictionary as the data container is not guaranteed.
As a slight improvement over Z4-tier's solution, please use object.__setattr__
instead of self.__dict__
to manipulate attributes of a frozen dataclass. The fact that classes use a dictionary to store their attributes is just the default behavior, and dataclasses in particular will regularly use __slots__
instead because it reduces the memory footprint.
from dataclasses import dataclass
@dataclass(frozen=True)
class Foo:
id: str
name: str
def strip_id(self):
object.__setattr__(self, 'a', None)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With