Search code examples
pythoninheritanceconstructorclass-method

Overriding __init__ with parent classmethod in python


I want to do something like the following (in Python 3.7):

class Animal:

    def __init__(self, name, legs):
        self.legs = legs
        print(name)

    @classmethod 
    def with_two_legs(cls, name):
        # extremely long code to generate name_full from name
        name_full = name
        return cls(name_full, 2)

class Human(Animal):

    def __init__(self):
        super().with_two_legs('Human')

john = Human()

Basically, I want to override the __init__ method of a child class with a factory classmethod of the parent. The code as written, however, does not work, and raises:

TypeError: __init__() takes 1 positional argument but 3 were given

I think this means that super().with_two_legs('Human') passes Human as the cls variable.

1) Why doesn't this work as written? I assumed super() would return a proxy instance of the superclass, so cls would be Animal right?

2) Even if this was the case I don't think this code achieves what I want, since the classmethod returns an instance of Animal, but I just want to initialize Human in the same way classmethod does, is there any way to achieve the behaviour I want?

I hope this is not a very obvious question, I found the documentation on super() somewhat confusing.


Solution

  • super().with_two_legs('Human') does in fact call Animal's with_two_legs, but it passes Human as the cls, not Animal. super() makes the proxy object only to assist with method lookup, it doesn't change what gets passed (it's still the same self or cls it originated from). In this case, super() isn't even doing anything useful, because Human doesn't override with_two_legs, so:

    super().with_two_legs('Human')
    

    means "call with_two_legs from the first class above Human in the hierarchy which defines it", and:

    cls.with_two_legs('Human')
    

    means "call with_two_legs on the first class in the hierarchy starting with cls that defines it". As long as no class below Animal defines it, those do the same thing.

    This means your code breaks at return cls(name_full, 2), because cls is still Human, and your Human.__init__ doesn't take any arguments beyond self. Even if you futzed around to make it work (e.g. by adding two optional arguments that you ignore), this would cause an infinite loop, as Human.__init__ called Animal.with_two_legs, which in turn tried to construct a Human, calling Human.__init__ again.

    What you're trying to do is not a great idea; alternate constructors, by their nature, depend on the core constructor/initializer for the class. If you try to make a core constructor/initializer that relies on an alternate constructor, you've created a circular dependency.

    In this particular case, I'd recommend avoiding the alternate constructor, in favor of either explicitly providing the legs count always, or using an intermediate TwoLeggedAnimal class that performs the task of your alternate constructor. If you want to reuse code, the second option just means your "extremely long code to generate name_full from name" can go in TwoLeggedAnimal's __init__; in the first option, you'd just write a staticmethod that factors out that code so it can be used by both with_two_legs and other constructors that need to use it.

    The class hierarchy would look something like:

    class Animal:
        def __init__(self, name, legs):
            self.legs = legs
            print(name)
    
    class TwoLeggedAnimal(Animal)
        def __init__(self, name):
            # extremely long code to generate name_full from name
            name_full = name
            super().__init__(name_full, 2)
    
    class Human(TwoLeggedAnimal):
        def __init__(self):
            super().__init__('Human')
    

    The common code approach would instead be something like:

    class Animal:
        def __init__(self, name, legs):
            self.legs = legs
            print(name)
    
        @staticmethod
        def _make_two_legged_name(basename):
            # extremely long code to generate name_full from name
            return name_full
    
        @classmethod 
        def with_two_legs(cls, name):
            return cls(cls._make_two_legged_name(name), 2)
    
    class Human(Animal):
        def __init__(self):
            super().__init__(self._make_two_legged_name('Human'), 2)
    

    Side-note: What you were trying to do wouldn't work even if you worked around the recursion, because __init__ doesn't make new instances, it initializes existing instances. So even if you call super().with_two_legs('Human') and it somehow works, it's making and returning a completely different instance, but not doing anything to the self received by __init__ which is what's actually being created. The best you'd have been able to do is something like:

    def __init__(self):
        self_template = super().with_two_legs('Human')
        # Cheaty way to copy all attributes from self_template to self, assuming no use
        # of __slots__
        vars(self).update(vars(self_template))
    

    There is no way to call an alternate constructor in __init__ and have it change self implicitly. About the only way I can think of to make this work in the way you intended without creating helper methods and preserving your alternate constructor would be to use __new__ instead of __init__ (so you can return an instance created by another constructor), and doing awful things with the alternate constructor to explicitly call the top class's __new__ to avoid circular calling dependencies:

    class Animal:
        def __new__(cls, name, legs):  # Use __new__ instead of __init__
            self = super().__new__(cls)  # Constructs base object
            self.legs = legs
            print(name)
            return self  # Returns initialized object
        @classmethod
        def with_two_legs(cls, name):
            # extremely long code to generate name_full from name
            name_full = name
            return Animal.__new__(cls, name_full, 2)  # Explicitly call Animal's __new__ using correct subclass
    
    class Human(Animal):
        def __new__(cls):
            return super().with_two_legs('Human')  # Return result of alternate constructor