On 2/22/2018 1:56 AM, Raymond Hettinger wrote:
When working on the docs for dataclasses, something unexpected came up. If a
dataclass is specified to be frozen, that characteristic is inherited by
subclasses which prevents them from assigning additional attributes:
>>> @dataclass(frozen=True)
class D:
x: int = 10
>>> class S(D):
pass
>>> s = S()
>>> s.cached = True
Traceback (most recent call last):
File "<pyshell#49>", line 1, in <module>
s.cached = True
File
"/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/dataclasses.py",
line 448, in _frozen_setattr
raise FrozenInstanceError(f'cannot assign to field {name!r}')
dataclasses.FrozenInstanceError: cannot assign to field 'cached'
Other immutable classes in Python don't behave the same way:
>>> class T(tuple):
pass
>>> t = T([10, 20, 30])
>>> t.cached = True
>>> class F(frozenset):
pass
>>> f = F([10, 20, 30])
>>> f.cached = True
>>> class B(bytes):
pass
>>> b = B()
>>> b.cached = True
I'll provide some background, then get in to the dataclasses design
issues. Note that I'm using "field" here in the PEP 557 sense.
There are some questions to resolve:
1. What happens when a frozen dataclass inherits from a non-frozen
dataclass?
2. What happens when a non-frozen dataclass inherits from a frozen
dataclass?
3. What happens when a non-dataclass inherits from a frozen dataclass?
4. Can new non-field attributes be created for frozen dataclasses?
I think it's useful to look at what attrs does. Unsurprisingly, attrs
works the way the dataclasses implementation in 3.7.0a1 works:
- If a frozen attrs class inherits from a non-frozen attrs class, the
result is a frozen attrs class.
- If a non-frozen attrs class inherits from a frozen attrs class, the
result is a frozen attrs class.
- For a frozen attrs class, you may not assign to any field, nor create
new non-field instance attributes.
- If a non-attrs class derives from a frozen attrs class, then you
cannot assign to or create any non-field instance attributes. This is
because they override the class's __setattr__ to always raise. This is
the case that Raymond initially brought up on this thread (but for
dataclasses, of course).
As I said, this is also how 3.7.0a1 dataclasses also works. The only
difference between this and 3.7.0.a2 is that I prohibited inheriting a
non-frozen dataclass from a frozen one, and also prohibited the
opposite: you can't inherit a frozen dataclass from a non-frozen
dataclass. This was just a stop-gap measure to give us more wiggle room
for future changes. But this does nothing to address Raymond's concern
about non-dataclasses deriving from frozen dataclasses.
A last piece of background info on how dataclasses and attrs work: the
most derived class implements all of the functionality. They never call
in to the base class to do anything. The base classes just exist to
provide the list of fields.
If frozen dataclasses only exist to protect fields that belong to the
hash, then my suggestion is to change the implementation of frozen class
to use properties instead of overwriting __setattr__ (Nick also
suggested this). This would allow you to create non-field attributes. If
the purpose is really to prevent any attributes from being added or
modified, then I think __setattr__ should stay but we should change it
to allow non-dataclass subclasses to add non-field instance attributes
(addressing Raymond's concern).
I think we shouldn't allow non-field instance attributes to be added to
a frozen dataclass, although if anyone has a strong feeling about it,
I'd like to hear it.
So, given a frozen dataclass "C" with field names in "field_names", I
propose changing __setattr__ to be:
def __setattr__(self, name, value):
if type(self) is C or name in field_names:
raise FrozenInstanceError(f'cannot assign to field {name!r}')
super(cls, self).__setattr__(name, value)
In the current 3.7.0a2 implementation of frozen dataclasses, __setattr__
always raises. The change is the test and then call to
super().__setattr__ if it's a derived class. The result is an exception
if either self is an instance of C, or if self is an instance of a
derived class, but the attribute being set is a field of C.
So going back to original questions above, my suggestions are:
1. What happens when a frozen dataclass inherits from a non-frozen
dataclass? The result is a frozen dataclass, and all fields are
non-writable. No non-fields can be added. This is a reversion to the
3.7.0a1 behavior.
2. What happens when a non-frozen dataclass inherits from a frozen
dataclass? The result is a frozen dataclass, and all fields are
non-writable. No non-fields can be added. This is a reversion to the
3.7.0a1 behavior. I'd also be okay with this case being an error, and
you'd have to explicitly mark the derived class as frozen. This is the
3.7.0a2 behavior.
3. What happens when a non-dataclass inherits from a frozen dataclass?
The fields that are in the dataclass are non-writable, but new non-field
attributes can be added. This is new behavior.
4. Can new non-field attributes be created for frozen dataclasses? No.
This is existing behavior.
I'm hoping this change isn't so large that we can't get it in to 3.7.0a3
next month.
Eric
_______________________________________________
Python-Dev mailing list
Python-Dev@python.org
https://mail.python.org/mailman/listinfo/python-dev
Unsubscribe:
https://mail.python.org/mailman/options/python-dev/archive%40mail-archive.com