Basic And Second Generation Inheritance In Python With super().__init__()

What Is OOP?

OOP is a way of thinking about (and doing) code that takes all these disparate data structures and functions and gloms them into one thing. It’s the difference between having, for example, all your functions and data about dogs in different places and sticking them all together in one. Doing the latter would be achieved through creating a class, which is like a blueprint for the objects we use in OOP.

What Is Inheritance?

If we’re trying to model the behavior of these creatures, there’s a lot of overlap. The Dog class, for starters, likely has methods (class-specific functions) for things like sleep(), defecate(), eat(), and so on, but all the other animals do these things too. Instead of rewriting these methods for every new mammalian class we make, we could rewind a bit and start instead with a class named Mammal, or even just Animal if we’re going for much broader strokes.

In either case, the shared methods that are used by all these forthcoming “children” should be put in the more general class Mammal. Then, when we write the Dog and Lion and Cheetah classes, we have them “inherit” those methods from the “parent” class. Simply put, we make a generic blueprint first, then make specific cases and only add the case-specific information.

The same is true with attributes, which are the data points of an object. For the animals, these could be things like height, weight, or fur_color.

Inheritance Examples

Making A Parent Class

Image from stefanonsoftware.com

To begin, let’s make our most general class. We want an inventory system that stores different types of items. A class simply named Item seems like a good enough place to start. Let’s keep it simple, and only have class Item store one piece of data, slots — or, the amount of inventory space an item takes up:

class Item:    def __init__(self, slots = 1):    self.slots = slots

Line by line, here’s what we’re doing:

  • class Item: tells Python we want to define a class named Item. Notice the lack of () which are mandatory in all function definitions. Also notice that it’s capitalized, which isn’t a programmatic necessity, but part of Pythonic cultural conventions.
  • def __init__(self, slots = 1): tells Python what information we want to create about the class when an object is initialized. What do we need to know about an item when we create that item in our program?
  • self.slots = slots is class-specific language for saying that this specific Item object’s slots are equal to what is passed in as “slots” on initialization. Redundant, I know, but that’s that.

Great! We have a class named Item that does the bare minimum. We could make an Item like this:

chocolate_bar_1 = Item()

Or like this:

chocolate_bar_2 = Item(slots = 2)

Just like in functions, if we set an argument equal to something in the definitory language, it will default to that value when called. This means that the first chocolate bar has 1 slot, just like it is set to default to in the class’s __init__() method:


Okay, so we’ve got a basic class working, but how do we build on this? Let’s say the next type of Item we want to make for our RPG is Weapon. We can define this class as a subclass of Item.

Basic Inheritance

class Weapon(Item):    def __init__(self, slots = 2, damage = 1, element = “normal”):        super().__init__(slots)        self.damage = damage        self.element = element

Line by line:

  • class Weapon(Item): tells Python we’re defining class Weapon and that it is the child of Item, notice the parentheses this time!
  • def __init__(self, slots = 2, damage = 1, element = “normal”): does the same as before, but with the addition of a few attributes that weren’t present in the parent class.
  • super().__init__(slots) is the part that actually “does” the inheritance. This just means “Use the parent’s protocol for initializing slots.” Here it doesn’t save us any lines, but in more complicated examples, it saves a lot of headache with rewriting initialization stuff.
  • self.damage = damage and self.element = element are doing the same as self.slots did in the previous example.

Simple enough, but let’s add a method! Right below that we’re adding the Weapon-specific method do_damage(). This code is being simplified for the lesson’s sake, but feel free to check out the full thing on the repo. (Nothing I’ve omitted is general enough to matter for you to learn.)

def do_damage(self, target):    target.health -= self.damage

Two things to note here:

  • First, class methods must always be passed the argument self first. This lets the class know that it’s their own version of this method that’s being called.
  • Second, the same is true when referencing attributes. Notice how we called self.damage and not just damage? Not doing this would throw an error that damage was never defined.

We have a method. We have a subclass. We have a desire for more!

“Second Generation” Inheritance

Another second-generation class would be something like Sword or MeleeWeapon. By having defined do_damage() previously in Weapon, we don’t need to rewrite in every sub-subclass, and any time we want to update it, we only have to do so in one place!

For the Blaster class, we’re adding attributes and a method:

class Blaster(Weapon):    def __init__(self, slots = 2, damage = 1, element = “normal”, mag = 8):        super().__init__(slots, damage, element)        self.range_ = range_        self.mag = mag        self.mag_status = mag        self.ammo = ammo        self.accuracy = accuracy    def shoot(self, target):        # Logic to determine if target in range, if you hit, etc.        # Removed for lesson simplicity        # Deal damage        self.do_damage(target)        # Decrement ammo        self.mag_status -= 1        print(“Successful Hit”)

So here, everything in __init__() is logically the same as it was in Weapon, as is the class Blaster(Weapon): portion. You’ll notice, however, that this time we’re inheriting three attributes from Weapon (slots, damage, element) as opposed to just one (slots).

The method shoot(), which has been simplified, contains the line self.do_damage(target). do_damage(), however, has not been defined within the Blaster class. Normally this would throw an error, but since we’re inheriting from the Weapon class, we inherit this method too, even without explicitly saying so. When we make that call to self.do_damage() it’s like we’re calling the Weapon class and asking how it’s done.

In Conclusion

Data Scientist / Python Programmer / NLP Geek