OOP TUTORIAL — PYTHON
Basic And Second Generation Inheritance In Python With
What Is OOP?
Before we get into the meat of this lesson, there’s a few things that need defining. Obviously the terms in the title need it, but so too does “object-oriented programming” (OOP), the topic within which all of this sits.
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?
Remember how I said we could make a “class” pertaining to dogs? What if we want to have a class pertaining to lions, too? Cheetahs? Other terrestrial critters? Well then we’d have to program another class for each of these examples, and our code could become quite redundant.
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
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
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
Making A Parent Class
Okay so we’ve got the abstract theory in place, but what does this look like in practice? For examples, I’ll be going off this repository I’m working on to simulate an RPG inventory system. No knowledge of video games necessary to follow along.
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 = slotsis 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
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
So, before we write class
Weapon, let’s think about what all weapons have in common — They deal damage. For simpleness sake, that’s the only thing we’re going to program them to do. But before we can do that, we’ve got to define our
__init__() method. But! Since this class will be the child of class
Item, we do it a little differently:
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
Weaponand 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 = damageand
self.element = elementare doing the same as
self.slotsdid in the previous example.
Simple enough, but let’s add a method! Right below that we’re adding the
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
selffirst. 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.damageand not just
damage? Not doing this would throw an error that
damagewas never defined.
We have a method. We have a subclass. We have a desire for more!
“Second Generation” Inheritance
I haven’t seen anyone else call it this, and honestly I’m not sure if there’s an agreed-upon term for this, but I’m talking about subclasses of subclasses and so on. What we’re doing here is making a subclass of
Blaster. Lazer rifles, bazookas, wild west revolvers. Anything that shoots will be counted as a
Another second-generation class would be something like
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!
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 (
element) as opposed to just one (
shoot(), which has been simplified, contains the line
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.
You’ve now learned how to enact Python’s
super().__init__() to perform inheritance, and even how to do so on a second-generation scale. Now go get coding, and show me what you make! I’m accessible on Twitter at @zych_steven. The full repository with much more complicated code can be found here if you wanna dive into this inventory system, or drop a suggestion or two! Thanks for reading! :)