Closed thisisrandy closed 3 years ago
DataClassMeta.new is invoked once by the dataclass decorator, and then once again going up the chain from
Derived
->Base
->DataClassMeta
(->type
), which sees the result of the first invocation and decides it doesn't need to add any new slots, only to have the result of the first invocation lost in the final result. Does that sound like a reasonable version of events?
Yes, if I understand your explanation correctly, this is indeed what happens. Using a metaclass means that DataClassMeta.__new__
is run for each new subclass, then applying the decorator means the subclass is replaced with a new version (this happens here if you are interested). It can cause issues.
If you're wondering, the way my fix works is by exploiting the fact that the dict_
passed in looks different between these two invocations. In the first instance (subclassing), the metaclass is called before Python has done any of its inheritance magic, meaning __slots__
is empty. In the second instance (decorator usage), the class is already fully formed, meaning __slots__
has been set, so it's not replaced.
This change also means that a user-defined __slots__
will not be wiped out which I think is good anyway.
Thanks for the detailed explanation and quick fix. I'm not sure I had it 100% in my original explanation, but it's clear what was going on now.
Code speaks more precisely than words:
My expectation, if it isn't obvious, is that
Derived.__slots__ == ("bar",)
, just as would be true of any other python derived class adding additional slots to its parent.The desired effect can be acheived by omitting the decorator on the derived class:
but this seems non-ideal. If I use the built-in
dataclasses
with__slots__
, I can decorate the derived class without any issues, so it would be much clearer ifdataclassy
behaved the same way:Of course, I also want to use default values, so I would much prefer to use
dataclassy
. You'll probably not be surprised to learn that I came here via your SO answer.I've tried to do some debugging myself, and it looks like for the
Base
/Derived
example that I began with, we end up inDataClassMeta.__new__
three times, during the third of which we begin withdict_["__slots__"] == ("bar",) and bases == (Base,)
,Base
thus providingfoo
toall_slots
, so at line 110, we setdict_["__slots__"] = ()
. I literally learned what a metaclass is today in order to try to figure this out, so I might be off the mark, but I think the problem is thatDataClassMeta.__new__
is invoked once by thedataclass
decorator, and then once again going up the chain fromDerived
->Base
->DataClassMeta
(->type
), which sees the result of the first invocation and decides it doesn't need to add any new slots, only to have the result of the first invocation lost in the final result. Does that sound like a reasonable version of events?This is a lot more nuts and bolts of python than I normally dig into, so I can't say I know what to do about it, but perhaps there's an obvious or at least feasible solution here that you can illuminate. Thanks in advance.
EDIT: I just finished the docs, and I realized that you tout the need to only apply the decorator to base classes as a feature. I think I'm inclined to agree, but it probably also explains why this issue wasn't considered during design. I'll probably go ahead and remove the decorator from subclasses in my project for now, but I still believe that this should be fixed to more closely mimic the
dataclasses
API.