Lecture 21: Objects, Classes, and Inheritance

The structure of a class

Let's start with a very simple class called Person which holds some state (instance variables) and behaviors (methods).

All people have names and ages, so those are going to be our instance variables. It's also nice to have a method that will increment the age of a person on their birthday.

In [1]:
class Person:
    def __init__(self, name, age):
        """Constructor to initialize instance variables for name and age.
        Name is represented as 'First Last'"""
        self.age = age
        self.name = name
        
    def getFirstName(self):
        """Returns the person's first name"""
        return self.name.split()[0]
        
    def printGreeting(self):
        """Greets this person and tells them their last name"""
        print "Hello {name}. You are {age} years old.".format(name=self.name, age=self.age)
        
    def todayBirthday(self):
        """Increment a person's age and wish them happy birhtday"""
        self.age = self.age + 1
        print "Happy birthday", self.getFirstName() + "!"

Now let's create some Person objects by invoking the constructor.

In [2]:
taylor = Person("Taylor Swift", 26)
bruno = Person("Bruno Mars", 30)
serena = Person("Serena Williams", 34)
rafa = Person("Rafael Nadal", 29)

Here's how to invoke an object's behaviors.

In [3]:
rafa.printGreeting()
Hello Rafael Nadal. You are 29 years old.
In [4]:
serena.todayBirthday()
Happy birthday Serena!
In [5]:
serena.printGreeting()
Hello Serena Williams. You are 35 years old.

Displaying object information

We sometimes want to print out the current state of an object. Can I do the following?

In [6]:
print serena
<__main__.Person instance at 0x10c603cf8>

Not very informative, is it?

There's a special method called __repr__ that lets you specify a string representation of an object. We're going to add it to the Person class.

In [10]:
class Person:
    def __init__(self, name, age):
        """Constructor to initialize instance variables for name and age.
        Name is represented as 'First Last', and we'll store them separately"""
        self.age = age
        self.name = name
        
    def getFirstName(self):
        """Returns the person's first name"""
        return self.name.split()[0]
        
    def printGreeting(self):
        """Greets this person and tells them their last name"""
        print "Hello {name}. You are {age} years old.".format(name=self.name, age=self.age)
        
    def todayBirthday(self):
        """Says that today is this person's birthday, so increment their age and wish them"""
        self.age = self.age + 1
        print "Happy birthday", self.getFirstName() + "!"
        
    def __repr__(self):
        """String representation of object's state"""
        return "<Person - name: {n}; age: {a}>".format(n=self.name, a=self.age)
        # Alternative solution with string concatenation, not formatting
        #return '<Person - name: '+ self.name + '; age: '+ str(self.age)+'>'
 

NOTE: The reason for showing an example of the format method is that differently from string concatenation,
it doesn't require the user to convert values to string, it takes care of it as part of the string formatting.
It is also similar to the Jinja2 examples we saw in Lecture 20.

Now let's try defining the objects and printing them again.

In [11]:
taylor = Person("Taylor Swift", 26)
bruno = Person("Bruno Mars", 30)
serena = Person("Serena Williams", 34)
rafa = Person("Rafael Nadal", 29)
In [12]:
print taylor
<Person - name: Taylor Swift; age: 26>
In [13]:
str(taylor)
Out[13]:
'<Person - name: Taylor Swift; age: 26>'

Now it's Taylor's birthday!

In [14]:
taylor.todayBirthday()
Happy birthday Taylor!
In [15]:
print taylor
<Person - name: Taylor Swift; age: 27>
In [16]:
taylor
Out[16]:
<Person - name: Taylor Swift; age: 27>

Exercise 1: MutableString

Let's define a class named MutableString that behaves like Python's str class, except that it is mutable.

It should support the following behaviors that str has:

  1. getting a character at a given index
  2. getting the length of the string
  3. displaying the value of the string when printed

In addition, it should support the following mutable behaviors not in str:

  1. changing a character at a given index to a new value
  2. changing the value of the string to its reverse

Consider two different implementations:

  1. An instance of MutableString has a single instance variable named chars that holds a mutable list of the characters in the string.
  2. An instance of MutableString has a single instance variable named string that holds a string. (Even though strings are immutable, instance variables themselves are mutable, so it is possible to change the string over time.)

Solution 1

(use the variable chars that is a list of characters)

In [17]:
class MutableString:
    def __init__(self, data): 
        self.chars = list(data)
        
    def length(self): 
        "return the length of the MutableString object"
        return len(self.chars)

    def getval(self, index): 
        "return the character at index"
        return self.chars[index]

    def setval(self, index, value): 
        "change the value of the MutableString object at index to c"
        self.chars[index] = value

    def reverse(self): 
        "change the value of the MutableString object to its reverse"
        self.chars.reverse()

    def __repr__(self): 
        "specify how the MutableString object is displayed"
        return ''.join(self.chars)
    

Test this out below.

In [18]:
s = MutableString('deer')
print 's is:', s
s is: deer
In [19]:
print 's.length() is:', s.length()
s.length() is: 4
In [20]:
print 's.getval(3) is:', s.getval(3)
s.getval(3) is: r
In [21]:
s.reverse()
print 'after s.reverse(), s is:', s
after s.reverse(), s is: reed
In [22]:
s.setval(0, 's')
print "after s.setval(0, 's'), s is:", s
after s.setval(0, 's'), s is: seed
In [24]:
s
Out[24]:
seed

Solution 2

(use the variable string that hold the string)

In [25]:
class MutableString:
    def __init__(self, data):   
        self.string = data
        
    def length(self):   
        "return the length of the MutableString object"
        return len(self.string)
        
    def getval(self, index):
        "return the character at index"
        return self.string[index]
                
    def setval(self, index, c):   
        "change the value of the MutableString object at index to c"
        self.string = self.string[:index] + c + self.string[index+1:]
        
    def reverse(self): 
       "change the value of the MutableString object to its reverse"
       self.string = self.string[::-1]
        
    def __repr__(self): 
        "specifies how the MutableString object is displayed"
        return self.string
In [26]:
s = MutableString('deer')
print 's is:', s
s is: deer
In [27]:
print 's.length() is:', s.length()
s.length() is: 4
In [28]:
print 's.getval(3) is:', s.getval(3)
s.getval(3) is: r
In [29]:
s.reverse()
print 'after s.reverse(), s is:', s
after s.reverse(), s is: reed
In [30]:
s.setval(0, 's')
print "after s.setval(0, 's'), s is:", s
after s.setval(0, 's'), s is: seed
In [31]:
s
Out[31]:
seed

Inheritance

Just like the inheritance hierarchies of data types in Python, or classes in cs1graphics, we can define our own classes that inherit from others.

Here is a class called Animal. Animals generally make noises and walk.

In [32]:
class Animal:
    def __init__(self, animalName, animalSpecies, animalNoise): 
        self.name = animalName
        self.species = animalSpecies
        self.noise = animalNoise
        
    def makeNoise(self): 
        print 'This', self.species, 'named', self.name, 'says', self.noise
        
    def walk(self):
        print 'This', self.species, 'named', self.name, 'has gone for a walk'
In [33]:
snoopy = Animal('Snoopy', 'dog', 'woof')
garfield = Animal('Garfield', 'cat', 'miaow')
In [34]:
snoopy.makeNoise()
garfield.makeNoise()
snoopy.walk()
This dog named Snoopy says woof
This cat named Garfield says miaow
This dog named Snoopy has gone for a walk

And here is a class called Bird. Bird is just a specific type of animal, so it inherits all its states and behaviors from Animal.

Birds can fly, save for a few cases.

In [35]:
class Bird(Animal): # A Bird inherits all Animal methods plus can define extra ones

    def canFly(self):   # can this bird fly?
        return self.species not in ['penguin', 'ostrich', 'kiwi']
In [36]:
tuxedo = Bird('Tuxedo', 'penguin', 'honk')
flit = Bird('Flit', 'hummingbird', 'buzz')
In [37]:
flit.makeNoise()
This hummingbird named Flit says buzz
In [38]:
flit.walk()
This hummingbird named Flit has gone for a walk
In [39]:
print 'Can', tuxedo.name, 'fly?', tuxedo.canFly()
Can Tuxedo fly? False
In [40]:
print 'Can', flit.name, 'fly?', flit.canFly()
Can Flit fly? True

Overriding

We can define Fish that inherits from Animal as well.

But fish can't walk! So we shall over-ride the walk method of Animal. Note that we cannot delete the method, only change what it does.

In [41]:
class Fish(Animal): 
    def walk(self): # Override the walk method inherited from Animal 
        print 'Silly!', self.species, 'cannot walk.'
In [42]:
nemo = Fish('Nemo', 'clownfish', 'glub, gurgle')
nemo.makeNoise()
This clownfish named Nemo says glub, gurgle
In [43]:
nemo.walk()
Silly! clownfish cannot walk.

This does not change the walk method of Animal or Bird.

In [44]:
simba = Animal('Simba', 'lion', 'roar')
simba.makeNoise()
simba.walk()
This lion named Simba says roar
This lion named Simba has gone for a walk

Exercise 2: Define Insect

Define a new class Insect that inherits from Animal. In Insect override the definition of the method walk.

In [45]:
class Insect(Animal):
    def walk(self): 
        "Override the walk method inherited from Animal"
        if self.species.lower()=='butterfly':
            print 'This', self.species, 'named', self.name, 'flies in the air'
        else:
            print 'This', self.species, 'named', self.name, 'crawls'
    
In [46]:
monarch = Insect('Monarch', 'butterfly', 'click click')
monarch.walk()
simba.walk() # This doesn't change the walk method of Animal or Bird
This butterfly named Monarch flies in the air
This lion named Simba has gone for a walk

Using the inherited method when overriding it

What if we wanted our constructor to print a message that we created a Bird object?

To do this, we need to override the __init__ method for Bird. Since we want it to do the same thing as the Animal constructor, but with an additional printed message, we can just invoke the constructor of the Animal class.

In [47]:
class Bird(Animal):   
    def __init__(self, animalName, animalSpecies, animalNoise):
        Animal.__init__(self, animalName, animalSpecies, animalNoise)
        print 'You just created a Bird object for a', self.species
        
    def canFly(self):   # can this bird fly?
        return self.species not in ['penguin', 'ostrich', 'kiwi']
In [48]:
tweety = Bird('Tweety', 'canary', 'tweet')
You just created a Bird object for a canary
In [50]:
tweety.makeNoise()
This canary named Tweety says tweet
In [51]:
print 'Can', tweety.name, 'fly?', tweety.canFly()
Can Tweety fly? True

Multiple levels of inheritance

Some birds can talk as well as make noises.

In [52]:
class TalkingBird(Bird):
    def talk(self):
        print 'Arg. Polly want a cracker. Arg.'
In [53]:
sir_pecks_a_lot = TalkingBird('Sir-Pecks-A-Lot', 'parrot', 'squack')
You just created a Bird object for a parrot
In [54]:
sir_pecks_a_lot.makeNoise()
This parrot named Sir-Pecks-A-Lot says squack
In [55]:
sir_pecks_a_lot.walk()
This parrot named Sir-Pecks-A-Lot has gone for a walk
In [56]:
sir_pecks_a_lot.talk()
Arg. Polly want a cracker. Arg.

What will happen with the line below?

In [57]:
tweety.talk()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-57-fd9ec66c5321> in <module>()
----> 1 tweety.talk()

AttributeError: Bird instance has no attribute 'talk'

More examples of inheritance

In [58]:
class Human:
    def __init__(self, name):
        self.name = name
        print 'Hello, my name is', self.name
        
    def exhibitWeakness(self):
        print 'mortality'
        
class Vampire(Human):
    # vampires are humans but aren't mortal, so we should over-ride their weakness. 
    # just for fun, we'll also say that their weakness is not the same as a general human's
    def exhibitWeakness(self):
        print 'not',    # comma at end of print statement avoids a new line
        Human.exhibitWeakness(self)
        print 'but a stake through the heart!'
        
class Superhero(Human):
    # superheros are humans with more abilities
    def fly(self):
        print 'wheeee, I am saving the world!'
        
    # they are also not mortal
    def exhibitWeakness(self):
        print 'not', 
        Human.exhibitWeakness(self)
        print 'but Kryptonite.'
In [59]:
bieber = Human('Justin Bieber')
print 'My weakness is',
bieber.exhibitWeakness()
Hello, my name is Justin Bieber
My weakness is mortality
In [60]:
dracula = Vampire('Count Dracula')
print 'My weakness is',
dracula.exhibitWeakness()
Hello, my name is Count Dracula
My weakness is not mortality
but a stake through the heart!
In [61]:
kent = Superhero('Clark Kent')
kent.fly()
print 'My weakness is',
kent.exhibitWeakness()
Hello, my name is Clark Kent
wheeee, I am saving the world!
My weakness is not mortality
but Kryptonite.
In [62]:
bieber.fly()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-62-e6fc60b26e9a> in <module>()
----> 1 bieber.fly()

AttributeError: Human instance has no attribute 'fly'

Exercise 3: Class hierarchy

Define the following class hierarchy. What instance variable and methods should be common to all classes?

In [64]:
#Fill in your code for Shape
class Shape:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.description = "This shape is undefined"
        
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * self.width + 2 * self.height
    
    def describe(self, text):
        self.description=text
        
    def __repr__(self): 
        return "<{d} with width: {w}, and height: {h}>".format(d = self.description, 
                                                               w = self.width, 
                                                               h = self.height)
In [65]:
#Fill in your code for Rectangle
class Rectangle(Shape):
    def __init__(self,width,height):
        Shape.__init__(self,width,height)
        self.description = "This is a rectangle"
In [68]:
#Fill in your code for Triangle

#Fill in your code for Triangle
class Triangle(Shape):
    def __init__(self,base,side_a,side_c,height,):
        Shape.__init__(self,base,height)
        self.side_a=side_a
        self.side_c=side_c
        self.description = "This is a triangle"
        
    def area(self):
        return (self.width * self.height)/2.0
    
    def perimeter(self):
        return self.width + self.side_a + self.side_b
    
    def describe(self,text):
        self.description=text
        
    def __repr__(self): 
        return self.description + " with side a: " + str(self.side_a) + ", side b: " + str(self.width) + ", side c: " + str(self.side_c)
        # notice how much work you have to do with string concatenation
        # can you use the format method instead?
In [66]:
#Fill in your code for Circle
import math

class Circle(Shape):
    def __init__(self,radius):
        Shape.__init__(self,0,0)
        self.radius=radius
        self.description="This is a circle"
        
    def area(self):
        return math.pi*self.radius*self.radius
    
    def perimeter(self):
        return 2*math.pi*self.radius
    
    def describe(self,text):
        self.description=text
        
    def __repr__(self): 
        return self.description + " with a radius of: " + str(self.radius)
In [69]:
#Test your classes
s = Shape(20,30)
print s

r = Rectangle(20,30)
print r, ", area= " + str(r.area())

t = Triangle(12,10,10,8)
print t, ", area= " + str(t.area())

c = Circle(10)
print c, ", area= " + str(c.area())
<This shape is undefined with width: 20, and height: 30>
<This is a rectangle with width: 20, and height: 30> , area= 600
This is a triangle with side a: 10, side b: 12, side c: 10 , area= 48.0
This is a circle with a radius of: 10 , area= 314.159265359

Classes for geometric shapes with turtles

In [70]:
from turtle import *

class RegularPolygon:       
    """polygons where all sides have the same length"""
    def __init__(self, numSides, sideLen):
        self.numSides = numSides
        self.sideLen = sideLen
        self.angle = 360.0/numSides # store angle as instance variable, because it's useful
        
    def drawShape(self):
        for i in range(self.numSides):
            fd(self.sideLen)
            lt(self.angle)
        exitonclick()
            
    def getPerimeter(self):
        return self.sideLen * self.numSides
    
    def printInfo(self):
       print 'I have', self.numSides, 'sides, each with length', self.sideLen
       print 'My angle is', self.angle
       print 'My perimeter is', self.getPerimeter()
    
    def test(self):
        setup(600, 600, 0, 0)
        self.drawShape()
        self.printInfo()

class Square(RegularPolygon):
    def __init__(self, sideLen):
        RegularPolygon.__init__(self, 4, sideLen)    # invoke special case of polygon constructor
        
    def getArea(self):
        return self.sideLen * self.sideLen
    
    def printInfo(self):
        RegularPolygon.printInfo(self)
        print 'My area is', self.getArea()
        
class ColoredSquare(Square):
    def __init__(self, sideLen, color):
        Square.__init__(self, sideLen)
        self.color = color
        
    def drawShape(self):    # over-riding drawShape from Polygon.
        pencolor(self.color)
        Square.drawShape(self)
    
In [71]:
p = RegularPolygon(6, 100)
p.test()
I have 6 sides, each with length 100
My angle is 60.0
My perimeter is 600
In [72]:
s = Square(200)
s.test()
I have 4 sides, each with length 200
My angle is 90.0
My perimeter is 800
My area is 40000
In [73]:
c = ColoredSquare(200, 'red')
c.test()  # what version of drawShape will showInfo use? The one with or without the color?
I have 4 sides, each with length 200
My angle is 90.0
My perimeter is 800
My area is 40000
In [ ]: