python modifying frozen dataclass

2.7k views Asked by At

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.

1

There are 1 answers

4
jsbueno On BEST ANSWER

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