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.
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