[Python-Dev] Should the dataclass frozen property apply to subclasses?

Nick Coghlan ncoghlan at gmail.com
Thu Feb 22 21:43:09 EST 2018


On 22 February 2018 at 20:55, Eric V. Smith <eric at trueblade.com> wrote:
> 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'
>
>
> This is because "frozen-ness" is implemented by adding __setattr__ and
> __delattr__ methods in D, which get inherited by S.
>
>> 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
>
>
> The only way I can think of emulating this is checking in __setattr__ to see
> if the field name is a field of the frozen class, and only raising an error
> in that case.

If you were going to do that then it would likely make more sense to
convert the frozen fields to data descriptors, so __setattr__ only
gets called for attempts to add new attributes.

Then for the `frozen=False` case, the decorator could force
__setattr__ and __delattr__ back to the default implementations from
object, rather than relying on the behaviour inherited from base
classes.

> A related issue is that dataclasses derived from frozen dataclasses are
> automatically "promoted" to being frozen.
>
>>>> @dataclass(frozen=True)
> ... class A:
> ...     i: int
> ...
>>>> @dataclass
> ... class B(A):
> ...     j: int
> ...
>>>> b = B(1, 2)
>>>> b.j = 3
> Traceback (most recent call last):
>   File "<stdin>", line 1, in <module>
>   File "C:\home\eric\local\python\cpython\lib\dataclasses.py", line 452, in
> _frozen_setattr
>     raise FrozenInstanceError(f'cannot assign to field {name!r}')
> dataclasses.FrozenInstanceError: cannot assign to field 'j'
>
> Maybe it should be an error to declare B as non-frozen?

It would be nice to avoid that, as a mutable subclass of a frozen base
class could be a nice way to model hashable-but-mutable types:

    >>> @dataclass(frozen=True) # This is the immutable/hashable bit
    ... class A:
    ...     i: int
    ...
    >>> @dataclass # This adds the mutable-but-comparable parts
    ... class B(A):
    ...     j: int
    ...     __hash__ = A.__hash__


However, disallowing this case for now *would* be a reasonable way to
postpone making a decision until 3.8 - in the meantime, folks would
still be able to experiment by overriding __setattr__ and __delattr__
after the dataclass decorator sets them.

Cheers,
Nick.

-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia


More information about the Python-Dev mailing list