# Object-Oriented Programming

# Learning Objectives

After completing this lesson, you will be able to:

  1. Create classes.
  2. Instantiate objects with constructors.
  3. Customize classes with properties and methods.
  4. Extend base classes and overrides method in subclasses.

# Lesson

Way back in the last century, in the 1990s, children all over the world obsessed over their digital pets - little electronic toys about the size of a car keyfob.

These toys had a black and white screen that showed how hungry and happy the digital pet was. You pressed buttons to feed and play with your pet, thereby increasing these levels.

In this lesson, we'll create our own digital pets using Object-Oriented Programming techniques.

Object-Oriented Programming is well-suited for programs that simulate something in the real (or imaginary) world. In this case, we'll simulate a Pet that has a certain amount of happiness and fullness. Over time these amounts start to decrease, and it's up to you to play with and feed your Pet.

# Overview

# A Virtual Pet, without Object-Oriented Programming

Before we use Object-Oriented Programming (OOP) techniques to build our Virtual Pet, let's consider what a solution might look like using the tools we already know - arrays, dictionaries, loops, and functions.

# Define a dictionary that holds our pet's attributes.
puppy = {
    "name": "Cujo",
    "fullness": 50,
    "happiness": 20,
}

# Define functions that increase a pet's attribute levels.
def feed_pet(pet):
    pet["fullness"] += 10

def play_with_pet(pet):
    pet["happiness"] += 10

# Decrease a pet's attribute levels.    
def get_hungry_and_mopey(pet):
    pet["fullness"] -= 5
    pet["happiness"] -= 5

# Prompt the user to interact with the pet
while True:

    print("""
%s's stats:
Fullness: %d
Happiness: %d
""" % (puppy["name"], puppy["fullness"], puppy["happiness"]))
    
    choice = int(input("""
1. Feed puppy
2. Play with puppy
3. Do nothing
"""))
    if choice == 1:
        feed_pet(puppy)
    elif choice == 2:
        play_with_pet(puppy)
    else:
        pass

    # Each time the loop runs, the pet
    # will need some attention!
    get_hungry_and_mopey(puppy)    
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

We define the puppy variable and assign it a dictionary that holds our pet's attributes for fullness and happiness. For convenience, we also define methods that increase and decrease these attribute levels.

A while loop handles our interactions with our pet and also calls the function that decreases the pet's levels (reflecting the passage of time).

We've repeated the strings "fullness" and "happiness" throughout the code, and this might make you a little nervous. Each string is another opportunity for a typo, potentially raising a KeyError.

And, what happens when we adopt another pet? Based on the existing code, you might simply add another dictionary. And you could keep the pets in a list:

# Define multiple dictionaries that holds each pet's attributes.
puppy_1 = {
    "name": "Cujo",
    "fullness": 50,
    "happiness": 20,
}
puppy_2 = {
    "name": "Benji",
    "fullness": 50,
    "happiness": 100,    
}

# Define a list that holds all of our pets.
pets = [puppy_1, puppy_2]
1
2
3
4
5
6
7
8
9
10
11
12
13
14

When feeding or playing with our pets, the loop gets more complicated:


















 


 









# Prompt the user to interact with pets.
while True:

    # Loop through each pet, printing their status.
    for pet in pets:
        print("""
%s's stats:
Fullness: %d
Happiness: %d
""" % (pet["name"], pet["fullness"], pet["happiness"]))
    
    choice = int(input("""
1. Feed puppy
2. Play with puppy
3. Do nothing
"""))
    if choice == 1:
        which_pet = int(input("Which pet?"))
        feed_pet(pets[which_pet])
    elif choice == 2:
        which_pet = int(input("Which pet?"))        
        play_with_pet(pets[which_pet])
    else:
        pass

    # Each time the loop runs, the pet
    # will need some attention!
    for pet in pets:
        get_hungry_and_mopey(pet)        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

We haven't even added any error handling (should the user enter an invalid value when asked "Which pet?").

Another issue is that each pet behaves exactly the same, getting hungry and mopey at the same rate (which is in no way realistic).

One solution to this is to store each pet's hunger and mopiness amounts in their dictionary.






 
 





 
 




 
 

# Define multiple dictionaries that holds each pet's attributes.
puppy_1 = {
    "name": "Cujo",
    "fullness": 50,
    "happiness": 20,
    "hunger": 7,
    "mopiness": 4,
}
puppy_2 = {
    "name": "Benji",
    "fullness": 50,
    "happiness": 100,    
    "hunger": 3,
    "mopiness": 2,
}

# Each pet is adjusted invidually.
def get_hungry_and_mopey(pet):
    pet["fullness"] -= pet["hunger"]
    pet["happiness"] -= pet["mopiness"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

When we update a pet, we subtract the that pet's hunger amount from their fullness level. Likewise, we subtract that pet's mopiness amount from their happiness level.

You can see that as we start adding more attributes to pets, we have to modify each individual dictionary, any function that alters a pet, and our main loop.

As we add features, the amount of code we must change multiplies.

Imagine trying to add:

  • the ability to adopt more pets
  • toys for pets (allowing pets to increase their own happiness)
  • play dates (so that one pet increases another pet's happiness)

Modifying and debugging our code will soon be unmanageable!

# Classes and Objects

Up to now, the programs we've written are simple enough that a single programmer can hold all the pieces of the program in their head at one time.

Our Virtual Pet program is starting to get more complex and we need a better way to organize the details. To tackle this complexity, we'll rewrite our code using classes.

You can think of a class like a blueprint for making a specific kind of house. You can use the blueprint to make individual instances of that class.

Attributes are the qualities of a specific house, such as the color of the exterior, the number of floors, and the size of the pool on the roof. Each instance will be different from other houses based on these attributes.

Classes usually have methods, which are functions that describe what instances of that class can do. Continuing with our analogy, these are like rooms in a house that have a dedicated purpose, like a kitchen or a game room.

The rooms connect to each other via a hallway and can access the attributes of that house.

here's a diagram!

Get ready for an example!

# How do you define a class?

We start by creating our Pet class:

class Pet:
    pass
1
2

Just like a function, a class has a name and an indented body. We haven't defined any attributes or methods yet, so we use python's pass keyword as a placeholder.

# How do you create an instance of a class?

When you use the name of the class like a function, it returns a new instance of that class.

cujo = Pet()
benji = Pet()
1
2

You can call a class multiple times and get a different instance of the class.

TIP

Some developers think of classes as "factories" for new instances.

# Constructors

A blueprint defines a house, but a blueprint does not build a house. In order to do that, a team of workers does the job of pouring the foundation, framing the structure, and building out the rooms.

Luckily, Python takes care of this process for us when it builds an object, letting us configure the object as it is built.

An individual object created from a class is known as an instance of that class. The function that creates instances is the constructor.

# How do you give attributes to an instance?

Let's roll our code back just a little bit so that we're only tracking the name, fullness, and happiness for a Pet:

attribute value
name "Cujo"
fullness 50
happiness 20

To implement this as part of our Pet class, you have to tell the class to add these attributes when it creates an instance.

class Pet:
    def __init__(self):
        self.name = "Cujo"
        self.fullness = 50
        self.happiness = 20
1
2
3
4
5

TIP

It is customary to name this parameter self.

Technically, you can give it any valid parameter name, but seasoned Python developers would find this jarring.

We never have to call __init__ directly - Python automatically calls it when we make a new instance:

cujo = Pet()
print(cujo.name)
# "Cujo"
1
2
3
# How do you customize the attributes of each instance?

If you make another instance of Pet, you'd find that no matter what, it has all the same attribute values as the other one.

cujo = Pet()
print(cujo.name)
# "Cujo"

benji = Pet()
print(benji.name)
# "Cujo"
1
2
3
4
5
6
7

We need a way to configure each instance with its own attribute values. We can achieve this by defining additional parameters for __init__ and using those values for the instance's attributes:


 
 
 
 

class Pet:
    def __init__(self, name, fullness, happiness):
        self.name = name
        self.fullness = fullness
        self.happiness = happiness
1
2
3
4
5

When we create new Pet instances, we pass in the values for the attributes:

 



 



cujo = Pet("Cujo", 50, 20)
print(cujo.name)
# "Cujo"

benji = Pet("Benji", 50, 100)
print(benji.name)
# "Benji"
1
2
3
4
5
6
7

As with regular functions, we can define default argument values:


 




class Pet:
    def __init__(self, name, fullness=50, happiness=50):
        self.name = name
        self.fullness = fullness
        self.happiness = happiness
1
2
3
4
5

After this modification, we only need to provide a name for each Pet instance:

 



 



cujo = Pet("Cujo")
print(cujo.name)
# "Cujo"

benji = Pet("Benji")
print(benji.name)
# "Benji"
1
2
3
4
5
6
7

# Methods

We now have a class that we can use to create instances with their own attribute values. But the real power of classes is the ability to define custom methods that make use of those attribute values.

We can add the methods eat_food() and get_love() for the Pet class. Each modifies the instance's attributes accordingly:







 
 
 
 
 

class Pet:
    def __init__(self, name, fullness=50, happiness=50):
        self.name = name
        self.fullness = fullness
        self.happiness = happiness

    def eat_food(self):
        self.fullness += 30

    def get_love(self):
        self.happiness += 30
1
2
3
4
5
6
7
8
9
10
11

As with __init__, the first parameter is self, which is how the body of the method access the instance.

cujo = Pet("Cujo")
cujo.eat_food()
print(cujo.fullness)
# 80
print(cujo.happiness)
# 50

benji = Pet("Benji")
benji.get_love()
print(cujo.fullness)
# 50
print(benji.happiness)
# 80
1
2
3
4
5
6
7
8
9
10
11
12
13

Each instance has its own eat_food() and get_love() methods. Calling cujo.eat_food() only affects the value of cujo.fullness. Likewise, calling benji.get_love() only affects the value of benji.happiness.

# Encapsulation

In Object-Oriented Programming, one of the main features of classes is that they provide a way to bundle state (attributes) and behavior (methods). This is known as encapsulation.

Practicing good encapsulation is a matter of deciding the minimum amount of information an object needs to store in its state in order to do its work via its behaviors. Likewise, methods should have as few responsibilities as possible. As with functions, a method should have one clear and specific purpose.

Implementing be_alive() so that it decrements a certain amount of fullness and happiness:













 
 
 

class Pet:
    def __init__(self, name, fullness=50, happiness=50):
        self.name = name
        self.fullness = fullness
        self.happiness = happiness

    def eat_food(self):
        self.fullness += 30

    def get_love(self):
        self.happiness += 30

    def be_alive(self):
        self.fullness -= 5
        self.happiness -= 5    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# specializing via parameters

Modifying be_alive() so that it the amounts are parameterized:













 
 
 

class Pet:
    def __init__(self, name, fullness=50, happiness=50):
        self.name = name
        self.fullness = fullness
        self.happiness = happiness

    def eat_food(self):
        self.fullness += 30

    def get_love(self):
        self.happiness += 30

    def be_alive(self, hunger, mopiness):
        self.fullness -= hunger
        self.happiness -= mopiness    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

This moves those to the constructor, but are not configurable:






 
 








 
 


class Pet:
    def __init__(self, name, fullness=50, happiness=50):
        self.name = name
        self.fullness = fullness
        self.happiness = happiness
        self.hunger = 5
        self.mopiness = 5

    def eat_food(self):
        self.fullness += 30

    def get_love(self):
        self.happiness += 30

    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

And this makes them configurable:


 



 
 











class Pet:
    def __init__(self, name, fullness=50, happiness=50, hunger=5, mopiness=5):
        self.name = name
        self.fullness = fullness
        self.happiness = happiness
        self.hunger = hunger
        self.mopiness = mopiness

    def eat_food(self):
        self.fullness += 30

    def get_love(self):
        self.happiness += 30

    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

asking ourselves, what if we end up sending the same parameters over and over, when conceptually, we mean a more specific kind of pet.

benji = Pet("Benji", 50, 20, 20, 1)
lassie = Pet("Lassie", 50, 20, 20, 1)
clifford = Pet("Old Yeller", 50, 20, 20, 1)
1
2
3

like a cuddly pet responds more strongly to affection and a light eater doesn't need to be fed very much

In the next section, we'll build out a CuddlyPup that is naturally happy and responds strongly to affection.

# Inheritance

Another technique from Object-Oriented Programming involves making specialized versions of classes.

While an instance of the Pet class can be manually configured to be happy, creating many of them with the same constructor arguments is a sign that we need another class.

This new class should be exactly like the Pet class, except with specific traits built into it. We'll tackle that in two parts. First, we'll make a subclass of Pet, taking on all of its properties and methods. Then we will handle the customizations.

Here is how to make a subclass of Pet:

class CuddlyPet(Pet):
    pass
1
2

By putting Pet in parentheses next to the name of the class, we're telling Python that the CuddlyPet class inherits from Pet.

TIP

A class that inherits from another is a subclass.

Subclasses inherit from superclasses, also known as parent classes.

There is nothing special about superclasses. They are simply classes that provide default attributes and methods for their subclasses.

This means that instances of CuddlyPet has all the same attributes and methods that instances of Pet do.

In fact, you even instantiate a CuddlyPet with the same arguments:

benji = CuddlyPet("Benji", 50, 20, 20, 1)
print(benji.fullness, benji.happiness)
# 50 20
benji.be_alive()
print(benji.fullness, benji.happiness)
# 30 19
1
2
3
4
5
6

# How do I add new methods to a subclass?

Now that we have a subclass of Pet, we need to differentiate it. Let's define a new method called cuddle():

from oop_16 import Pet
class CuddlyPet(Pet):
    def cuddle(self, other_pet):
        other_pet.get_love()
1
2
3
4

This method accepts the implicit argument self, and also accepts the argument other_pet - the target of the cuddle.

Here's an example of our new cuddle() method in action:

benji = CuddlyPet("Benji", 50, 20, 20, 1)
cujo = Pet("Cujo", 50, 10, 30, 10)
print(cujo.happiness)
# 10
benji.cuddle(cujo)
print(cujo.happiness)
# 40
1
2
3
4
5
6
7

# How do I override an existing methods in a subclass?

We know that in an instance of CuddlyPet, we have access all the properties and methods of Pet. And now we know that CuddlyPet can implement its own new methods.

But what if we wanted be_alive() to have a different effect in CuddlyPet than it does in Pet?

The answer is that we simply redefine the method.




 




class CuddlyPet(Pet):
    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness/2
        
    def cuddle(self, other_pet):
        other_pet.get_love()
1
2
3
4
5
6
7

By writing a new version of def be_alive(), we override the version in the Pet class. An instance of CuddlyPet stays happy for twice as long as your typical Pet!

# Can I override a method, but still call the superclass version?

While a CuddlyPet instance stays happy, it might be better if it always started out with different values for happiness and mopiness.

Whenever we create an instance of CuddlyPet, we want to repeat all the steps in creating an instance of Pet, but with the traits for happiness baked in.

One option is to override the __init__() method, just as we did with be_alive():

class CuddlyPet(Pet):
    def __init__(self, name, fullness=50, happiness=50, hunger=5, mopiness=5):
        self.name = name
        self.fullness = fullness
        self.happiness = 100
        self.hunger = hunger
        self.mopiness = 1
    
    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness/2
        
    def cuddle(self, other_pet):
        other_pet.get_love()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Here, we are simply disregarding the happiness and mopiness arguments passed to the constructor and using hard-coded values.

We could take this a little further and remove happiness and mopiness from the parameter list:

class CuddlyPet(Pet):
    def __init__(self, name, fullness=50, hunger=5):
        self.name = name
        self.fullness = fullness
        self.happiness = 100
        self.hunger = hunger
        self.mopiness = 1
    
    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness/2
        
    def cuddle(self, other_pet):
        other_pet.get_love()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

But this is not ideal. If we made changes to __init__() in the Pet class, we would have to repeat those changes in CuddlyPet (probably by copying and pasting the code). This problem would extend to any additional subclasses of Pet we created. It would be better if we could just call Pet's __init__() from CuddlyPet's __init__().

Luckily, Python lets us do just that - we can override __init()__ while also reusing the __init__() code from the superclass. The key is to use the built-in super() function to access Pet:



 








class CuddlyPet(Pet):
    def __init__(self, name, fullness=50, hunger=5):
        super().__init__(name, fullness, 100, hunger, 1)
    
    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness/2
        
    def cuddle(self, other_pet):
        other_pet.get_love()
1
2
3
4
5
6
7
8
9
10

With this technique, a CuddlyPet could accept additional constructor arguments:


 

 






 
 


class CuddlyPet(Pet):
    def __init__(self, name, fullness=50, hunger=5, cuddle_level=1):
        super().__init__(name, fullness, 100, hunger, 1)
        self.cuddle_level = cuddle_level
    
    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness/2
        
    def cuddle(self, other_pet):
        # Super cuddle powers, activate!
        for i in range(self.cuddle_level):
            other_pet.get_love()
1
2
3
4
5
6
7
8
9
10
11
12
13

# When should I add attributes to the superclass and when should I add them to subclasses?

When you want to make a change to the superclass and all its subclasses, make the change to the superclass. If you only want the change to affect a specific subclass, only change that subclass.

This works both for attributes as well as methods.

# Polymorphism (message passing is agnostic to class)

It's time to add our while loop back to our app so we can interact with our Pet instances.

We'll build on the previous version of the loop by providing an option to adopt new Pet instances (including CuddlyPet).

Before we continue, make sure that your Pet and CuddlyPet class definitions are in a file named pet.py:

class Pet:
    def __init__(self, name, fullness=50, happiness=50, hunger=5, mopiness=5):
        self.name = name
        self.fullness = fullness
        self.happiness = happiness
        self.hunger = hunger
        self.mopiness = mopiness

    def eat_food(self):
        self.fullness += 30

    def get_love(self):
        self.happiness += 30

    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness

        
class CuddlyPet(Pet):
    def __init__(self, name, fullness=50, hunger=5, cuddle_level=1):
        super().__init__(name, fullness, 100, hunger, 1)
        self.cuddle_level = cuddle_level
    
    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness/2
        
    def cuddle(self, other_pet):
        # Super cuddle powers, activate!
        for i in range(self.cuddle_level):
            other_pet.get_love()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

Then, create a file named main.py where our while loop will go. In this file, we'll import our two classes so we can create new instances.

Here's a skeleton version of our while loop, using a few helper functions to collect the user input and make sure the user has made a valid choice:

from pet import Pet, CuddlyPet

# Begin with no pets.
pets = []

main_menu = [   
    "Adopt a Pet",
    "Play with Pet",
    "Feed Pet",
    "View status of pets",
    "Do nothing",
]

def print_menu_error():
    print("That was not a valid choice. Try again.\n\n\n")    

def choices_to_string(choice_list):
    choice_string = ""
    num = 1
    for choice in choice_list:
        choice_string += "%d: %s\n" % (num, choice)
        num += 1
    choice_string += "Please choose an option: "
    return choice_string

def get_user_choice(choice_list):
    choice = -1
    choice_string = choices_to_string(choice_list)
    while choice == -1:
        try:
            choice = int(input(choice_string))
            if choice <= 0 or choice > len(choice_list):
                raise ValueError
        except ValueError:
            print_menu_error()
    return choice

def main():    
    while True:
        choice = get_user_choice(main_menu)
main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# How do I instantiate while putting it in a list?

Let's fill out the part of our main loop that lets the user adopt a pet. This will prompt the user for a pet name, instantiate the new Pet, and add it to the pets list:






 
 





#...
def main():    
    while True:
        choice = get_user_choice(main_menu)
        if choice == 1:
            pet_name = input("What would you like to name your pet? ")
            pets.append(Pet(pet_name))
            print("You now have %d pets" % len(pets))
            
main()

1
2
3
4
5
6
7
8
9
10
11

You'll notice that there was no need to create a variable for the new Pet. We are passing the result of Pet(pet_name) directly to pets.append().

# How do I produce a human-readable version of an instance?

If you do the following in a Python prompt:

print(Pet("Fido"))
1

You'll see something similar to the following:

<pet.Pet object at 0x7fb7d4224588>
1

That's probably not what you expected. Python is being awfully literal. It's telling us that the printable version of the Pet instance is an object located somewhere in the computer's memory - that's the 0x7fb7d4224588 part of what prints out. (When you run the same code, you'll see a different memory address.)

To get a more readable version of a Pet, add a __str__() method to it:



















 
 
 
 
 
 
 
class Pet:
    def __init__(self, name, fullness=50, happiness=50, hunger=5, mopiness=5):
        self.name = name
        self.fullness = fullness
        self.happiness = happiness
        self.hunger = hunger
        self.mopiness = mopiness

    def eat_food(self):
        self.fullness += 30

    def get_love(self):
        self.happiness += 30

    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness

    def __str__(self):
        return """
        %s:
        Fullness: %d
        Happiness: %d
        """ % (self.name, self.fullness, self.happiness)   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

Just like __init__(), Python will automatically call __str__().

TIP

__init__() and __str__() are examples of Python's "magic methods" - methods that are called automatically by Python.

To learn more about them, check the links in the Additional Resources section.

Let's use this as part of our main loop:










 
 
 
 



#...
def main():    
    while True:
        choice = get_user_choice(main_menu)
        if choice == 1:
            pet_name = input("What would you like to name your pet? ")
            pets.append(Pet(pet_name))
            print("You now have %d pets" % len(pets))

        if choice == 4:
            for pet in pets:
                print(pet)
            
main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# How do I differentiate between Pet and CuddlyPet?

You might start to worry that when we mix Pet instances and CuddlyPet instances in our list that our main loop will get terribly complicated. To avoid this, won't we have to check to see which kind of instance we're dealing with?

Spoiler alert: you don't.

Let's take this in steps. Add an adoption_menu variable to hold a list of pet choices. Then, prompt the user to choose a type of pet to adopt:


 
 
 







 
 
 
 
 
 









#...
adoption_menu = [
    "Pet",
    "Cuddly Pet"
]
#...
def main():    
    while True:
        choice = get_user_choice(main_menu)
        if choice == 1:
            pet_name = input("What would you like to name your pet? ")
            print("Please choose the type of pet:")
            type_choice = get_user_choice(adoption_menu)
            if type_choice == 1:
                pets.append(Pet(pet_name))
            elif type_choice == 2:
                pets.append(CuddlyPet(pet_name))
            print("You now have %d pets" % len(pets))

        if choice == 4:
            for pet in pets:
                print(pet)
            
main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

At this point, you should be able to adopt either a Pet or a CuddlyPet. When you choose 4 to "View status of pets", each pet should correctly use __str__() to print its status.

In Object-Oriented Programming, this has a special name - polymorphism. What this means is that regardless of the class used to create an instance, as long as the class (or its superclass) defines a particular method, the instance can respond to that message.

Put plainly, you don't have to know what kind of thing it is. You only have to pass it a message that it can respond to.

TIP

# Method resolution

When you perform a function call, such as benji.get_love(), Python checks if that object knows how to get_love(). If the class benji belongs to didn't define that method, Python checks the superclass. If that class didn't define it, Python checks the superclass of the superclass.

It does this until it finds the method or runs out of superclasses to check.

Now that we have confirmed that we don't need to differentiate between the different kinds of Pet objects we're interacting with, it's time to implement the menu options for playing with and feeding them:













 
 
 
 
 
 







def main():    
    while True:
        choice = get_user_choice(main_menu)
        if choice == 1:
            pet_name = input("What would you like to name your pet? ")
            print("Please choose the type of pet:")
            type_choice = get_user_choice(adoption_menu)
            if type_choice == 1:
                pets.append(Pet(pet_name))
            elif type_choice == 2:
                pets.append(CuddlyPet(pet_name))
            print("You now have %d pets" % len(pets))
        if choice == 2:
            for pet in pets:
                pet.get_love()
        if choice == 3:
            for pet in pets:
                pet.eat_food()
        if choice == 4:
            for pet in pets:
                print(pet)
            
main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

For the sake of brevity, we're not prompting the user to tell us which of the pets to play with or to feed.

And finally, let's add in the code that runs when we choose to do nothing:






















 
 
 
 
 



def main():    
    while True:
        choice = get_user_choice(main_menu)
        if choice == 1:
            pet_name = input("What would you like to name your pet? ")
            print("Please choose the type of pet:")
            type_choice = get_user_choice(adoption_menu)
            if type_choice == 1:
                pets.append(Pet(pet_name))
            elif type_choice == 2:
                pets.append(CuddlyPet(pet_name))
            print("You now have %d pets" % len(pets))
        if choice == 2:
            for pet in pets:
                pet.get_love()
        if choice == 3:
            for pet in pets:
                pet.eat_food()
        if choice == 4:
            for pet in pets:
                print(pet)
        if choice == 5:
            # Pet levels naturally lower.
            for pet in pets:
                pet.be_alive()
            
main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# Composition (not everything should be a subclass)

It's getting pretty tough to play with all these pups! You might consider getting some toys for them, each of which can provide them with a certain amount of happiness.

# When should I store additional information in another class?

We might think that a toy might just be a Boolean field - does the pet have a toy or not? But what an individual Pet could have multiple toys? And what if each toy could provide a certain amount of happiness, or a limited lifespan?

Once we start adding attributes to an attribute, it might be a good time to make another class.

Create a new file toy.py with the following class definition:

class Toy:
    pass
1
2

Here's a sketch of the attributes a Toy class might have:

attribute value
happiness_bonus 20
newness 10

To implement these, let's define a __init__() for the Toy class:

class Toy:
    def __init__(self, bonus=10, newness=10):
        self.bonus = 10
        self.newness = 10
1
2
3
4

And finally, we need to define what it means for a Toy to be played with. Each time we use() a Toy, it should decrease in newness and it should return a happiness bonus.






 
 
 
 
 
 

class Toy:
    def __init__(self, bonus=10, newness=10):
        self.bonus = 10
        self.newness = 10

    def use(self):
        if self.newness == 0:
            return 0
        else:
            self.newness -= 1
            return self.bonus
1
2
3
4
5
6
7
8
9
10
11

# What is the relationship between a Pet and a Toy?

As we put bundles of related state and behavior into classes, we make decisions about how the instances of those classes should interact.

A CuddlyPet is a kind of Pet. But a Toy is not a kind of Pet - it's a separate concept altogether. A Pet (or a CuddlyPet) can have toys, perhaps stored in a list attribute of the Pet class.

TIP

The terms "is-a" and "has-a" are used as a common sense way to help us decide what should be a separate class and what should be a subclass.

Let's give every Pet a list that can hold toys, starting it as an empty list:








 

class Pet:
    def __init__(self, name, fullness=50, happiness=50, hunger=5, mopiness=5):
        self.name = name
        self.fullness = fullness
        self.happiness = happiness
        self.hunger = hunger
        self.mopiness = mopiness
        self.toys = []
1
2
3
4
5
6
7
8

Then, modify the be_alive() method so that the Pet plays with toys when it's not doing anything else.






 
 

class Pet:
    #...
    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness
        for toy in self.toys:
            self.happiness += toy.use()
1
2
3
4
5
6
7

We'll make a corresponding change in CuddlyPet:






 
 

class CuddlyPet:
    #...
    def be_alive(self):
        self.fullness -= self.hunger
        self.happiness -= self.mopiness/2
        for toy in self.toys:
            self.happiness += toy.use()
1
2
3
4
5
6
7

(Can you think of any ways we could use super() instead of repeating the change?)

Now, we're ready to give Toys to the Pets!

# Giving a Toy to a Pet

Add a method to Pet that lets it receive a toy:



 
 

class Pet:
    #...
    def get_toy(self, toy):
        self.toys.append(toy)
1
2
3
4

While we could easily just call pet.toys.append(Toy()), this is not good Object-Oriented Programming. We want the Pet class to manage its own attributes. The details of how it manages its toys should be its own responsibility. Code that interacts with Pet instances should not need to know the details.

In main.py, we need to import the Toy class, add another option to the main_menu, and handle this new choice:


 









 








 
 
 
 




from pet import Pet, CuddlyPet
from toy import Toy

# Begin with no pets.
pets = []

main_menu = [   
    "Adopt a Pet",
    "Play with Pet",
    "Feed Pet",
    "View status of pets",
    "Give a toy to all your pets",
    "Do nothing",
]

#...

        if choice == 4:
            for pet in pets:
                print(pet)
        if choice == 5:
            for pet in pets:
                pet.get_toy(Toy())
        if choice == 6:
            # Pet levels naturally lower.
            for pet in pets:
                pet.be_alive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Try your code out, running through all of the menu options. Make sure to check the status of your pets to see that their changes are adjusting to your interactions.

# Summary

In this lesson, you learned how to write classes, customize object instantiation with constructors, define methods, and create subclasses.

# Training Exercises

To solidify your knowledge, here are a set of exercises that will require you to use the techniques you've just learned in the lesson above.

They are organized into small, medium, and large sized problems. The small exercises will be very similar to the examples in the lesson. If you get stuck, refer to the relevant section above. The medium exercises will require you to combine concepts. The lesson may not have a single, specific example for you to reference. The large exercises are more open-ended and may require you to search the web for additional material.

# Small

# 1. Overriding __str__() in CuddlyPet

When you pass a CuddlyPet instance to the print() function, it uses the Pet version of the method.

Override the definition of __str__() in CuddlyPet so that the return value includes the string "Extra cuddly".

# 2. Using super() in CuddlyPet.be_alive()

# Medium

# 1. Add a Treat class to your Pet simulator.

When you give one of your Pet objects a treat, prompt the user to choose one of three kinds:

  • ColdPizza
  • Bacon
  • VeganSnack

Create a class for each of these, and customize them so that they have differing effects on the Pet object's fullness and happiness levels.

# 2. Create a Menu class for your Pet simulator.

Create a Menu class that has the following attributes:

  • prompt_text - the text to show the user

Add the following methods:

  • get_choice() - shows the user the prompt_text and converts their input to an int, prompting again if they enter an invalid value.

Modify the while loop of your Pet simulator so that it uses a Menu instance to handle user interaction.

For each additional prompt (such as choosing which kind of Pet subclass to adopt), use additional Menu instances.

# Large

# 1. Garden simulator

Create the following classes to simulate a garden:

  • Tree - its shade decreases water loss by 2
  • Gnome - each instance adds a 5% chance of rain
  • Woodchuck - creates a 5% chance of a Tree disappearing
  • Garden - has separate lists for instances of Tree, Gnome, and Woodchuck

Create a main while loop that runs your simulator. During each turn, your Garden may experience rain, or may have a Woodchuck move in. For each of its lists, tally up the various percents that an event will occur and use the built-in random module to decide what happens during that turn. (See https://docs.python.org/3.5/library/random.html for more information)

Every 10th turn, you have a random chance of earning another Tree or Gnome.

The simulation ends if you reach 10 Tree instances.

# 2. Adding more classes
  • FruitTree (a subclass of Tree) - after its water_level reaches 100, it should increase its fruit attribute by 1
  • Squirrel - each one adds a 5% chance that your fruit levels will decrease by 1.

The simulation ends if your FruitTree instances are able produce 10 fruits.

# Interview Questions

# Fundamentals

  • What is Object-Oriented Programming?
  • What are the differences between classes and instances?
  • How are super classes and subclasses related?

# Bugfix

When we run this code:

class Puppy:
    def __init__(self, strength=1):
        self.happiness = 10
        self.strength = strength
        
    def bite(squeak_toy):
        if squeak_toy.did_squeak(strength):
            print("Yay!")
            self.happiness += squeak_toy.fun

class SqueakToy:
    def __init__(self, toughness=3):
        self.toughness = toughness
        
    def did_squeak(self, strength):
        return strength >= self.toughness


pup = Puppy()
toy = SqueakToy()
pup.bite(toy)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

We see the following error:

TypeError: bite() takes 1 positional argument but 2 were given
1

What needs to be changed in order to make it work?

Solution

The bite() method definition is missing one crucial piece: the self argument.






 
















class Puppy:
    def __init__(self, strength=1):
        self.happiness = 10
        self.strength = strength
        
    def bite(self, squeak_toy):
        if squeak_toy.did_squeak(strength):
            print("Yay!")
            self.happiness += squeak_toy.fun

class SqueakToy:
    def __init__(self, toughness=3):
        self.toughness = toughness
        
    def did_squeak(self, strength):
        return strength >= self.toughness


pup = Puppy()
toy = SqueakToy()
pup.bite(toy)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Remember that all methods at least one argument - a reference to the instance.

Even though you aren't responsible for passing it in (Python does that automatically), this must be declared as the first argument.

# Conceptual

Give some examples of when it is better to use composition instead of inheritance.

# Architect

You are creating a program to manage the inventory of a movie streaming service. What classes would you create? What classes, if any, would inherit from others?

# Additional Resources

  • SOLID principals
  • History of OOP
    • Also, as opposed to imperative, structured/modular, and FP (https://softwareengineering.stackexchange.com/questions/117092/whats-the-difference-between-imperative-procedural-and-structured-programming)
  • The Python Data Model
  • A Guide to Python's Magic Methods