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

Reply via email to