Object Oriented Programming#

Object oriented programming is a way to organize your code around objects, or types, instead of functions or dictionaries.

It allows us to define our own types and give those types properties and behaviors.

Table of Contents

Introduction#

Lets think back to our old pypet program. We had something like this:

Listing 301 old pypet.py#
 1cat = {
 2    'name': "Flufosourus",
 3    'weight': 7,
 4    'is_hungry': True,
 5    'pic': "(=^o.o^=)__",
 6}
 7
 8fish = {
 9    'name': "Scaley",
10    'weight': 0.5,
11    'is_hungry': False,
12    'pic': "<`)))><",
13}
14
15def feed(pet):
16    if pet["is_hungry"]:
17        print("Feeding: " + pet["name"])
18        pet["is_hungry"] = False
19        pet["weight"] = pet["weight"] + 1
20    else:
21        print(pet["name"] + "is not hungry, thanks anyway.")
22
23def main():
24  feed(cat)
25  feed(fish)
26
27if __name__ == "__main__":
28  main()

There are a lot of similarities between each of our pets, aren’t there? Each one has a name, a weight, a pic and an is_hungry value.

In this lesson we’ll rewrite that using object oriented programming, staring by creating an Animal class.

Part 1: Simple Classes#

Here is the simplest class.

class Animal:
    ...

Now we can create a new animal instance like so:

cat = Animal()
type(cat)
__main__.Animal

We could assign properties to cat after it has been instantiated.

cat.name = "Flufosourus"
cat.weight = 7
cat.is_hungry = False
cat.pic = "(=^o.o^=)__"

print(cat.name)
print(cat.pic)
Flufosourus
(=^o.o^=)__

Part 1.1 Exercise#

Exercise 94 (Car class)

  1. Create a Car class.

  2. Make a new Car object and assign it to the variable car.

  3. Add properties for make, model, color and year.

  4. Print those properties make, model, color and year.

Part 2: Constructors#

Usually when we create a class, we have an idea of what properties it is going to have. Instead of relying on the programmer to assign arbitrary properties, we often want the programmer to provide those values when the object is being created.

This is where a constructor comes in. A constructor is a dunder method that is used to create new instances of that class like when you call Animal().

To make a constructor we’ll add the __init__ method to the Animal class. Class methods always take at least one argument self, which is a special variable that refers to the object itself.

class Animal:
    def __init__(self):
        print("Here is a new animal.")

Now lets see what happens when we create a new Animal object.

cat = Animal()
Here is a new animal.

To make this class more useful, lets have __init__ take the arguments name, pic, weight and is_hungry. Then we’ll use self to assign each of these values to their respective properties on the object.

class Animal:
    def __init__(self, name, pic, weight, is_hungry):
        self.name = name
        self.pic = pic
        self.weight = weight
        self.is_hungry = is_hungry

        print(f"Here is your new animal: {self.name}.")

Now we can pass in arguments when we instantiate the object.

cat = Animal(
  "Flufosourus",
  "(=^o.o^=)__",
  7,
  True
)
Here is your new animal: Flufosourus.

And each of the properties that we assigned on self will now be available from cat.

print(cat.name)
print(cat.pic)
Flufosourus
(=^o.o^=)__

Part 2.1 Exercise#

Exercise 95 (Car Constructor)

  1. Add an __init__ method to your car class that takes the arguments make, model, year, and color.

  2. Remove the lines where you set the properties on car.

  3. Modify your code that creates the car object to send those values as arguments to Car.

Part 3: Default and Keyword Arguments#

Often you want to give an property a default value if the user does not specify the value. You can do this in the method definition with PARAM=DEFAULT_VALUE.

Lets make is_hungry False by default.

class Animal:
    def __init__(self, name, pic, weight, is_hungry=False):
        self.name = name
        self.pic = pic
        self.weight = weight
        self.is_hungry = is_hungry

Now when we create the cat object, we can choose to leave off the is_hungry argument, and it will get set to False.

cat = Animal(
  "Flufosourus",
  "(=^o.o^=)__",
  7,
)

print(cat.is_hungry)
False

If you have more than two arguments or if it isn’t obvious what the arguments are just by looking at it, it can make your code clearer to use keyword arguments.

To do this, just put NAME=VALUE when calling the constructor.

cat = Animal(
  name="Flufosourus",
  pic="(=^o.o^=)__",
  weight=7,
)

Part 3.1 Exercises#

Exercise 96 (Car Keyword Arguments)

Modify where you create the car object to use keyword arguments.

Exercise 97 (Car Default)

  1. Add an is_clean argument to the Car constructor and set it to False by default.

  2. In the constructor, set the is_clean property on self to the value of the is_clean argument.

  3. After creating the car object, print the value of car.is_clean.

  4. Make a second Car object named truck with different values for the make, model and so on. Pass True for is_clean.

  5. Print the properties for truck, including is_clean.

Part 4: Methods#

A benefit to writing things in an object oriented way is that data and the behavior associated with it are all packaged together. That means that an objects methods already have access to its properties.

To demonstrate this, lets add a feed() method to the Animal class.

class Animal:
    def __init__(self, name, pic, weight, is_hungry=False):
        self.name = name
        self.pic = pic
        self.weight = weight
        self.is_hungry = is_hungry

        print(f"Here is your new animal: {self.name}.")

    def feed(self):
        if self.is_hungry:
            print("Feeding: " + self.name)
            self.is_hungry = False
            self.weight = self.weight + 1
        else:
            print(self.name + "is not hungry, thanks anyway.")

cat = Animal(
    "Flufosourus",
    "(=^o.o^=)__",
    7,
    True
)

print(cat.name)
print(cat.pic)
Here is your new animal: Flufosourus.
Flufosourus
(=^o.o^=)__

Now we can call cat.feed().

cat.feed()
Feeding: Flufosourus

And we can see that cat.weight and cat.is_hungry have both been changed.

print(cat.weight)
print(cat.is_hungry)
8
False

Part 4.1 Exercise#

Exercise 98 (Car Method)

  1. Add a wash method to your Car class that:

    • prints washing the YEAR MAKE MODEL

    • changes is_clean to True.

  2. Call the wash() method on car and truck then print the is_clean property of each.

Part 5: Class Properties#

When you set properties inside the class via self., or outside of the class using an object that has already been instantiated, these properties are called instance properties, which means that they belong to an individual object.

You can also set properties that belong to the class and are the same for all instances.

You can do this just like assigning any variable, except inside the class.

class Animal:
    ears = 2

    def __init__(self, name, pic, weight, is_hungry=False):
        self.name = name
        self.pic = pic
        self.weight = weight
        self.is_hungry = is_hungry

    def feed(self):
        if self.is_hungry:
            print("Feeding: " + self.name)
            self.is_hungry = False
            self.weight = self.weight + 1
        else:
            print(self.name + "is not hungry, thanks anyway.")

You can then access it via dot notation on the class.

Animal.ears
2

As well as on every instance of that class.

cat = Animal(
    "Flufosourus",
    "(=^o.o^=)__",
    7,
    True
)

cat.ears
2

You can change the value on any particular instance, but the class value will remain the same. This can be handy for defaults values.

snake = Animal(
    "Medusa",
    r"_/\__/\_/--{ :>~",
    2,
)

snake.ears = 0

print("Medusa's:", snake.ears)
print("Animal:", Animal.ears)
Medusa's: 0
Animal: 2

Part 5.1: Exercise#

Exercise 99 (Class Properties)

Add the class property doors to the Car class and assign it the value of 4. After creating your truck object, set the value of its doors property to 2. Print the value of Car.doors, car.doors and truck.doors.

Part 6: Gotchas with Mutable Types#

You have to be careful with mutable types when it comes to default arguments or class properties, as they can lead to unexpected behavior.

Let’s say we add a class attribute toys to the Animal class, and assign it to an empty list.

class Animal:
    ears = 2
    toys = []

    def __init__(self, name, pic, weight, is_hungry=False):
        self.name = name
        self.pic = pic
        self.weight = weight
        self.is_hungry = is_hungry

    def feed(self):
        if self.is_hungry:
            print("Feeding: " + self.name)
            self.is_hungry = False
            self.weight = self.weight + 1
        else:
            print(self.name + "is not hungry, thanks anyway.")

Then we create a cat object and add some toys.

cat = Animal(
    "Flufosourus",
    "(=^o.o^=)__",
    7,
)

cat.toys.append("catnip mouse")
cat.toys.append("cardboard box")
cat.toys.append("laser pointer")

print(cat.toys)
['catnip mouse', 'cardboard box', 'laser pointer']

What would toys contain on a new snake object?

You may be surprised to see that it has the same contents as the cat object. That is because all instances share the exact same object. Since a list is mutable, (unlike an integer or string), any instance can make changes that will apply to all instances of the same class.

snake = Animal(
    "Medusa",
    r"_/\__/\_/--{ :>~",
    2,
)

print(snake.toys)
['catnip mouse', 'cardboard box', 'laser pointer']

The same behavior is seen when using a mutable object for a default value.

This is because the default value is created when the method or function is defined, not when an object is instantiated.

class Animal:
    ears = 2

    def __init__(self, name, pic, weight, is_hungry=False, toys=[]):
        self.name = name
        self.pic = pic
        self.weight = weight
        self.is_hungry = is_hungry
        self.toys = toys

    def feed(self):
        if self.is_hungry:
            print("Feeding: " + self.name)
            self.is_hungry = False
            self.weight = self.weight + 1
        else:
            print(self.name + "is not hungry, thanks anyway.")

As a result, all instances that use the default value share the exact same object.

cat = Animal(
    "Flufosourus",
    "(=^o.o^=)__",
    7,
)

cat.toys.append("catnip mouse")
cat.toys.append("cardboard box")
cat.toys.append("laser pointer")

print(cat.toys)
['catnip mouse', 'cardboard box', 'laser pointer']
snake = Animal(
    "Medusa",
    r"_/\__/\_/--{ :>~",
    2,
)

print(snake.toys)
['catnip mouse', 'cardboard box', 'laser pointer']

The moral of the story is, if you want a mutable property that is different for each instance of a class, assign it in the __init__() function.

class Animal:
    ears = 2

    def __init__(self, name, pic, weight, is_hungry=False, toys=None):
        self.name = name
        self.pic = pic
        self.weight = weight
        self.is_hungry = is_hungry

        if not toys:
            toys = []

        self.toys = toys

    def feed(self):
        if self.is_hungry:
            print("Feeding: " + self.name)
            self.is_hungry = False
            self.weight = self.weight + 1
        else:
            print(self.name + "is not hungry, thanks anyway.")

This will ensure the mutable property is created when the object is instantiated so it will be different for each instance.

cat = Animal(
    "Flufosourus",
    "(=^o.o^=)__",
    7,
)

cat.toys.append("catnip mouse")
cat.toys.append("cardboard box")
cat.toys.append("laser pointer")

print(cat.toys)
['catnip mouse', 'cardboard box', 'laser pointer']
snake = Animal(
    "Medusa",
    r"_/\__/\_/--{ :>~",
    2,
)

print(snake.toys)
[]

Part 6.1: Exercise#

Exercise 100 (Mutable Gotchas)

  1. Add the class property features to your Car class and assign it to an empty list. Append some features to your car object. Print car.features and truck.features.

  2. Remove the features class property, and instead add it as an argument to __init__ with an empty list for a default value. Append some features to your car object. Print car.features and truck.features.

  3. Change the features default to None. In your __init__ function, check if features is falsy. If it is, set it to an empty list. Append some features to your car object. Print car.features and truck.features.

Reference#

Glossary#

class variable#
class property#
class attribute#

A variable that belongs to a class as well as all instances of that class.

instance variable#
instance property#
instance attribute#

A variable that belongs to one specific object, or instance.