Reputation: 93
I used namedtuples for immutable data structures until I came across dataclasses, which I prefer in my use-cases (not relevant to the question).
Now I learned that they are not immutable! At least not strictly speaking.
setattr(frozen_dc_obj, "prop", "value")
raises an exception. ok.
But why does object.__setattr__(frozen_dc_obj,..)
work?
Compared to namedtuple
, where it raises an exception!
from collections import namedtuple
from dataclasses import dataclass
NTTest = namedtuple("NTTest", "id")
nttest = NTTest(1)
setattr(nttest, "id", 2) # Exception
object.__setattr__(nttest, "id", 2) # Exception
@dataclass(frozen=True)
class DCTest:
id: int
dctest = DCTest(1)
setattr(dctest, "id", 2) # Exception
object.__setattr__(dctest, "id", 2) # WORKS
Upvotes: 7
Views: 3606
Reputation: 93
Someone else on a python Discord channel gave another great insight:
The frozen dataclass probably restricts access through its own setattr, which you bypass by passing it over to object.
Namedtuples are derived from actual tuples instead of an in python implementation so they can be "truly" immutable, but it's a moot point if you're going around the interface if the dataclass (same could be done to the tuple in a messier way). I'd recommend using NamedTuple from typing over namedtuple for the readability it provides with being a proper class, but if you need the dataclass then a frozen one should be fine. The NamedTuple isn't really a typehint, but works with them in the class definition like dataclasses do and gives it a neater definition compared to the type-like constructor.
I think there just isn't a way to restrict all access in pure python, since you can invoke object's methods that just work.
Assigning on a tuple would involve interacting with (c)python at the C level, but is the same in the sense that it's not an intended interface for it.
Python overall isn't big on safety like that, if something shouldn't be done then it's the problem of the user if they do it.
Upvotes: 2
Reputation: 36309
namedtuple defines __slots__ = ()
and hence you can't set any attribute (it doesn't have a __dict__
).
Frozen dataclasses on the other hand perform a manual check in their __setattr__
method and raise an exception if it's a frozen instance.
Compare the following:
>>> class Foo:
... __slots__ = ()
...
>>> f = Foo()
>>> f.__dict__ # doesn't exist, so object.__setattr__ won't work
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute '__dict__'
>>> @dataclass(frozen=True)
... class Bar:
... pass
...
>>> b = Bar()
>>> b.__dict__ # this exists, so object.__setattr__ works
{}
Upvotes: 6