4

Given a dataclass structure:

@dataclass
class RichParent:
   many_fields: int = 1
   more_fields: bool = False

class Child(RichParent):
   some_extra: bool = False
   
   def __init__(seed: RichParent):
      # how to init more_fields and more_fields fields here without referencing them directly?
      # self.more_fields = seed.more_fields
      # self.many_fields = seed.many_fields
      pass

What would be the right way to shallow copy seed object fields into the new child object? I wouldn't mind even converting seed to Child type since there is no use for parent object after initialization.

Why do I do that? I want to avoid changing Child class every time RichParent has a change as long as parent stays a plain dataclass.

y.selivonchyk
  • 8,987
  • 8
  • 54
  • 77
  • 2
    In the class child: can you do something like: def __init__(self, seed: RichParent): self.__dict__.update(seed.__dict__) self.some_extra = False ? – ottonormal Dec 20 '22 at 15:19
  • Do I understand that correctly that later in the program you want to write `seed.many_fields = 10 assert child.many_fields == 10`? – kosciej16 Dec 20 '22 at 15:49
  • I want to feed it into `def consumer_func(obj: RichParent)` instead of the parent object that would expect all the data to be in place. – y.selivonchyk Dec 20 '22 at 18:17
  • 1
    `self.__dict.update(seed.__dict__)` @ottonormal would indeed work. But is it a hack or perfectly fine? Maybe dataclass has a function that would sort of do it for me, or maybe __dict__ doesn't capture everything or even capture too much of the parent object. – y.selivonchyk Dec 20 '22 at 18:34

1 Answers1

4

I am unsure why you'd want to write an explicit __init__ *, although you may need to be on Python 3.10 to be able to pass in fields specific to Child to its init. **

from dataclasses import dataclass

@dataclass(kw_only=True)
class RichParent:
   many_fields: int = 1
   more_fields: bool = False

#for some reason not setting `@dataclass`` again here means the built in str/repr
#skips the child-only field.
#also if you don't use `kw_only=True` on Child, can't pass in Child only fields
@dataclass(kw_only=True)
class Child(RichParent):
   some_extra: bool = False

seed = RichParent(many_fields=2, more_fields=True)
print(f"\n{seed=}")


child = Child(**vars(seed))
print(f"\n{child=} with {vars(child)=} which does include more_fields")

child2 = Child(**vars(seed),some_extra=True)
print(f"\n{child2=}")


#see the behavior changes in print and field acceptable to constructor
class ChildNoDC(RichParent):
   some_extra: bool = False

child_no_dcdec = ChildNoDC(**vars(seed))
print(f"\n{child_no_dcdec=} with {vars(child_no_dcdec)=} which does include more_fields")



try:
    child_no_dcdec2 = ChildNoDC(some_extra=True,**vars(seed))
except (Exception,) as e: 
    print("\n",e, "as expected")



output:

seed=RichParent(many_fields=2, more_fields=True)

child=Child(many_fields=2, more_fields=True, some_extra=False) with vars(child)={'many_fields': 2, 'more_fields': True, 'some_extra': False} which does include more_fields

child2=Child(many_fields=2, more_fields=True, some_extra=True)

child_no_dcdec=ChildNoDC(many_fields=2, more_fields=True) with vars(child_no_dcdec)={'many_fields': 2, 'more_fields': True} which does include more_fields

 RichParent.__init__() got an unexpected keyword argument 'some_extra' as expected

*

If you did need some custom init on Child, use the builtin post_init hook to do it:

    def __post_init__(self):
        print(f"{self} lets do stuff here besides the std data init...")

From talking about shallow copies, you know about mutability issues, but you could hack some stuff like self.my_dict = self.my_dict.copy() hacks here to work around that.

**

as far as I understand, @dataclass does not flip the class into a dataclass type. It just builds an __init__ and some methods, (using metaclasses?). So ChildNoDC doesn't know it is dataclass-style class and just tries to pass all fields passed into its constructor call onto Parent's __init__. Child having been "told" it's dataclassed too, knows better. (That's also the reason the child-only field wasn't being printed). And kw-only flag frees up some positioning and defaults-only constraints too.

JL Peyret
  • 10,917
  • 2
  • 54
  • 73