This is not how one is supposed to use multiple inheritance in Python.
The language linearizes the inheritance tree for a class, so that one needs to call methods in the superclass, that is made through a single call, usually using the super()
language built-in - and not trying to manually call the methods in any specific super-class, like you do in Bat.__init__
.
That is made easier with the mechanism of "kweyword arguments", using a double-asterisk prefix to both pack and unpack arbitrary keyword arguments, allowing one method to convey parameters it doesn't even know about to the super-classes.
In your case, without this resource, the Mammal class would have to "know" at coding time that it might be in a class hierarchy along with "Bird", so it would have to recognize a wingspan
parameter, which, if passed, would be conveyed upstream:
class Mammal(Animal):
def __init__(self, species, habitat, wingspan=None):
if wingspan is None:
super().__init__(species)
else:
super().__init__(species, wingspan=wingspan)
self.habitat = habitat
This would allow your Bat to function like it is declared, but with a single super call -
class Bat(Mammal, Bird):
def __init__(self, species, habitat, wingspan, name):
super().__init__(species=specicies, wingspan=wingspan, habitat=habitat)
self.name = name
if the inheriting order where reversed, though, it is the Bird.__init__
that would have to know about the habitat
parameter and convey it optionally.
Also note that one is better requiring passing around named argument, because maintaining a consistent order across many children classes will become a nightmare, really fast.
So - this works, but it is quite awkward. When using the resource of "kwargs" that I mentioned earlier (this is a conventional abreviation, and usually the parameter name for this parameter), the code can become easily maintainable and expandable to more child classes:
Here is the working code using "kwargs" and a single super
call. (I removed the print methods for concision, they will work as they are in your code)
class Animal:
def __init__(self, species):
self.species = species
class Mammal(Animal):
def __init__(self, *, habitat, **kwargs):
super().__init__(**kwargs)
self.habitat = habitat
class Bird(Animal):
def __init__(self, *, wingspan, **kwargs):
super().__init__(**kwargs)
self.wingspan = wingspan
class Bat(Mammal, Bird):
def __init__(self, *, name, **kwargs):
super().__init__(**kwargs)
self.name = name
...
class Parrot(Bird):
def __init__(self, *, color, **kwargs):
super().__init__(**kwargs)
self.color = color
...
# Creating an instance of the Bat class
bat = Bat(species="Bat", habitat="Cave", wingspan="1.2 meters", name="Batty")
#bat.display_info()
# Creating an instance of the Parrot class
parrot = Parrot(species="Parrot", wingspan="30 cm", color="Green")
#parrot.display_info()
Note that with this approach, it would all work just the same if one would change Bat to inherit from Bird first - class Bat(Bird, Mammal):
- as the extra parameters for each __init__
are black-boxed inside the variable keyword arguments.
I would provide a link to Python docs where they explain the keyword arguments feature, but I failed to locate the resource in there - it is analogous to arbitrary argument lists, as explained in sessiosn 4.8.4 and 4.8.5 here: https://docs.python.org/3/tutorial/controlflow.html#arbitrary-argument-lists
The idea is that whenever one writes a function call, an arbitrary mapping (usually a dict), can be unpacked in place as if all of the key/value pairs in that dictionary where typed as named arguments. I.e., in:
parameters = {"fruit": "banana", "quantity": 5}
create_foods(when="now", **parameters)
The function "create_foods" would perceive the arguments as if they where written like:
create_foods(when="now", fruit="banana", quantity=5)
(Note the usage of "
: the dictionary to be expanded is a plain dictionary, which in Python means the keys are typed within quotes)
And the converse thing is the feature most used above: any named arguments a function does not "want" to know about can be catch in a single parameter, that is presented as a dictionary. So, for the above call, a function declared as:
def create_food(when=None, **kwargs):
...
Would receive the parameters for fruits
and count
packed as a dictionary in the kwargs
parameter.