Let's now build upon the Abstraction and Encapsulation mindset from last session with Inheritance and Polymorphism.
The class instances we create can interact by taking another instance/object as input to a method call. Let's illustrate this by a Point
class example where the distance (Euclidean) between point objects can be determined:
import math
class Point:
''' Defines a 2D 'Point' class that can compute distance between points'''
def __init__(self, x, y):
self._x = x
self._y = y
def distance_to(self, other):
'''Return the distance to another point instance.'''
x0, y0 = self._x, self._y
x1, y1 = other._x, other._y
return self.distance(x0, y0, x1, y1)
@staticmethod
def distance(x0, y0, x1, y1):
return math.sqrt( (x1-x0)**2 + (y1-y0)**2 )
def __repr__(self):
# unambiguous/official description for debugging/development
return f'{self.__class__.__name__}({self._x}, {self._y})'
def __str__(self):
# readable/informal description
return f'({self._x}, {self._y})'
Initiation of instances for Origo and two points:
origo = Point(0, 0)
p1 = Point(1, 1)
p2 = Point(3, 0)
Evaluate distances between the various Point instances:
p1.distance_to(p2)
2.23606797749979
p2.distance_to(origo)
3.0
Because of the two dunder methods __str__
and __repr__
a much cleaner representation is obtained instead of the default __main__.Point object at 0x0000022EC29BADC8
:
print(p2)
print(str(p2)) # str(p2) = p2.__str__
print(repr(p2)) # repr(p2) = p2.__repr__
p2
(3, 0) (3, 0) Point(3, 0)
Point(3, 0)
Some other examples of useful string representations:
Examples | str | repr |
---|---|---|
our point | (3, 0) | Point(3, 0) |
timestamp | 2016-02-22 19:32:04 | datetime.datetime(2016, 2, 22, 19, 32, 4) |
complex number | 10 + i20 | Rational(10, 20) |
QUESTION:
Is the len()
function actually an intelligent function that automatically detects what data type the input consists of before it decides what to do?
# it can find the number of charactors in a string
len('my string')
9
# it can find the length of a list
len([x for x in range(5)])
5
# it can find the number of items in a dictionary
len({'key1':'value1', 'key2':'value2'})
2
The length len()
function call is actually a hidden object method call.
'my string'.__len__()
9
[x for x in range(5)].__len__()
5
{'key1':'value1', 'key2':'value2'}.__len__()
2
This is refered to as dunder or magic methods. Note that double underscore is pronaunced "dunder". Other examples are: abs, str, init, sizeof, iter, dir...
Let's make our points more intelligent with build-in support for classical operators '+', '-', '*', '/'. This can be done by incorporating the corresponding dunder methods 'add', 'sub', 'mul', 'div' into our class:
class Point:
''' Defines a 2D 'Point' class that can compute distance between points'''
def __init__(self, x, y):
self._x = x
self._y = y
def distance_to(self, other):
'''Return the distance to another point instance.'''
x0, y0 = self._x, self._y
x1, y1 = other._x, other._y
return self.distance(x0, y0, x1, y1)
@staticmethod
def distance(x0, y0, x1, y1):
return math.sqrt( (x1-x0)**2 + (y1-y0)**2 )
def __repr__(self):
return f'{self.__class__.__name__}({self._x}, {self._y})'
def __add__(self, other):
x = self._x + other._x
y = self._y + other._y
return Point(x, y)
def __sub__(self, other):
x = self._x - other._x
y = self._y - other._y
return Point(x, y)
def __mul__(self, other):
# Check if the other object is an instance of class `int` or `float`
if isinstance(other, (int, float)):
# Perform multiplication and return
x = self._x * other
y = self._y * other
return Point(x, y)
else:
return "I don't know how to do multiply two points"
def __div__(self, other):
return "I don't know how to do divide two points"
First we re-create our point instances from this new class definition:
origo = Point(0, 0)
p1 = Point(1, 1)
p2 = Point(3, 0)
Let's see what happens when we apply operations:
p1 + p2 # equivalent to p1.__add__(p2)
Point(4, 1)
origo - p1 # equivalent to origo.__sub__(p1)
Point(-1, -1)
p1 * 5 # equivalent to p1.__mul__(5)
Point(5, 5)
p1 * p2 # equivalent to p1.__mul__(p2)
"I don't know how to do multiply two points"
So even just adding two numbers in Python involves OOP.
It wasn't that nice that we had to copy all the class definition code above when we wanted to add additional functionally to our class. This can be avoided by using inheritance to polymorph an existing class, so you don't have to start from scratch everytime you define a Class.
Here we define a new class named NewPoint (could also overwrite Point if we wanted to). Notice how we specify where to inherit from by specifiying a *parent* class as the input to the class definition. The new *child* class will inherit all methods etc. from it's parent.
Note that it's also possible to inherit from multiple parents.
class NewPoint(Point):
'''NewPoint class which inherits from the `Point` class.'''
def __pow__(self, other):
return "I don't know how to do point to the power of something"
# Define a point with the new child class
p3 = NewPoint(4, 4)
The NewPoint
class has inherited from the Point
class. Thus, all methods from Point
are now available in NewPoint
.
We can see this e.g. by printing the class instance. Recall that we wrote the __repr__
method in the Point
to create a better printed message than <__main__.{class_name} object at 0x00000284A08C0848>
.
print(p3)
NewPoint(4, 4)
The new method in NewPoint
is added "on top":
p3**origo
"I don't know how to do point to the power of something"
We could also use inheritance for creating a new Point3D
class. Notice how we use super()
to refer to the parent's __init__
method so we don't have to repeat any code. You just redefine any methods that you want to overwrite (known as *Method Overriding*).
class Point3D(NewPoint):
def __init__(self, x, y, z):
super().__init__(x, y)
self._z = z
def distance_to(self, other):
x0, y0, z0 = self._x, self._y, self._z
x1, y1, z1 = other._x, other._y, other._z
return self.distance(x0, y0, z0, x1, y1, z1)
@staticmethod
def distance(x0, y0, z0, x1, y1, z1):
return math.sqrt( (x1-x0)**2 + (y1-y0)**2 + (z1-z0)**2 )
p1 = Point3D(1, 1, 1)
p2 = Point3D(2, 2, 2)
p1.distance_to(p2)
1.7320508075688772
We're not limited to only work on our own classes. Let's e.g. define a new MyFloat
class where for once negative times negative doesn't yield positive.
class MyFloat(float):
def __mul__(self, other):
if self.real < 0 and other.real < 0:
return -self.real*other.real
else:
return self.real*other.real
# normal float behaviour
f1 = -2.
f2 = -4.
f1*f2 # equivalent to calling f1.__mul__(f2)
8.0
# new MyFloat behaviour
f1 = MyFloat(-2.)
f2 = MyFloat(-4.)
f1*f2
-8.0
# negative divide with negative to yields positive as we didn't overwrite that method
f1 / f2
0.5
Beside this sort of "flat" usage of inheritance, there's also a vertical/hierarchical approach.
If you for example have written two useful classes but suddenly see the need to duplicate some of the code in-between the two, because they both need some of the same functionality, it's properly because they should share a parent. Whatever they have in common should be defined in the shared parent (never duplicate code!).
This type of inheritance should generally be limited to a depth of maximum 2-3 levels to avoid a program design that is hard to follow.
In the following example we have a Mammal
class (where instances are not allowed) with two children classes (Person
and Dog
) and finally with two grandchildren classes (Student
and Teacher
).
# Abstract Base Classes are special classes that cannot generate instances
from abc import ABC, abstractmethod
class Mammal(ABC):
characteristics = 'nourished with milk from mother as young' # class variable
@abstractmethod # This method must be overriden before instances are allowed
def says(self):
pass
mammal = Mammal()
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-29-2774676c00aa> in <module> ----> 1 mammal = Mammal() TypeError: Can't instantiate abstract class Mammal with abstract methods says
class Person(Mammal):
def __init__(self, fname, lname):
self.fname = fname
self.lname = lname
@property
def full_name(self):
return f'{self.fname} {self.lname}'
def says(self):
return f'{self.full_name} says hello!'
me = Person('Kenneth', 'Kleissl')
print(me.says())
print(me.characteristics)
Kenneth Kleissl says hello! nourished with milk from mother as young
class Dog(Mammal):
def __init__(self, name):
self.name = name
def says(self):
return f'{self.name} says Woof!'
dog = Dog('Max')
print(dog.says())
print(dog.characteristics)
Max says Woof! nourished with milk from mother as young
class Student(Person):
def __init__(self, fname, lname, student_no):
super().__init__(fname, lname)
self.student_no = student_no
def get_student_no(self):
return self.student_no
student = Student('Kim', 'Jensen', 's040203')
print(student.says())
print('student number is:', student.get_student_no())
print(student.characteristics)
Kim Jensen says hello! student number is: s040203 nourished with milk from mother as young
class Teacher(Person):
def __init__(self, fname, lname, department):
super().__init__(fname, lname)
self.department = department
def get_department(self):
return self.department
staff = Teacher('Lars', 'Hansen', 'Civil Engineering')
print(staff.says())
print('department:', staff.get_department())
print(staff.characteristics)
Lars Hansen says hello! department: Civil Engineering nourished with milk from mother as young
Create a new enhanced Triangle class based on the one you did for the exercises in the previous session. Use inheritance to avoid retyping the original class definition.
__lt__
(less than '<') dunder method so the two triangle areas are compared just by asking triangle1 < triangle2__repr__
dunder method with a reasonable descriptive outputCreate a Polyline class that is initialised from a list of Point instances with abitrary length.
__len__
dunder method that returns the integer number of line segments.get_total_length
method that returns the total length of the polyline by using the distance method available in the point objects.zip(points, points[1:])
.¶The cell below is for setting the style of this document. It's not part of the exercises.
# Apply css theme to notebook
from IPython.display import HTML
HTML('<style>{}</style>'.format(open('../css/cowi.css').read()))