2

I have a python dataclass whose attributes I didn't intend to modify. However, as I just realized, it is still modifiable via __dict__ even when I set frozen=True. For instance:

@dataclass(frozen=True)
class C:
    a: int = 1

c = C()
c.a = 2 # dataclasses.FrozenInstanceError: cannot assign to field 'a'
c.__dict__['a'] = 2 # legit

print(c) 
# C(a=2)

I understand that this is owning to the underlying __dict__ attr is a mutable object, and raising error at __setattr__ doesn't really cover.

On the other hand, I do have a need to access all the attributes in a collective manner. What is the best practice for this kind of task? Do I make a copy of __dict__? Or maybe slots=True would be a potential candidate?

thanks in advance.

David Chu
  • 23
  • 5
  • I don't understand your question. Look, all user-defined classes are trivially mutable in Python. "Immutability" is more of a guardrail, certainly not a guarantee – juanpa.arrivillaga Apr 15 '22 at 02:02
  • 1
    The best practice is to just stop worrying about this – juanpa.arrivillaga Apr 15 '22 at 02:02
  • so you're saying is no matter how many layers of "protection" against mutability, there's always a way to modify the supposedly "immutable" attr? – David Chu Apr 15 '22 at 02:17
  • and perhaps by 'best practice' I want to know if it is encouraged when I call `__dict__` – David Chu Apr 15 '22 at 02:18
  • Yes, there is no way to prevent a motivated user from mutating your user-defined object. I'm not sure I understand you mean by "I want to know if it is encouraged when I call `__dict__`" – juanpa.arrivillaga Apr 15 '22 at 03:53
  • my original incentive was to collect the attrs of my dataclass instances, do something with them (swap them around, for example), and return new instances. as I had experienced that manipulating them by calling ```__dict__``` naively will lead me to this problem. and wanting to know if there's a better way at achieveing this or this kind of action is discouraged in general. – David Chu Apr 15 '22 at 04:20

1 Answers1

2

As you had perceived well, one has to go well out of his way to modify one of these attributtes.

While it is possible to make it a bit more difficult than accessing the instance's __dict__ directly to modify an attribute, it will always be possible for one to change the underlying attribute in Python, due to the dynamic and reflexive nature of the language. It is really not the intent of the language to prevent one that "wants" to change an attribute from modifying it (note that 'private' and 'protected' attributes in languages that feature them like C++ and JAVA can also be altered by one intending to do so by using reflexive API's or going through the name mangling).

In other words there is no scenario in a serious system that modifying an attribute, by one having access to the system code or classes should be "dangerous" and off limits for developers writting code that will run in the same process. If you have some designing thinking so, you'd better rethink that, more likely putting the "read only" data in a separate service, accessible via API or other RPC method, so that potential "malicious coders" do not have in process access to your variable.

All that said, there are ways to freeze attributes that might prevent the __dict__ approach - one of which would be to create a special descriptor that after a certain change on the instance would lock writing: attributes that are assigned to a descriptor in the class won't go through __dict__. But as stated above, anyone intending to change teh attribute can go to whichever storage the descriptor keeps the attribute in (it has to be in a certain place), or simply reverse the change that indicates the value should be read-only from a certain point on.

I played around, and came up with example code for such a descriptor, but it is really, really silly - as it at some point has to use the "way to circunvent itself" in order to mark the class as locked. However, using __slots__ as you described will work with a frozen dataclass and will not feature a mutable __dict__ - on the other hand, it still trivial to change the instance attribute by going through the class attribute's .__set__ method:

In [124]: @dataclass(frozen=True)
     ...: class A:
     ...:     __slots__=("a",)
     ...:     a: int
     ...: 

In [125]: a = A(42)

In [126]: a.a = 23
FrozenInstanceError ...        


In [127]: a.__dict__
...
AttributeError: 'A' object has no attribute '__dict__'

In [128]: A.a.__set__(a, 23)

In [139]: a.a
Out[139]: 23

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • thanks, i think i got the point. i'm better off to rethink about the design than worry indefinitely. just going to wait for a few day and see if there are other opinions. – David Chu Apr 15 '22 at 02:36
  • I am afraid there will be no divergent "opinions" on that by any one in the know of the inner workings of Python. It really can't be protected against a little bit of reverse engineering - which can be done in any Python REPL environment. You already had @juanpa.arrivilaga stepping in and saying basically the same, and he is easily on the the top 5 most known people about Python around. I quoted "opinion" because it is not an opinion when it is a fact. Even if someone suggests a mechanism that would not depend on code obfuscation, which is improbable, + – jsbueno Apr 15 '22 at 02:44
  • + it could be even worse, as it would with all probability be vulnerable to being broken, but with added burden that you'd be trusting it as safe. – jsbueno Apr 15 '22 at 02:45
  • thanks again. by no means I want to leave any incentives at disrespect the giants like you and @juanpa.arrivilaga or the facts you provided. what I meant by 'opinion' was hoping an oppurtunity to increase my understanding about Python more. I saw your edited answer and it tied up the loose end of modifying the sloted variables. So I think I can call this issue closed. – David Chu Apr 15 '22 at 03:44