Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python frozen dataclass, allow changing of attribute via method

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'

like image 239
alex_halford Avatar asked Jan 25 '23 06:01

alex_halford


2 Answers

Well, you can do it by directly modifying the __dict__ member of the instance modifying the attribute using object.__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.

like image 138
Z4-tier Avatar answered Jan 27 '23 18:01

Z4-tier


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)   
like image 37
Arne Avatar answered Jan 27 '23 18:01

Arne