Inheritance: understanding OOP in Python

By Russell Barnes. Posted

Discover one of OOP's most powerful features, how objects inherit from each other

Inheritance is an incredibly powerful feature in object oriented programming (OOP). With OOP we can create hundreds or thousands of objects with their own set of variables.

In our previous programming guides, we used Scratch and then Python to create a dice game. Players are objects, and each has a set of three dice that they roll. The player with the highest score wins.

The program doesn't really test anything, because the players are all using the same dice and have the same chance of winning. What would happen if we introduced some cheats? These will use inheritance to get the dice and roll methods from our dice players, but add their own cheat methods.

See also:

Inheritance: create cheats for our dice game

We’re going to create a cheat who swaps out one die for a six. The blighter. Our cheat is not alone. We have another one who uses three loaded dice. They always roll one higher (unless they’re already a six).

Both cheats would beat a regular player over a few hundred games. But which of the cheats would win against the other?

It’s pretty close. To find out the answer, we’ll need to run a simulation that plays hundreds of thousands of games.

We’re going to create our cheats using a technique called inheritance. This technique is where you create a class that takes on all the properties (instance variables and methods) of another class. It also adds a few of its own.

Think of a child inheriting its parents’ features. It might get its dad’s big nose but grow knobbly knees all by itself.

Our cheats will use inheritance to get the same dice and roll functions as the parent, but they will have cheat functions all of their own.

We'll start by creating this program bunco_module.py:

from random import randint

class Player:
  def __init__(self):
    self.dice = []

  def roll(self):
    self.dice = [] # clears current dice
    for i in range(3):
      self.dice.append(randint(1,6))

  def get_dice(self):
    return self.dice

class Cheat_Swapper(Player):
  def cheat(self):
    self.dice[-1] = 6

class Cheat_Loaded_Dice(Player):
  def cheat(self):
    i = 0
    while i < len(self.dice):
      if self.dice[i] < 6:
        self.dice[i] += 1
      i += 1

Understanding the Bunco Module code

The bunco_module.py program defines the Player() class and two children:

class Player:
class Cheat_Swapper(Player):
class Cheat_Loaded_Dice(Player):

Objects that inherit from a parent are defined using the same class keyword.

However, the name of the parent is placed inside parentheses of the child.

Our two cheats inherit all the variables and methods (functions) from the parent. So they already have a dice list and roll() and get_dice() methods.

We now give each cheat an additional method, called cheat. This is implemented in a different way for each type of cheat.

Inheritance: creating additional methods

The Cheat_Swapper class definition has a relatively straightforward cheat method:

class Cheat_Swapper(Player):
    def cheat(self):
    self.dice[-1] = 6

Cheat_Swapper’s cheat method finds the last item in the dice list and sets it to 6.

Our CheatLoadedDice class definition has a slightly more complicated cheat method:

class Cheat_Loaded_Dice(Player):
    def cheat(self):
        i = 0
        while i < len(self.dice):
            if self.dice[i] < 6:
            self.dice[i] += 1
            i += 1

This method iterates through the dice in the list, checking whether each die is lower than six. If so, it increases its value by one.

Make sure that you create the code in bunco_module.py and save it with the same name.

Notice that this code doesn’t have any procedural programming below it. This is because we’re going to import it (so you can see what happens when you use import in your Python programs).

Now we will create the code that uses these objects in a separate file. Create this buncosingletest.py code and make sure you save it in the same directory as bunco_module.py.

from bunco_module import Player
from bunco_module import Cheat_Swapper
from bunco_module import Cheat_Loaded_Dice

cheater1 = Cheat_Swapper()
cheater2 = Cheat_Loaded_Dice()

cheater1.roll()
cheater2.roll()

cheater1.cheat()
cheater2.cheat()

print("Cheater 1 rolled" + str(cheater1.get_dice()))
print("Cheater 2 rolled" + str(cheater2.get_dice()))

if sum(cheater1.get_dice()) == sum(cheater2.get_dice()):
  print("Draw!")

elif sum(cheater1.get_dice()) > sum(cheater2.get_dice()):
  print("Cheater 1 wins!")

else:
  print("Cheater 2 wins!")

Understanding the single test code

The first line imports the Player class definitions from our bunco_module.py.

from bunco_module import Player

Extra points to you if you spotted that bunco_module is listed without the ‘.py’ file extension. This is how you import code from other files into your program.

The import Player line pastes in the class Player code from bunco_module.py. It’s as if you had included that code in your program.

Compare this line to the from random import randint code at the start of bunco_module.py. The idea is the same. Only the randint.py file is stored elsewhere on your computer in a folder that Python checks automatically. Your module must be placed in the same folder as your program.

Importing our own modules in Python

We import the other two class definitions we created:

from bunco_module import Cheat_Swapper
from bunco_module import Cheat_Loaded_Dice

The rest of the buncosingletest.py code creates the same game as our earlier bunco_oop.py program.

Now we create two object instances using the CheatSwapper and CheatLoadedDice definitions we imported from buncomodule:

cheater1 = Cheat_Swapper()
cheater2 = Cheat_Loaded_Dice()

We then use the roll() method. Notice that neither CheatSwapper() or CheatLoaded_Dice() has a roll method definition. This is a function they both inherit from their parent class, Player():

cheater1.roll()
cheater2.roll()

Next we call the cheat() method from each object instance:

cheater1.cheat()
cheater2.cheat()

Although each object has a method called cheat(), the objects have different implementations. So cheater1 changes the last die to a 6, and cheater2 increases each individual die’s value by one.

Run the program by pressing F5 and see which of the players wins. Run it again and you’ll get different results. Keep running the program and you’ll find it’s a pretty close call.

Look inside the folder containing the code and you’ll see a new file has appeared called buncomodule.pyc. This is a ‘compiled file’ and is created the first time you run a program that imports code. You don’t usually see compiled files because you import code tucked away inside Python on your computer. Don’t worry about it. You can’t open and make sense of it in a text editor. Delete it if you wish. It’ll be recreated when you use buncomodule.py in our final program. You can just ignore it for now.

To discover which of the two cheats has the edge, we need to run a simulation. We need to play hundreds of thousands of games and keep track of who wins the games.

Learn OOP in Python: The final simulation

Our final program, buncosimulation.py, does just that. This program brings together everything we’ve learned about OOP. The code in buncosimulation.py creates two cheats and plays 100,000 games. It imports class definitions from our bunco_module.py program (so make sure you save it in the same folder).

You can find bunco_simulation.py in The MagPi 54 on Page 74.

From The MagPi store

Subscribe

Subscribe to the newsletter

Get every issue delivered directly to your inbox and keep up to date with the latest news, offers, events, and more.