2020-CS107 / AC207 / CSCI E-207

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

Key Word(s): Inheritance, super(), duck typing, Dunder methods, Class methods, Static methods, Instance methods

Download Notebook


Lecture 7¶

Object Oriented Programming III¶

Tuesday, September 29th 2020¶

Last Time¶

  • Classes
  • Inheritance

Today¶

  • Super class initializers
  • Interfaces
  • Special methods (the dunder methods)
  • The Python Data Model
  • Class methods, static methods, instance methods <-- If time

https://www.youtube.com/watch?v=HTLu2DFOdTg&ab_channel=NextDayVideo

In [ ]:
from IPython.display import HTML

Inheritance Example¶

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 [1]:
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'}

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 [2]:
# 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.

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.
    • Python is a consenting adults language.
  • This usage of common interfaces is pervasive in dunder functions to comprise the Python data model.
    • What are these dunder methods?
    • What is the Python data model?
    • What are common interfaces?

Dunder Methods via Example¶

Example: Printing with __repr__ and __str__¶

  • The way printing works is that Python wants classes to implement __repr__ and __str__ methods.
    • This is a documented interface.
  • 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 [7]:
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 [8]:
r = Animal("David")
r
Out[8]:
Animal('David')
In [9]:
print(r)
Animal('David')
In [10]:
repr(r)
Out[10]:
"Animal('David')"
  • The return value of __repr__ is in quotes. Why?
  • The expression returned by __repr__ should be able to be fed into the eval built-in.
    • eval accepts a Python expression as a string.
    • The Python expression is then evaluated.
    • Convenient for debugging!
  • __repr__ returns the Python code needed to rebuild our object.
In [11]:
eval(repr(r))
Out[11]:
Animal('David')

Now we see how r was created!

Notes¶

  • There can be confusion about the difference between __repr__ and __str__.
  • Here is a great Stackoverflow discussing this issue: Difference between __str__ and __repr__?.
  • Use __repr__ to show how an object is created --- this is useful for developers.
  • Use __str__ to describe the object --- this is useful for users.
  • Note: print() first looks for __str__ and if that's not found it looks for __repr__.

The pattern with dunder methods¶

There are functions without double-underscores that cause the methods with the double-underscores to be called.

Thus repr(an_object) will cause an_object.__repr__() to be called.

In user-level code, you SHOULD NEVER see the latter. In library level code, you might see the latter. The definition of the class is considered library level code.

Example¶

Instance Equality via __eq__¶

We can now ask and answer the question: What makes two objects equal?

To do this, we will add a new dunder method to the mix, the unimaginatively named (that's a good thing) __eq__.

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

    def __eq__(self, other):
        return self.name == other.name # two animals are equal if their names are equal
In [13]:
A = Animal("Tom")
B = Animal("Jane")
C = Animal("Tom")

There are three separate object identities, but we made two of them equal!

In [14]:
print(id(A), "   ", id(B), "   ", id(C))

print(A==B, "         ", B==C, "         ", A==C)
140513643763664     140513643763600     140513643763728
False           False           True

This is critical because it gives us a say in what equality means.

Python's power comes from the data model, composition, and delegation¶

The data model is used (from Fluent Python) to provide a:

description of the interfaces of the building blocks of the language itself, such as sequences, iterators, functions, classes....

The special "dunder" methods we talk about are invoked by the Python interpreter to perform basic operations.

For example, __getitem__ gets an item in a sequence. This is used to do something like a[3].

__len__ is used to say how long a sequence is. Its invoked by the len built-in function.

A sequence, for example, must implement __len__ and __getitem__. That's it.

  • Note: A sequence has a specific meaning here and it's meaning is something that implements __len__ and __getitem__

The original reference for this data model is: https://docs.python.org/3/reference/datamodel.html.

Tuple¶

An example of a sequence in Python is the tuple. Since a tuple is a sequence, it must support indexing and be able to tell us its length.

In [15]:
a = (1,2)
a[0] # indexing
Out[15]:
1
In [16]:
len(a) # length
Out[16]:
2

Great. That worked out nicely. Let's take a look at some "enhanced" tuples.

NamedTuples¶

collections.namedtuple¶

  • Produces subclasses of tuples
  • The tuples are enhanced with field names and a class name.

Consider the example from Fluent Python (Example 1-1):

In [17]:
import collections
Card = collections.namedtuple('Cards', ['rank', 'suit'])
repr(Card)
Out[17]:
"<class '__main__.Cards'>"
In [18]:
my_card = Card('3', 'diamonds')
print(my_card)
print(type(my_card))
print(my_card.rank)
Cards(rank='3', suit='diamonds')
<class '__main__.Cards'>
3

A Custom Sequence¶

Let's create a FrenchDeck as an example of something that follows Python's Sequence protocol. Remember, the sequence protocol requires implementation of two methods: __len__ and __getitem__. That's it.

In [ ]:
[Card(rank, suit) for suit in "spade diamond club heart".split() for rank in [str(n) for n in range(2,11)] + list('JKQA')]
In [19]:
class FrenchDeck:
    ranks = [str(n) for n in range(2,11)] + list('JKQA')
    suits = "spade diamond club heart".split()

    def __init__(self):
        # composition: there are items IN this class that constitute its structure
        # delegation: the storage for this class is DELEGATED to this list below
        self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]
        
    def __len__(self):
        return len(self._cards)
    
    def __getitem__(self, position):
        return self._cards[position]
In [20]:
deck = FrenchDeck()
len(deck)
Out[20]:
52
In [21]:
deck[0], deck[-1], deck[3]
Out[21]:
(Cards(rank='2', suit='spade'),
 Cards(rank='A', suit='heart'),
 Cards(rank='5', suit='spade'))
In [22]:
deck[10:15]
Out[22]:
[Cards(rank='K', suit='spade'),
 Cards(rank='Q', suit='spade'),
 Cards(rank='A', suit='spade'),
 Cards(rank='2', suit='diamond'),
 Cards(rank='3', suit='diamond')]
  • The FrenchDeck class supports the sequence protocol
  • As a result, we can use functions like random.choice directly on instances of FrenchDeck.
  • This is the power of interfaces and the data model.
In [23]:
from random import choice
choice(deck)
Out[23]:
Cards(rank='K', suit='diamond')

Breakout Room¶

  • Figure out your favorite color. The speaker for the room is the person whose color is first alphabetically.
  • Create your own sequence!
    • No need to do any coding here, but pseudo-code is okay.
  • Describe in words how your sequence could be implemented.

Building out our class: instances and classmethods¶

At this point, you should feel comfortable with classes, special methods, and the python data model.

We will take a short excursion to enhance our classes using classmethods. We will also see staticmethods and regular instance methods.

A Favorite Example¶

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

    @classmethod
    def make_complex(cls, real, imaginary):
        return cls(real, imaginary)

    def __repr__(self):
        class_name = type(self).__name__
        return "%s(real=%r, imaginary=%r)" % (class_name, self.real, self.imaginary)

    def __eq__(self, other):
        return (self.real == other.real) and (self.imaginary == other.imaginary)
In [25]:
c1 = ComplexClass(1,2)
c1
Out[25]:
ComplexClass(real=1, imaginary=2)

make_complex is a class method. See how its signature is different above. It is a factory to produce instances.

class ComplexClass():
    def __init__(self, real, imaginary):
        self.real = real
        self.imaginary = imaginary

    @classmethod
    def make_complex(cls, real, imaginary):
        return cls(real, imaginary)

    def __repr__(self):
        class_name = type(self).__name__
        return "%s(real=%r, imaginary=%r)" % (class_name, self.real, self.imaginary)

    def __eq__(self, other):
        return (self.real == other.real) and (self.imaginary == other.imaginary)
In [26]:
c2 = ComplexClass.make_complex(1,2)
c2
Out[26]:
ComplexClass(real=1, imaginary=2)
In [27]:
c1 == c2
Out[27]:
True

The take-away¶

  • A classmethod has access to the actual class, but not the instance of the class.
  • Use it to provide alternative ways of constructing an object of your class.
    • The original client only needed to create complex numbers from the real and imaginary parts.
    • A new client needs to create complex numbers from the polar form (radius and angle).
    • Provide a classmethod that allows users to construct complex number objects from the polar form!
In [28]:
import numpy as np

class ComplexClass():
    def __init__(self, real, imaginary):
        self.real = real
        self.imaginary = imaginary

    @classmethod
    def from_polar(cls, r, theta):
        real = r * np.cos(theta)
        imaginary = r * np.sin(theta)
        return cls(real, imaginary)

    def __repr__(self):
        class_name = type(self).__name__
        return "%s(real=%r, imaginary=%r)" % (class_name, self.real, self.imaginary)

    def __eq__(self, other):
        return (self.real == other.real) and (self.imaginary == other.imaginary)

Breakout Room¶

  • Recall your favorite number. The speaker for this room will be the person with the largest favorite number.
  • Think about some situations where you might need to create a classmethod.

Static Methods, Class Methods, Instance Methods¶

What's really going on under the hood here?

In [ ]:
# From fluent python
class Demo():
    @classmethod
    def class_method(*args): # Class methods do not have to return an instance of the class
        return args
    
    @staticmethod
    def static_method(*args): # This is just a regular function
        return args
    
    def instance_method(*args): # This is a true blue instance method
        return args
In [ ]:
sm = Demo.static_method(1,2)
print(type(sm))
sm
In [ ]:
# From fluent python
class Demo():
    @classmethod
    def class_method(*args): # Class methods do not have to return an instance of the class
        return args

    @staticmethod
    def static_method(*args): # This is just a regular function
        return args
    
    def instance_method(*args): # This is a true blue instance method
        return args
In [ ]:
cm = Demo.class_method(1,2)
print(type(cm))
cm
In [ ]:
ademo = Demo()
Demo.instance_method(ademo, 1,2)
In [ ]:
ademo.instance_method(1,2)
In [ ]:
cm = ademo.class_method(1,2)
sm = ademo.static_method(1,2)
im = ademo.instance_method(1,2)
In [ ]:
print(type(cm), type(sm), type(im))
In [ ]:
cm
In [ ]:
sm
In [ ]:
im

PythonTutor Example

Class variables and instance variables¶

In [ ]:
class Demo2():
    classvar = 1
In [ ]:
ademo2 = Demo2()
print(Demo2.classvar, ademo2.classvar)
In [ ]:
ademo2.classvar = 2 # Different from the classvar above
print(Demo2.classvar, ademo2.classvar)

PythonTutor Example

Class variables are shared between all instances of the class.

Copyright 2018 © Institute for Applied Computational Science