Lecture 9

Object Oriented Programming

Monday, October 2nd 2017


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.

Thus we would 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 (API) and use it.

  • In a similar way, we want to encapsulate our data: we dont 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, lets think of tuples.


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

def real(c): # method
    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)
<ipython-input-5-4a58229b27de> in <module>()
----> 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=="str":
            return "{0}+{1}i".format(a, b)
    return dispatch

In [7]:
z=Complex2(1,2)
print(z("real"), "     ", z("imag"), "     ", z("str"))


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):
    in_a=a
    in_b=b
    def dispatch(message, value=None):
        nonlocal in_a, in_b
        if message=='set_real' and value != None:
            in_a = value
        elif message=='set_imag' and value != None:
            in_b = value
        elif message=="real":
            return in_a
        elif message=='imag':
            return in_b
        elif message=="str":
            return "{0}+{1}i".format(in_a, in_b)
    return dispatch

In [9]:
c3=Complex3(1,2)
print(c3("real"), "     ", c3("imag"), "     ", c3("str"))


1       2       1+2i

In [10]:
c3('set_real', 2)

In [11]:
print(c3("real"), "     ", c3("imag"), "     ", c3("str"))


2       2       2+2i

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('<iframe width="800" height="500" frameborder="0" src="http://pythontutor.com/iframe-embed.html#code=class%20ComplexClass%28%29%3A%0A%20%20%20%20%0A%20%20%20%20def%20__init__%28self,%20a,%20b%29%3A%0A%20%20%20%20%20%20%20%20self.real%20%3D%20a%0A%20%20%20%20%20%20%20%20self.imaginary%20%3D%20b%0A%0Ac1%20%3D%20ComplexClass%281,2%29&codeDivHeight=400&codeDivWidth=350&cumulative=false&curInstr=0&heapPrimitives=false&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe>')


Out[13]:

In [14]:
c1 = ComplexClass(1,2)
print(c1, c1.real)


<__main__.ComplexClass object at 0x10f2ee3c8> 1

In [15]:
print(vars(c1), "     ",type(c1))


{'real': 1, 'imaginary': 2}       <class '__main__.ComplexClass'>

In [16]:
c1.real=5.0
print(c1, "     ", c1.real, "     ", c1.imaginary)


<__main__.ComplexClass object at 0x10f2ee3c8>       5.0       2

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():
    
    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"
  • Animal is the superclass (a.k.a the base class).
  • Dog and Cat are both subclasses (a.k.a derived classes) of the Animal superclass.

Using the Animal class


In [18]:
a0 = Animal("David")
print(a0.name)
a0.make_sound()


David
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
<ipython-input-18-53384c99ce0d> in <module>()
      1 a0 = Animal("David")
      2 print(a0.name)
----> 3 a0.make_sound()

<ipython-input-17-e5cc3e7d7cbb> in make_sound(self)
      5 
      6     def make_sound(self):
----> 7         raise NotImplementedError
      8 
      9 class Dog(Animal):

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, "     ", Dog.make_sound)


<bound method Dog.make_sound of <__main__.Dog object at 0x10f3dbcf8>>       <function Dog.make_sound at 0x10f3ccf28>

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)
<ipython-input-22-11ebae4e7564> in <module>()
----> 1 Dog.make_sound()

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

How does this all work?


In [23]:
HTML('<iframe width="800" height="500" frameborder="0" src="http://pythontutor.com/iframe-embed.html#code=class%20Animal%28%29%3A%0A%20%20%20%20%0A%20%20%20%20def%20__init__%28self,%20name%29%3A%0A%20%20%20%20%20%20%20%20self.name%20%3D%20name%0A%20%20%20%20%20%20%20%20%0A%20%20%20%20def%20make_sound%28self%29%3A%0A%20%20%20%20%20%20%20%20raise%20NotImplementedError%0A%20%20%20%20%0Aclass%20Dog%28Animal%29%3A%0A%20%20%20%20%0A%20%20%20%20def%20make_sound%28self%29%3A%0A%20%20%20%20%20%20%20%20return%20%22Bark%22%0A%20%20%20%20%0Aclass%20Cat%28Animal%29%3A%0A%20%20%20%20%0A%20%20%20%20def%20__init__%28self,%20name%29%3A%0A%20%20%20%20%20%20%20%20self.name%20%3D%20%22A%20very%20interesting%20cat%3A%20%7B%7D%22.format%28name%29%0A%20%20%20%20%20%20%20%20%0A%20%20%20%20def%20make_sound%28self%29%3A%0A%20%20%20%20%20%20%20%20return%20%22Meow%22%0A%0Aa1%20%3D%20Dog%28%22Snoopy%22%29%0Aa2%20%3D%20Cat%28%22Hello%20Kitty%22%29%0Aanimals%20%3D%20%5Ba1,%20a2%5D%0Afor%20a%20in%20animals%3A%0A%20%20%20%20print%28a.name%29%0A%20%20%20%20print%28isinstance%28a,%20Animal%29%29%0A%20%20%20%20print%28a.make_sound%28%29%29%0A%20%20%20%20print%28\'--------\'%29&codeDivHeight=400&codeDivWidth=350&cumulative=false&curInstr=0&heapPrimitives=false&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe>')


Out[23]:

Calling a superclasses 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.
  • See https://rhettinger.wordpress.com/2011/05/26/super-considered-super/


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 %s as %s" % (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('<iframe width="800" height="500" frameborder="0" src="http://pythontutor.com/iframe-embed.html#code=class%20Animal%28%29%3A%0A%20%20%20%20%0A%20%20%20%20def%20__init__%28self,%20name%29%3A%0A%20%20%20%20%20%20%20%20self.name%3Dname%0A%20%20%20%20%20%20%20%20print%28%22Name%20is%22,%20self.name%29%0A%20%20%20%20%20%20%20%20%0Aclass%20Mouse%28Animal%29%3A%0A%20%20%20%20def%20__init__%28self,%20name%29%3A%0A%20%20%20%20%20%20%20%20self.animaltype%3D%22prey%22%0A%20%20%20%20%20%20%20%20super%28%29.__init__%28name%29%0A%20%20%20%20%20%20%20%20print%28%22Created%20%25s%20as%20%25s%22%20%25%20%28self.name,%20self.animaltype%29%29%0A%20%20%20%20%0Aclass%20Cat%28Animal%29%3A%0A%20%20%20%20pass%0A%0Aa1%20%3D%20Mouse%28%22Tom%22%29%0Aa2%20%3D%20Cat%28%22Jerry%22%29&codeDivHeight=400&codeDivWidth=350&cumulative=false&curInstr=0&heapPrimitives=false&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe>')


Out[25]:

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

The Python Data Model

Duck typing is used throughout Python. Indeed it's what enables the "Python Data Model"

  • All python classes implicitly inherit from the root object class.
  • The Pythonic way is to just document your interface and implement it.
  • This usage of common interfaces is pervasive in dunder functions to comprise the Python data model.

Example: Printing with __repr__ and __str__

  • The way printing works is that Python wants classes to implement __repr__ and __str__ methods.
  • It will use inheritance to give the built-in objects methods when these are not defined.
  • Any class can define __repr__ and __str__.
  • When an instance of such a class is interrogated with the repr or str function, then these underlying methods are called.

We'll see __repr__ here. If you define __repr__ you have made an object sensibly printable.

__repr__


In [27]:
class Animal():
    
    def __init__(self, name):
        self.name=name
        
    def __repr__(self):
        class_name = type(self).__name__
        return "{0!s}({1.name!r})".format(class_name, self)

In [28]:
r = Animal("David")
r


Out[28]:
Animal('David')

In [29]:
print(r)


Animal('David')

In [30]:
repr(r)


Out[30]:
"Animal('David')"