2

I have a situation where I extend a class with several attributes:

class SuperClass:
    def __init__(self, tediously, many, attributes):
        # assign the attributes like "self.attr = attr"

class SubClass:
    def __init__(self, id, **kwargs):
        self.id = id
        super().__init__(**kwargs)

And then I want to create instances, but I understand that this leads to a situation where a subclass can only be instantiated like this:

super_instance = SuperClass(tediously, many, attributes)

sub_instance = SubClass(id, tediously=super_instance.tediously, many=super_instance.many, attributes=super_instance.attributes)

My question is if anything prettier / cleaner can be done to instantiate a subclass by copying a superclass instance's attributes, without having to write a piece of sausage code to manually do it (either in the constructor call, or a constructor function's body)... Something like:

utopic_sub_instance = SubClass(id, **super_instance)
peterz
  • 61
  • 5
  • Does this answer your question? [What are "inheritable alternative constructors"?](https://stackoverflow.com/questions/42996825/what-are-inheritable-alternative-constructors) – mkrieger1 Jul 27 '20 at 14:59
  • @mkrieger1 Not really, I'm interested if I can somehow pass a super_instance to the constructor of SubClass and have it fill the fields inherited from SuperClass. I am basically trying to avoid having to manually assign the many attributes inherited from SuperClass – peterz Jul 27 '20 at 18:19
  • Yes, you can do that by writing an alternative constructor which you can call like `SubClass.from_superclass(super_instance, id)` - how to write an alternative constructor is shown in the other question. – mkrieger1 Jul 27 '20 at 18:19
  • @mkrieger1 but won't I have to manually assign the attribute values from super_instance to the self attributes of SubClass in this alt constructor? – peterz Jul 27 '20 at 19:00
  • Yes, or maybe this could be helpful: https://stackoverflow.com/questions/1241148/copy-constructor-in-python – mkrieger1 Jul 27 '20 at 21:25

3 Answers3

2

Maybe you want some concrete ideas of how to not write so much code? So one way to do it would be like this:

class A:
    def __init___(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c


class B:
    def __init__(self, x, a, b, c):
        self.x = x
        super().__init__(a, b, c)


a = A(1, 2, 3)
b = B('x', 1, 2, 3)


# so your problem is that you want to avoid passing 1,2,3 manually, right?
# So as a comment suggests, you should use alternative constructors here.
# Alternative constructors are good because people not very familiar with
#  Python could also understand them.
# Alternatively, you could use this syntax, but it is a little dangerous and prone to producing 
# bugs in the future that are hard to spot


class BDangerous:
    def __init__(self, x, a, b, c):
        self.x = x
        kwargs = dict(locals())
        kwargs.pop('x')
        kwargs.pop('self')

        # This is dangerous because if in the future someone adds a variable in this 
        # scope, you need to remember to pop that also
        # Also, if in the future, the super constructor acquires the same parameter that
        # someone else adds as a variable here... maybe you will end up passing an argument
        # unwillingly. That might cause a bug
        # kwargs.pop(...pop all variable names you don't want to pass)
        super().__init__(**kwargs)


class BSafe:
    def __init__(self, x, a, b, c):
        self.x = x
        bad_kwargs = dict(locals())

        # This is safer: you are explicit about which arguments you're passing
        good_kwargs = {}
        for name in 'a,b,c'.split(','):
            good_kwargs[name] = bad_kwargs[name]

        # but really, this solution is not that much better compared to simply passing all 
        # parameters explicitly
        super().__init__(**good_kwargs)
  

Alternatively, let's go a little crazier. We'll use introspection to dynamically build the dict to pass as arguments. I have not included in my example the case where there are keyword-only arguments, defaults, *args or **kwargs

class A:
    def __init__(self, a,b,c):
        self.a = a
        self.b = b
        self.c = c


class B(A):
    def __init__(self, x,y,z, super_instance):
        import inspect
        spec = inspect.getfullargspec(A.__init__)

        positional_args = []
        super_vars = vars(super_instance)

        for arg_name in spec.args[1:]:  # to exclude 'self'
            positional_args.append(super_vars[arg_name])

        # ...but of course, you must have the guarantee that constructor
        # arguments will be set as instance attributes with the same names
        super().__init__(*positional_args)
vlad-ardelean
  • 7,480
  • 15
  • 80
  • 124
  • Oh, so there isn't really a way to be lazy here... Unfortunate. Probably using an alternative constructor with explicit passing of parameters is the way to go then – peterz Jul 28 '20 at 10:01
  • instance_b = Subclass.from_super_instance(arg1, arg2, super_instance=instance_a)... and then you implement from_super_instance to set the fields for you :P – vlad-ardelean Jul 28 '20 at 10:33
2

I managed to finally do it using a combination of an alt constructor and the __dict__ property of the super_instance.

class SuperClass:
    def __init__(self, tediously, many, attributes):
        self.tediously = tediously 
        self.many = many 
        self.attributes = attributes

class SubClass(SuperClass):
    def __init__(self, additional_attribute, tediously, many, attributes):
        self.additional_attribute = additional_attribute
        super().__init__(tediously, many, attributes)

    @classmethod
    def from_super_instance(cls, additional_attribute, super_instance):
        return cls(additional_attribute=additional_attribute, **super_instance.__dict__)

super_instance = SuperClass("tediously", "many", "attributes")

sub_instance = SubClass.from_super_instance("additional_attribute", super_instance)

NOTE: Bear in mind that python executes statements sequentially, so if you want to override the value of an inherited attribute, put super().__init__() before the other assignment statements in SubClass.__init__.

NOTE 2: pydantic has this very nice feature where their BaseModel class auto generates an .__init__() method, helps with attribute type validation and offers a .dict() method for such models (it's basically the same as .__dict__ though).

peterz
  • 61
  • 5
  • In pydantic, you want to use `dict(super_instance)` instead of the method `super_instance.dict()`. The latter is a deep transformation into a dictionary, which recursively turns the _fields_ of the superclass into dict too (indefinitely), while the former simply deconstructs the object into its fields. See [here](https://pydantic-docs.helpmanual.io/usage/exporting_models/#dictmodel-and-iteration). – amka66 May 26 '22 at 01:48
1

Kinda ran into the same question and just figured one could simply do:

class SubClass(SuperClass):
    def __init__(self, additional_attribute, **args):
        self.additional_attribute = additional_attribute
        super().__init__(**args)

super_class = SuperClass("tediously", "many", "attributes")
sub_instance = SuperClass("additional_attribute", **super_class.__dict__)

ChrisDelClea
  • 307
  • 2
  • 8
  • It's a good idea, but you need to take care about cases similar to @vlad-ardelean 's `BDangerous` class: it's kinda difficult to maintain, because you actually have to remember to do it if the context changes later on in the project's timeline. – peterz Aug 06 '22 at 15:00