Object Oriented Programming

Tips for navigating the slides:
  • Press O or Escape for overview mode.
  • Visit this link for a nice printable version
  • Press the copy icon on the upper right of code blocks to copy the code

Objects

What's an object?

An object bundles together information and related behavior.

Each list is an object. The information is the sequence of elements, and the behavior are methods (like append) plus slicing and bracket notation.


          lyrics = ['Ring', 'ding', 'ding', 'ding', 'dingeringeding!']
          lyrics[0]   # 'Ring'
          len(lyrics) # 5
          lyrics[0] = 'Ring'
          

Similarly, each string is an object. The information is the sequence of characters, and the behavior are methods (like split) plus slicing and bracket notation.


          what_fox_says = 'Hatee-hatee-hatee-ho'
          what_fox_says[0:5]       # Hatee
          what_fox_says.split('-') # ['Hatee', 'hatee', 'hatee', 'ho']
          

Lists and strings have similar behavior, but not the same.

Objects everywhere

In fact, every data type you've used so far is an object.

  • integers
  • floating point numbers
  • Booleans
  • lists
  • ranges
  • strings
  • dictionaries

Object types

Every object has a type, also known as its class.


          what_fox_says = 'Hatee-hatee-hatee-ho'
          type(what_fox_says)  # 'str'

          lyrics = ['Ring', 'ding', 'ding', 'ding', 'dingeringeding!']
          type(lyrics)        # 'list'
          
  • int: integers
  • float: floating point numbers
  • bool: Booleans
  • list: lists
  • range: ranges
  • str: strings
  • dict: dictionaries

Python includes a special syntax to create new classes.

Classes

Class terminology

  • A class is a template for defining new data types.
  • An object is a single instance of a particular class.
  • An object has associated data called instance variables.
  • An object has associated functions called methods.

A fully coded class and usage

The class definition:


            class Fox:
              species_name = 'Vulpes vulpes'

              def __init__(self, name):
                  self.name = name
                  self.happiness = 0

              def say(self):
                  return f'{self.name} says Wa-pa-pa-pa-pa-pow'

              def play(self, num_hours):
                  self.happiness += num_hours
            

Making a new objects of that class:


            pf = Fox('Pamela')
            pf.say()
            pf.play(2)
            

Let's break it down...

Object construction


            pf = Fox('Pamela')
            

The Fox() expression is the constructor for the class.

When the constructor is called:

  • A new instance of that class is created
  • The __init__ method of the class is called with the new object as its first argument (named self), along with any additional arguments provided in the call expression

            class Fox:

                def __init__(self, name):
                    self.name = name
                    self.happiness = 0
            

Instance variables

Instance variables are data attributes that describe the state of an object.

This __init__ initializes 2 instance variables:


            class Fox:

                def __init__(self, name):
                    self.name = name
                    self.happiness = 0
            

The class methods can then change the values of those variables or assign new variables.

Method invocation

This expression...


            pf.play(2)
            

...calls this function in the class definition:


            class Fox:
                def play(self, num_hours):
                    self.happiness += num_hours
            

pf.play is a bound method: a function which has its first parameter pre-bound to a particular value. In this case, self is pre-bound to pf and num_hours is set to 2.

It's equivalent to:


            Fox.play(pf, 2)
            

Dot notation

All object attributes (which includes variables and methods) can be accessed with dot notation:


            pf.play(2)
            

That evaluates to the value of the attribute looked up by play in the object referenced by pf.


The left-hand side of the dot notation can also be any expression that evaluates to an object reference:


            foxes = [pf, hf]
            foxes[0].play(2)
            

Class variables

A class variable is an assignment inside the class that isn't inside a method body.


          class Fox:
              species_name = 'Vulpes vulpes'
          

Class variables are "shared" across all instances of a class because they are attributes of the class, not the instance.


          pf = Fox('Pamela')
          hf = Fox('Hunter')

          pf.species_name   # 'Vulpes vulpes'
          hf.species_name   # 'Vulpes vulpes'
          Fox.species_name  # 'Vulpes vulpes'
          

All together now

The class definition:


            # Define a new type of data
            class Fox:
              # Set class variables
              species_name = 'Vulpes vulpes'

              # Set the initial values
              def __init__(self, name):
                  self.name = name
                  self.happiness = 0

              # Define methods
              def say(self):
                  return f'{self.name} says Wa-pa-pa-pa-pa-pow'

              def play(self, num_hours):
                  self.happiness += num_hours
            

Object instantiation and method invocation:


            pf = Fox('Pamela')
            pf.play(2)
            

Quiz: Objects + Classes

Multiple instances

There can be multiple instances of each class.


            pina_bar = Product("Piña Chocolotta", 7.99,
                ["200 calories", "24 g sugar"])

            cust1 = Customer("Coco Lover",
                ["123 Pining St", "Nibbsville", "OH"])

            cust2 = Customer("Nomandy Noms",
                ["34 Shlurpalot St", "Buttertown", "IN"])
            

What are the classes here? Product, Customer
How many instances of each? 1 Product, 2 Customer

Class vs. instance variables


            class Customer:

                salutation = "Dear"

                def __init__(self, name, address):
                    self.name = name
                    self.address = address

                def get_greeting(self):
                    return f"{self.salutation} {self.name},"

                def get_formatted_address(self):
                    return "\n".join(self.address)

            cust1 = Customer("Coco Lover",
                ["123 Pining St", "Nibbsville", "OH"])
            

What are the class variables? salutation
What are the instance variables? name, address

Inheritance

Building "Animal Conserving"

A game where we take care of cute furry/ferocious animals:

A bunch of pairs of animals

What should be the classes?

A bunch of pairs of animals

            Panda()
            Lion()
            Rabbit()
            Vulture()
            Elephant()
            Food()
            

A Food class

Let's start simple:


            class Food:

                def __init__(self, name, type, calories):
                    self.name = name
                    self.type = type
                    self.calories = calories
            

How would we use that class?


            broccoli = Food("Broccoli Rabe", "veggies", 20)
            bone_marrow = Food("Bone Marrow", "meat", 100)
            

An Elephant class


            class Elephant:
                species_name = "African Savanna Elephant"
                scientific_name = "Loxodonta africana"
                calories_needed = 8000

                def __init__(self, name, age=0):
                    self.name = name
                    self.age = age
                    self.calories_eaten  = 0
                    self.happiness = 0

                def play(self, num_hours):
                    self.happiness += (num_hours * 4)
                    print("WHEEE PLAY TIME!")

                def eat(self, food):
                    self.calories_eaten += food.calories
                    print(f"Om nom nom yummy {food.name}")
                    if self.calories_eaten > self.calories_needed:
                        self.happiness -= 1
                        print("Ugh so full")

                def interact_with(self, animal2):
                    self.happiness += 1
                    print(f"Yay happy fun time with {animal2.name}")
            

Object construction:


            el1 = Elephant("Willaby", 5)
            el2 = Elephant("Wallaby", 3)
            el1.play(2)
            el1.interact_with(el2)
            

A Rabbit class


            class Rabbit:
                species_name = "European rabbit"
                scientific_name = "Oryctolagus cuniculus"
                calories_needed = 200

                def __init__(self, name, age=0):
                    self.name = name
                    self.age = age
                    self.calories_eaten = 0
                    self.happiness = 0

                def play(self, num_hours):
                    self.happiness += (num_hours * 10)
                    print("WHEEE PLAY TIME!")

                def eat(self, food):
                    self.calories_eaten += food.calories
                    print(f"Om nom nom yummy {food.name}")
                    if self.calories_eaten > self.calories_needed:
                        self.happiness -= 1
                        print("Ugh so full")

                def interact_with(self, animal2):
                    self.happiness += 4
                    print(f"Yay happy fun time with {animal2.name}")
            

Object construction:


            rabbit1 = Rabbit("Mister Wabbit", 3)
            rabbit2 = Rabbit("Bugs Bunny", 2)
            rabbit1.eat(broccoli)
            rabbit2.interact_with(rabbit1)
            

Notice similarities?

Elephant Rabbit

                    # Class variables
                    species_name
                    scientific_name
                    calories_needed

                    # Instance variables
                    name
                    age
                    happiness

                    # Methods
                    eat(food)
                    play(num_hours)
                    interact_with(other)
                    

                    # Class variables
                    species_name
                    scientific_name
                    calories_needed

                    # Instance variables
                    name
                    age
                    happiness

                    # Methods
                    eat(food)
                    play(num_hours)
                    interact_with(other)
                    

Elephant and Rabbit are both animals, so they have similar attributes. Instead of repeating code, we can inherit the code.

Base classes and subclasses

When multiple classes share similar attributes, you can reduce redundant code by defining a base class and then subclasses can inherit from the base class.

Diagram of animal classes extending from Animal

Tip: The base class is also known as the superclass.

The base class

The base class contains method headers common to the subclasses, and code that is used by multiple subclasses.


            class Animal:
                species_name = "Animal"
                scientific_name = "Animalia"
                play_multiplier = 2
                interact_increment = 1

                def __init__(self, name, age=0):
                    self.name = name
                    self.age = age
                    self.calories_eaten  = 0
                    self.happiness = 0

                def play(self, num_hours):
                    self.happiness += (num_hours * self.play_multiplier)
                    print("WHEEE PLAY TIME!")

                def eat(self, food):
                    self.calories_eaten += food.calories
                    print(f"Om nom nom yummy {food.name}")
                    if self.calories_eaten > self.calories_needed:
                        self.happiness -= 1
                        print("Ugh so full")

                def interact_with(self, animal2):
                    self.happiness += self.interact_increment
                    print(f"Yay happy fun time with {animal2.name}")
            

The subclasses

To declare a subclass, put parentheses after the class name and specify the base class in the parentheses:


            class Panda(Animal):
            

Then the subclasses only need the code that's unique to them. They can redefine any aspect: class variables, method definitions, or constructor. A redefinition is called overriding.

The simplest subclass overrides nothing:


            class AmorphousBlob(Animal):
                pass
            

Overriding class variables

Subclasses can override existing class variables and assign new class variables:


            class Rabbit(Animal):
                species_name = "European rabbit"
                scientific_name = "Oryctolagus cuniculus"
                calories_needed = 200
                play_multiplier = 8
                interact_increment = 4
                num_in_litter = 12

            class Elephant(Animal):
                species_name = "African Savanna Elephant"
                scientific_name = "Loxodonta africana"
                calories_needed = 8000
                play_multiplier = 4
                interact_increment = 2
                num_tusks = 2
            

Overriding methods

If a subclass overrides a method, Python will use that definition instead of the superclass definition.


            class Panda(Animal):
                species_name = "Giant Panda"
                scientific_name = "Ailuropoda melanoleuca"
                calories_needed = 6000

                def interact_with(self, other):
                    print(f"I'm a Panda, I'm solitary, go away {other.name}!")
            

How would we call that method?


            panda1 = Panda("Pandeybear", 6)
            panda2 = Panda("Spot", 3)
            panda1.interact_with(panda2)