ericvsmith / dataclasses

Apache License 2.0
587 stars 53 forks source link

Please add inheritance example for __init__ from ancestor classes #94

Closed JimJJewett closed 6 years ago

JimJJewett commented 6 years ago

In particular, show how to ensure that super().init() is called, since (if I understand correctly), it should be called from __post_init__ to ensure that it happens.

ericvsmith commented 6 years ago

I don't think you'd ever call the base class __init__, although you might call __post_init__. You wouldn't call the base class __init__ because all of the fields have already been initialized in the derived class's __init__. I'll see about an example with __post_init__ chaining.

ericvsmith commented 6 years ago

I can't think of a meaningful example of this. The best I can come up with is something like:

@dataclass
class B:
  x: int
  y: int

  # always set x and y to 0, regardless of how this class was initialized
  def __post_init__(self):
    self.x = 0
    self.y = 0

@dataclass
class C(B):
  z: int

  def __post_init__(self):
    B.__post_init__(self)
    self.z = 0

assert astuple(C(1, 2, 3)) == (0, 0, 0)

I also don't plan on putting an example in the PEP, since all of this is just normal Python function calls, and not specific to dataclasses. I think this is best left for the user documentation.

JimJJewett commented 6 years ago

That reasoning works for pure-dataclass chains, but not if the dataclass inherits from a regular class, which is explicitly allowed. (Unless the generated init is fancier than I realized.)

ericvsmith commented 6 years ago

So, something like:

class B:
    def __init__(self, val):
        self.val = val

@dataclass
class C:
    i: int

    def __post_init__(self):
        B.__init__(self, 10*self.i)

c = C(5)
assert c.val == 50

?

Dutcho commented 6 years ago

I ran into a similar case.

Below fragment (no dataclass) works:

class B:
    def __init__(self, val=1):
        self.val = val
class C (B):
    pass
c = C()
assert c.val == 1

Making C into a dataclass doesn't work immediately:

from dataclasses import dataclass
class B:
    def __init__(self, val=1):
        self.val = val
@dataclass # <-- decorator added
class C (B):
    pass
c = C()
assert c.val == 1

This fragment doesn't fail on the assertion itself, but on "'C' objects has no attribute 'val'" (because B.__init__ isn't called).

__post_init__ is required to fix (which I'find somewhat counter-intuitive, as adding the @dataclass decoration shouldn't require an additional method):

from dataclasses import dataclass
class B:
    def __init__(self, val=1):
        self.val = val
@dataclass
class C (B):
    def __post_init__(self): # <-- method added
        super().__init__() # <-- base __init__ explicitly called
c = C()
assert c.val == 1

As @JimJJewett stated, I'd expect adding a call to super().__init__() (without args) to a dataclass's generated __init__ would resolve this and leave fewer surprises to the user. If args are required (i.e. no =1 default in B's parameter list), an __init__ would also have been needed in the non-dataclass case, so the user would expect they needed to supply this somehow in the dataclass case as well.

ericvsmith commented 6 years ago

This repo is now only for the backport of dataclasses.py to Python 3.6. I'm going to close this issue, please raise it on python-dev if you'd like to continue the discussion.

The reason this is happening is because the undecorated class C does not have a __init__, but the decorated one does. There are no fields defined in the decorated class, so nothing gets initialized. The generated __init__ cannot guess what parameters are needed in the base class __init__, so it never tries to call it. The mechanisms exist to make this work (either @dataclass(init=False) or __post_init__), but you do have to understand what's happening.

boeddeker commented 5 years ago

@ericvsmith is there a way to call super().__init__() before the dataclass init is called? One way to call it, would be to have a __pre_init__ as counterpart to __post_init__.

ericvsmith commented 5 years ago

@boeddeker : please open an issue on bugs.python.org for a feature request. This tracker is just for discussing the backport of dataclasses to previous Python versions.