1

I have a dataclass Data which holds data. I would like a subclass PositiveData, which is meant to distinguish positive data. Here is an attempt:

@dataclass(frozen=True)
class Data:
    id: int
    value: float


class PositiveData(Data):
    def __new__(cls, *args, **kwargs):
        obj = super().__new__(cls, *args, **kwargs)
        assert obj.value > 0
        return obj


x = Data(id=1, value=2)
y = PositiveData(1, value=2)

The call to PositiveData fails with error:

TypeError: object.__new__() takes exactly one argument (the type to instantiate)

Is there a way to subclass Data with a check in the constructor? Thank you.

I asked a similar question here: Subclass of dataclass, with some assertions. The difference is that here I would like the subclass to not have the dataclass decorator. It seems to cause some strange problems in my specific usecase.

hwong557
  • 1,309
  • 1
  • 10
  • 15
  • 2
    `__new__` is not, generally, the way you declare constructors in Python. It's a technical thing that can be used to do some exciting reflection. Generally, you want `__init__`. – Silvio Mayolo Aug 18 '22 at 00:31
  • @SilvioMayolo My (possibly wrong) understanding is that if you want to subclass from an immutable object (`Data` in this case), then you need `__new__` because you can't set attributes. – hwong557 Aug 18 '22 at 00:49
  • 1
    You can still use `__init__`. All `frozen=True` does is override `Data.__setattr__`, but you can still call `object.__setattr__` directly to do so. See [this answer](https://stackoverflow.com/a/58336722/2288659) (This is what Python does internally as well). And in your case, you don't set any fields in the child constructor anyway, so it all works out. – Silvio Mayolo Aug 18 '22 at 00:51

1 Answers1

0

In view of Silvio's comment above (thanks!), here is a an implementation:

from dataclasses import asdict, dataclass


@dataclass(frozen=True)
class Data:
    id: int
    value: float

    # This is not strictly necessary, but this allows us to have equality of x and y below.
    def __eq__(self, other: object) -> bool:
        if isinstance(other, Data):
            return asdict(self) == asdict(other)
        raise NotImplementedError


class PositiveData(Data):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        assert self.value > 0


x = Data(id=1, value=2)
y = PositiveData(id=1, value=2)
hwong557
  • 1,309
  • 1
  • 10
  • 15
  • 1
    don't use `asdict` because you're doing a needless conversion of a dataclass to dict. this is also a somewhat expensive call especially if you have a lot of fields in a dataclass. – rv.kvetch Aug 18 '22 at 15:23
  • 1
    rather, use `self.__dict__ == other.__dict__`. This is a lot faster because it just does attribute lookup and access, without the conversion of `asdict`. – rv.kvetch Aug 18 '22 at 15:24