2020-CS107 / AC207 / CSCI E-207

  • Syllabus
  • Schedule
  • Course Flow
  • Resources
  • Materials
  • Project

Key Word(s): Object oriented programming, Classes

Download Notebook


Lecture 6¶

Object Oriented Programming¶

Thursday, September 24th 2020¶

Last time¶

  • Towards intermediate Python
    • Closures and decorators

Today¶

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

closure_illustration

Back to Breakout Rooms!¶

  • Figure out who has the median height in your group. They will be the speaker.
  • Think of another useful decorator. Please don't look up common decorators; I want you to think of this on your own.
  • In broad strokes, how would you implement this decorator?

General Python Guidelines¶

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

  • Python Enchancement Proposals(PEPs)
  • PEP 8 - Python Style Guide
  • PEP 20- The Zen of Python

You can show off your PEP8 compliance on Github with a nice badge: PEP8 Badge

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;
}

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.
  • 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)
<ipython-input-5-87998c7e6ab1> 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=="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('<iframe width="1600" 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>')
/Users/dsondak/opt/anaconda3/lib/python3.7/site-packages/IPython/core/display.py:694: UserWarning: Consider using IPython.display.IFrame instead
  warnings.warn("Consider using IPython.display.IFrame instead")
Out[13]:
In [14]:
c1 = ComplexClass(1,2)
print(c1)
print(c1.real)
<__main__.ComplexClass object at 0x7f9c6dcb3750>
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 0x7f9c6dcb3750>       5.0       2
  • Python built-ins
  • vars
  • type

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)
<ipython-input-18-220932a3ec32> in <module>
      1 a0 = Animal("David")
      2 print(a0.name)
----> 3 a0.make_sound()

<ipython-input-17-204798d58b10> 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)
<bound method Dog.make_sound of <__main__.Dog object at 0x7f9c6dcbdc10>>
<function Dog.make_sound at 0x7f9c6dcabe60>
In [21]:
print(a1.make_sound())
print('----')
print(Dog.make_sound(a1))
Bark
----
Bark
In [24]:
Dog.make_sound()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-24-906531fce15c> in <module>
----> 1 Dog.make_sound()

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

Summary of different ways of calling¶

  • a1.make_sound - This is just the bound function.
    • a1 is an instance of the Dog class.
    • make_sound is an attribute of the class.
    • make_sound is accessed via the instance.
    • A bound method needs self as its first argument.
  • Dog.make_sound - This is just a regular function.
    • Dog is the class itself, not an instance of the class.
  • a1.make_sound() - Call the bound method of instance a1.
  • Dog.make_sound(a1) - Call make_sound directly from the class by passing in an instance.
    • In order for make_sound to work, it needs to work on an instance.
    • Dog is not an instance, so we need to pass an instance to this function for it to work.
  • Dog.make_sound() - Won't work
    • The function isn't bound to anything.
    • It expects an instance.

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('--------')

Cats and Dogs Example

In [ ]:
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>')

Calling a superclass initializer¶

  • Say we don't 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 [ ]:
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))

Tom and Jerry

In [ ]:
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>')

Recap¶

What you've learned to this point:

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

Interfaces¶

  • The examples so far 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 [ ]:
# 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())

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.
Copyright 2018 © Institute for Applied Computational Science