7

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'

alex_halford
  • 369
  • 3
  • 10
  • 5
    You want it to be immutable, except that you want it to be mutable. There's a fundamental contradiction in your goals here. – user2357112 Mar 16 '20 at 01:13
  • 3
    `foo = foo.strip_id()` is a perfectly fine thing to do. It's how string methods work, for example. – user2357112 Mar 16 '20 at 01:15
  • 4
    You can't have mutable, immutable objects. Sorry. Choose one. Also: "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" That isn't hacky *at all*. Indeed, that is generally considered best practice, there are entire languages that prevent mutable state, and this is how objects work in those languages. I would go with that route. – juanpa.arrivillaga Mar 16 '20 at 01:38

2 Answers2

8

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.

Z4-tier
  • 7,287
  • 3
  • 26
  • 42
  • This does work, thank you. I've edited my question to make the same comment, but surely there is a reasonable difference between 'mutable in a very specific, tightly controlled way', and 'totally mutable'? – alex_halford Mar 16 '20 at 01:26
  • 3
    Not really. I mean, who are you trying to protect things from? Are you, by chance, coming from a Java background, and looking for something more like class private members? – Z4-tier Mar 16 '20 at 01:28
  • Nope, no java background. It's for an internal company library, and this object should only rarely be changed, and only in this particular way. I'm protecting against library users misusing the data type, which would introduce a security issue. – alex_halford Mar 16 '20 at 01:30
  • If it's just to keep other programmers from incorrectly using this piece of data, then the way that is traditionally communicated in Python is to prefix the variable name with an underscore (single underscore, we're not making dunders here). That is usually understood to mean "private, don't modify" and it will also prevent it from being imported with a wildcard import. – Z4-tier Mar 16 '20 at 01:35
  • Fair enough, but this is a dataclass explicilty designed to store the id and the name, so making one of them private with an underscore seems just as hacky – alex_halford Mar 16 '20 at 01:37
  • you could use the `_` prefix to mark them private, and then add getter methods that return those values. basically: `def get_id(self): return self._id` – Z4-tier Mar 16 '20 at 01:40
  • 3
    @Z4-tier no, don't use getters in Python. – juanpa.arrivillaga Mar 16 '20 at 01:43
  • @juanpa.arrivillaga normally I agree with that, but in this case it seems like an acceptable way to meet the requirement of "somewhat private". Is there another pattern that would work better? – Z4-tier Mar 16 '20 at 01:50
  • @Z4-tier use a `property` – juanpa.arrivillaga Mar 16 '20 at 01:51
  • @juanpa.arrivillaga I added an update to this discussing the option of using `@property` decorators, but in the context of the original question, I can't find any way of using `@property` with frozen dataclasses. At least, I can't find a way that is clean and won't lead to rage on the part of whoever has to support it. Do you know a good way of doing this? – Z4-tier Nov 29 '21 at 01:37
  • @alex_halford it's reasonable to want this functionality for things like members that serve as a cache for expensive to derive data that can always be derived from the other members, or things like mutexes. This is why C++ has the `mutable` keyword. You can do type backflips to avoid it but you aren't crazy for wanting it. – Joseph Garvin Jul 06 '23 at 19:42
4

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)   
Arne
  • 17,706
  • 5
  • 83
  • 99