Lecture 6

Object Oriented Programming

Tuesday, September 24th 2019

Last time

  • Towards intermediate Python
    • Closures and decorators

Today

  • Towards object oriented programming
    • Objects
    • Classes
    • Inheritance and Polymorphism

General Python Guidelines

Now that you're starting to write some Python code, you should be aware of some useful guidelines and resources:

In [1]:
from IPython.display import HTML

Motiviation

We would like to find a way to represent complex, structured data in the context of our programming language.

For example, to represent a location, we might want to associate a name, a latitude and a longitude with it.

Want to create a compound data type which carries this information.

In C, for example, this is a struct:

struct location {
    float longitude;
    float latitude;
}

REMEMBER: A language has 3 parts:

  • expressions and statements: how to structure simple computations
  • means of combination: how to structure complex computations
  • means of abstraction: how to build complex units

Review

  • When we write a function, we give it some sensible name which can then be used by a "client" programmer. We don't care about how this function is implemented. We just want to know its signature (called the Application Progamming Interface, API) and use it.

Review

  • When we write a function, we give it some sensible name which can then be used by a "client" programmer. We don't care about how this function is implemented. We just want to know its signature (Application Progamming Interface, API) and use it.

  • In a similar way, we want to encapsulate our data: we don't want to know how it is stored and all that. We just want to be able to use it. This is one of the key ideas behind object oriented programming.

Review

  • When we write a function, we give it some sensible name which can then be used by a "client" programmer. We don't care about how this function is implemented. We just want to know its signature (Application Programming Interface, API) and use it.

  • In a similar way, we want to encapsulate our data: we don't want to know how it is stored and all that. We just want to be able to use it. This is one of the key ideas behind object oriented programming.

  • To do this, write constructors that make objects. We also write other functions that access or change data on the object. These functions are called the "methods" of the object, and are what the client programmer uses.

First Examples

Objects thru tuples: An object for complex numbers

How might we implement such objects? First, let's think of tuples.

In [2]:
# constructor
def Complex(a, b):
    return (a, b)

# methods
def real(c):
    return c[0]

def imag(c):
    return c[1]

def str_complex(c):
    return "{0}+{1}i".format(c[0], c[1])
In [3]:
c1 = Complex(1,2) # constructor
print(real(c1), "     ", str_complex(c1))
1       1+2i

But things aren't hidden so I can get through the interface:

In [4]:
c1[0]
Out[4]:
1

Because I used a tuple, and a tuple is immutable, I can't change this complex number once it's created.

In [5]:
c1[0] = 2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in 
----> 1 c1[0] = 2

TypeError: 'tuple' object does not support item assignment

Objects thru closures

Let's try an implementation that uses a closure to capture the value of arguments.

In [6]:
def Complex2(a, b): # constructor
    def dispatch(message): # capture a and b at constructor run time
        if message=="real":
            return a
        elif message=="imag":
            return b
        elif message=="both":
            return "{0}+{1}i".format(a, b)
    return dispatch
In [7]:
z = Complex2(1,2)
print(z("real"), "     ", z("imag"), "     ", z("both"))
1       2       1+2i

This looks pretty good so far.

The only problem is that we don't have a way to change the real and imaginary parts.

For this, we need to add things called setters.

Objects with Setters

In [8]:
def Complex3(a, b):
    def dispatch(message, value=None):
        nonlocal a, b
        if message=='set_real' and value != None:
            a = value
        elif message=='set_imag' and value != None:
            b = value
        elif message=="real":
            return a
        elif message=='imag':
            return b
        elif message=="both":
            return "{0}+{1}i".format(a, b)
    return dispatch
In [9]:
c3 = Complex3(1,2)
print(c3("real"), "     ", c3("imag"), "     ", c3("both"))
1       2       1+2i
def Complex3(a, b):
    def dispatch(message, value=None):
        nonlocal a, b
        if message=='set_real' and value != None:
            a = value
        elif message=='set_imag' and value != None:
            b = value
        elif message=="real":
            return a
        elif message=='imag':
            return b
        elif message=="both":
            return "{0}+{1}i".format(a, b)
    return dispatch
In [10]:
c3('set_real', 2)
In [11]:
print(c3("real"), "     ", c3("imag"), "     ", c3("both"))
2       2       2+2i

Objects with Setters

nonlocal

We used nonlocal in order to reset a name that was bound in a different scope.

You have a homework problem that uses the nonlocal keyword in which you are required to think through the mechanics of what is going on.

PEP 3104 is a great resource on the nonlocal keyword.

Python Classes and instance variables

We constructed an object system above. But Python comes with its own.

Classes allow us to define our own types in the Python type system.

In [12]:
class ComplexClass():
    
    def __init__(self, a, b):
        self.real = a
        self.imaginary = b

__init__ is a special method run automatically by Python: It is a constructor.

self is the instance of the object.

It acts like this in C++ but self is explicit.

In [13]:
HTML('')
Out[13]:
In [14]:
c1 = ComplexClass(1,2)
print(c1)
print(c1.real)
<__main__.ComplexClass object at 0x10ca584e0>
1
In [15]:
print(vars(c1), "     ",type(c1))
{'real': 1, 'imaginary': 2}       
In [16]:
c1.real = 5.0
print(c1, "     ", c1.real, "     ", c1.imaginary)
<__main__.ComplexClass object at 0x10ca584e0>       5.0       2

Recap

At this point, you know how to create a simple class with Python.

We need to go beyond this.

Inheritance and Polymorphism

Inheritance

Inheritance is the idea that a "Cat" is-a "Animal" and a "Dog" is-a "Animal".

Animals make sounds, but Cats Meow and Dogs Bark.

Inheritance makes sure that methods not defined in a child are found and used from a parent.

Polymorphism

Polymorphism is the idea that an interface is specified, but not necessarily implemented, by a superclass and then the interface is implemented in subclasses (differently).

[Actually Polymorphism is much more complex and interesting than this, and this definition is really an outcome of polymorphism. But we'll come to this later.]

Example: Super- and subclasses

In [17]:
class Animal(): # Animal is the superclass (a.k.a the base class).
    
    def __init__(self, name):
        self.name = name
        
    def make_sound(self):
        raise NotImplementedError

# Dog and Cat are both subclasses 
# (a.k.a derived classes) of the Animal superclass.
class Dog(Animal):
    
    def make_sound(self):
        return "Bark"
    
class Cat(Animal):
    
    def __init__(self, name):
        self.name = "A very interesting cat: {}".format(name)
        
    def make_sound(self):
        return "Meow"

Using the Animal class

In [18]:
a0 = Animal("David")
print(a0.name)
a0.make_sound()
David
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
 in 
      1 a0 = Animal("David")
      2 print(a0.name)
----> 3 a0.make_sound()

 in make_sound(self)
      5 
      6     def make_sound(self):
----> 7         raise NotImplementedError
      8 
      9 # Dog and Cat are both subclasses

NotImplementedError: 
In [19]:
a1 = Dog("Snoopy")
a2 = Cat("Hello Kitty")
animals = [a1, a2]
for a in animals:
    print(a.name)
    print(isinstance(a, Animal))
    print(a.make_sound())
    print('--------')
Snoopy
True
Bark
--------
A very interesting cat: Hello Kitty
True
Meow
--------
In [20]:
print(a1.make_sound)
print(Dog.make_sound)
>

In [21]:
print(a1.make_sound())
print('----')
print(Dog.make_sound(a1))
Bark
----
Bark
In [22]:
Dog.make_sound()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in 
----> 1 Dog.make_sound()

TypeError: make_sound() missing 1 required positional argument: 'self'

How does this all work?

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

    def make_sound(self):
        raise NotImplementedError

class Dog(Animal):
    def make_sound(self):
        return "Bark"

class Cat(Animal):

    def __init__(self, name):
         self.name = "A very interesting cat: {}".format(name)

    def make_sound(self):
         return "Meow"

a1 = Dog("Snoopy")
a2 = Cat("Hello Kitty")
animals = [a1, a2]
for a in animals:
    print(a.name)
    print(isinstance(a, Animal))
    print(a.make_sound())
    print('--------')
In [23]:
HTML('')
Out[23]:

Calling a superclass initializer

  • Say we don't want to do all the work of setting the name variable in the subclasses.

Calling a superclass initializer

  • Say we dont want to do all the work of setting the name variable in the subclasses.

  • We can set this "common" work up in the superclass and use super to call the superclass's initializer from the subclass.

Calling a superclass initializer

  • Say we dont want to do all the work of setting the name variable in the subclasses.

  • We can set this "common" work up in the superclass and use super to call the superclass's initializer from the subclass.

  • There's another way to think about this:

    • A subclass method will be called instead of a superclass method if the method is in both the sub- and superclass and we call the subclass (polymorphism!).
    • If we really want the superclass method, then we can use the super built-in function.
In [24]:
class Animal():
    def __init__(self, name):
        self.name = name
        print("Name is", self.name)
        
class Mouse(Animal):
    def __init__(self, name):
        self.animaltype = "prey"
        super().__init__(name)
        print("Created {0} as {1}".format(self.name, self.animaltype))


class Cat(Animal):
    pass

a1 = Mouse("Tom")
print(vars(a1))
a2 = Cat("Jerry")
print(vars(a2))
Name is Tom
Created Tom as prey
{'animaltype': 'prey', 'name': 'Tom'}
Name is Jerry
{'name': 'Jerry'}
In [25]:
HTML('')
Out[25]:

Recap

What you've learned to this point:

  • Python classes
  • Instance methods
  • Superclasses & subclasses and inheritance & polymorphism
  • Superclass initializers

Interfaces

  • The above examples show inheritance and polymorphism.
  • Notice that we didn't actually need to set up the inheritance.
  • We could have just defined 2 different classes and have them both make_sound.
  • In Java and C++ this is done more formally through Interfaces and Abstract Base Classes, respectively, plus inheritance.
  • In Python, this agreement to define make_sound is called duck typing.
    • "If it walks like a duck and quacks like a duck, it is a duck."
In [26]:
# Both implement the "Animal" Protocol, which consists of the one make_sound function
class Dog():
    
    def make_sound(self):
        return "Bark"
    
class Cat():
    
    def make_sound(self):
        return "Meow"  
    
a1 = Dog()
a2 = Cat()
animals = [a1, a2]
for a in animals:
    print(isinstance(a, Animal), "     ", a.make_sound())
False       Bark
False       Meow

A Few Comments on Duck Typing

  • When using duck typing, you are specifying an implicit interface.
  • This is possible in dynamically typed languages (but also in some statically typed languages like C++ [thru templates]).
  • Duck typing can speed up the short-term development process.
  • But beware! It can hinder long-term progress, at least in part due to the specification of an implicit interface.
    • "Can anyone remember what we were thinking here?"
  • When using duck typing, be sure to include extensive tests.