Calling the base class with super()
So far the derived classes had no constructor of their own: they used the one they inherited. Often, though, a derived class needs attributes of its own. A wizard needs mana, the energy that spells use up, so Wizard needs a constructor that takes the mana too. But a constructor in Wizard overrides the one in Character, like any other method, and then the base class's constructor doesn't run at all:
class Character:
def __init__(self, name, health):
self.name = name
self.health = health
def __str__(self):
return f"{self.name} ({self.health} health)"
class Wizard(Character):
def __init__(self, name, health, mana):
self.mana = mana
elva = Wizard("Elva", 30, 100)
print(elva)
Traceback (most recent call last):
File "main.py", line 16, in <module>
print(elva)
~~~~~^^^^^^
File "main.py", line 7, in __str__
return f"{self.name} ({self.health} health)"
^^^^^^^^^
AttributeError: 'Wizard' object has no attribute 'name'The wizard was created, but only self.mana was ever set: nothing gave it a name. Copying the two lines from Character's constructor would work, but that's the very repetition inheritance was meant to remove.
super()
super() gives you the base class's version of a method. A derived class's constructor calls the base class's constructor with super().__init__(...), and then sets only its own attributes:
class Wizard(Character):
def __init__(self, name, health, mana):
super().__init__(name, health)
self.mana = mana
super().__init__(name, health) runs Character's constructor for this wizard, and that sets name and health. Leave self out of the parentheses: as with any method call, Python passes it for you.
Extending a method instead of replacing it
super() works with any method, not only the constructor. An overriding method can call the base class's version and add something to it. Below, the wizard's __str__ takes the text from Character's __str__ and adds the mana. A knight's armor stops the first few points of every hit, and the knight's take_damage lets the base class subtract what's left:
class Character:
def __init__(self, name, health):
self.name = name
self.health = health
def take_damage(self, amount):
self.health -= amount
def __str__(self):
return f"{self.name} ({self.health} health)"
class Wizard(Character):
def __init__(self, name, health, mana):
super().__init__(name, health)
self.mana = mana
def __str__(self):
return super().__str__() + f", {self.mana} mana"
class Knight(Character):
def __init__(self, name, health, armor):
super().__init__(name, health)
self.armor = armor
def take_damage(self, amount):
if amount > self.armor:
super().take_damage(amount - self.armor)
elva = Wizard("Elva", 30, 100)
bram = Knight("Bram", 50, 5)
bram.take_damage(12)
bram.take_damage(3)
print(elva)
print(bram)
Elva (30 health), 100 mana
Bram (43 health)The hit of 12 does 7 damage after the armor, and the hit of 3 does none. Notice the difference between super().__str__() and self.__str__(). Inside the wizard's __str__, self.__str__() would call the wizard's own __str__ again, which would call itself again, and so on until Python stops with an error. super() goes straight to the base class's version.