Key Word(s): Object oriented programming, Classes
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:
You can show off your PEP8 compliance on Github with a nice badge: PEP8 Badge
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.
# 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])
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:
c1[0]
1
Because I used a tuple, and a tuple is immutable, I can't change this complex number once it's created.
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.
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
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¶
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
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
c3('set_real', 2)
print(c3("real"), " ", c3("imag"), " ", c3("both"))
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.
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.
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")
c1 = ComplexClass(1,2)
print(c1)
print(c1.real)
<__main__.ComplexClass object at 0x7f9c6dcb3750> 1
print(vars(c1), " ",type(c1))
{'real': 1, 'imaginary': 2} <class '__main__.ComplexClass'>
c1.real = 5.0
print(c1, " ", c1.real, " ", c1.imaginary)
<__main__.ComplexClass object at 0x7f9c6dcb3750> 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¶
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¶
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:
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 --------
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>
print(a1.make_sound())
print('----')
print(Dog.make_sound(a1))
Bark ---- Bark
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 theDog
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 instancea1
.
Dog.make_sound(a1)
- Callmake_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.
- In order for
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('--------')
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.
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))
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
andC++
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."
# 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.