Module 2, Practical 1¶
The practicals of the second teaching module are a refinement of those prepared by Massimiliano Luca and Erik Dassi. Many thanks for their help.
Object Oriented Programming¶
As seen in the lecture, Python is a multi-paradigm language and it supports in fact the imperative/procedural paradigm (programs are sequences of statements that change the state of the system), the functional paradigm (programs are seen as mathematical functions, e.g. list comprehensions), some libraries are declarative (they define the logic without specifying the control-flow e.g. Matplotlib) but is also Object Oriented. In fact everything in Python is an object. Moreover, as we will see, new data-types can be defined in Python.
In Object Oriented Programming (OOP) objects are data structures that contain data, which is attributes and functions to work with them. In OOP, programs are made by a set of objects that interact with each other.
OOP allows to create a distinction (abstraction) between the way objects are implemented and how objects are used (i.e. what we can do with them).
Classes, Methods and Objects¶
The three key players of OOP are: classes, objects and methods.
Classes (types) are an abstraction that captures:
the internal data representation (i.e. data attributes that are called fields)
the interface to interact with the class (i.e. functions that can be used to manipulate the the methods).
Objects are instances of classes. Classes define the structure and are used to create objects. Objects are a concrete realization of the class, a real instance based on the footprint of the class. Programs are interactions among different objects.
Methods are functions that can be applied to manipulate objects.
Attributes and methods within an instantiated object can be accessed by using the .
(dot) operator.
Self¶
Within a class method, we can refer to that very same instance of the object being created by using a special argument that is called self
. self is always the first argument of each method.
Important note: All data types seen so far are in fact classes and every time that we used a data type (e.g. defining a list, a string etc.) we were in fact instantiating an object of that type (class).
Definition of a class¶
The syntax to define a class is the following:
class class_name:
#the initializer method
def __init__(self, val1,...,valn):
self.att1 = val1
...
self.attn = valn
#definition of a method returning something
def method1(self, par1,...,parn):
...
return value
#definition of a method returning None
def method2(self, par1,...,parn):
...
In this case we defined a class class_name
that has att1,..., attn
(attributes) fields and two methods method1
with parameters par1,...,parn
returning a value value
and a method method2
with parameters par1,...,parn
that does not return anything.
The values of the fields are initialized when the object is instantiated at the beginning by calling the __init__ method, which does not return anything. Note also the use of self
in the initializer, which is used to specify that each field of this instance has to be given the corresponding value and must always be the first argument of the initializer.
The object is instantiated with:
my_class = class_name(p1,...,pn)
which attributes the values p1,...,pn
to the fields field1,...,fieldn
.
Example: Let’s define a simple class rectangle with two fields (length and width) and two methods (perimeter and area).
[ ]:
class class_name:
#the initilizer method
def __init__(self, val1,...,valn):
self.att1 = val1
...
self.attn = valn
#definition of a method returning something
def method1(self, par1,...,parn):
...
return value
#definition of a method returning None
def method2(self, par1,...,parn):
...
my_class = class_name(p1,...,pn)
[1]:
import math
class Rectangle:
def __init__(self, l,w):
self.length = l
self.width = w
def perimeter(self):
return 2*(self.length + self.width)
def area(self):
return self.length * self.width
def diagonal(self):
return math.sqrt(self.length**2 + self.width**2)
R = Rectangle(5,10)
print(type(R))
R1 = Rectangle(5,10)
print(type(R1))
print("R == R1? {} id R:{} id R1:{}".format(R == R1,
id(R),
id(R1)))
p = R.perimeter()
a = R.area()
d = R.diagonal()
print("\nR:\nLength: {} Width: {}\nPerimeter: {}\nArea:{}".format(R.length,
R.width,
p,
a))
print("R's diagonal: {:.2f}".format(d))
R2 = Rectangle(72,13)
p = R2.perimeter()
a = R2.area()
d = R2.diagonal()
print("\nR2:\nLength: {} Width: {}\nPerimeter: {}\nArea:{}".format(R2.length,
R2.width,
p,
a))
print("R's diagonal: {:.2f}".format(d))
<class '__main__.Rectangle'>
<class '__main__.Rectangle'>
R == R1? False id R:140461848354768 id R1:140461848353232
R:
Length: 5 Width: 10
Perimeter: 30
Area:50
R's diagonal: 11.18
R2:
Length: 72 Width: 13
Perimeter: 170
Area:936
R's diagonal: 73.16
Note that the type of the two objects R
and R2
are of type Rectangle
and that they have have different identifiers. Instantiating objects automatically calls the initializer methods (__init__
) passing the correct parameters to it. The dot .
operator is used to access methods of the objects. Through the dot operator we can also access the fields of an object, even though this is normally not the best practice and implementing methods to get
and set
the values of fields
are recommended.
The life-cycle of classes and objects in a program:¶
The usual life-cycle of classes and objects is the following:
Classes are defined with the specification of class attributes (fields) and methods;
Objects are instantiated based on the definition of the corresponding classes;
Objects interact with each other to implement the logic of the program and modify their state;
Objects are destroyed (explicitly with del) or implicitly when there are no more references to them.
Encapsulation¶
When defining classes, it is possible to hide some of the details that must be kept private to the object itself and not accessed directly. This can be done by setting methods and attributes (fields) as private to the object (i.e. accessible only internally to the object itself).
Private attributes and methods can be defined using the __
notation (i.e. the name of the attribute or method is preceded by two underscores __
).
Example Let’s see what happens to the rectangle class with encapsulation.
[2]:
import math
class Rectangle:
def __init__(self, l,w):
self.__length = l
self.__width = w
def perimeter(self):
return 2*(self.__length + self.__width)
def area(self):
return self.__length * self.__width
def diagonal(self):
return math.sqrt(self.__length**2 + self.__width**2)
R = Rectangle(10,6)
p = R.perimeter()
a = R.area()
d = R.diagonal()
#we might be tempted to access the encapsulated values:
print("\nR:\nLength: {} Width: {}\nPerimeter: {}\nArea:{}".format(R.__length,
R.__width,
p,
a))
#The following is going to fail alike.
#print("\nR:\nLength: {} Width: {}\nPerimeter: {}\nArea:{}".format(R.length,
# R.width,
# p,
# a))
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
/tmp/ipykernel_59974/2984036234.py in <module>
20 d = R.diagonal()
21 #we might be tempted to access the encapsulated values:
---> 22 print("\nR:\nLength: {} Width: {}\nPerimeter: {}\nArea:{}".format(R.__length,
23 R.__width,
24 p,
AttributeError: 'Rectangle' object has no attribute '__length'
Since the length
and width
attributes are private to the class, it is not possible to get access from the outside. The code above will fail with the object has no attribute error message
To work around this, we can define a specific interface to access the values (these are normally called getter methods as they get and return the value).
Example Let’s see what happens to the rectangle class with encapsulation and getter methods.
[3]:
import math
class Rectangle:
def __init__(self, l,w):
self.__length = l
self.__width = w
def getLength(self):
return self.__length
def getWidth(self):
return self.__width
def perimeter(self):
return 2*(self.__length + self.__width)
def area(self):
return self.__length * self.__width
def diagonal(self):
return math.sqrt(self.__length**2 + self.__width**2)
R = Rectangle(10,6)
p = R.perimeter()
a = R.area()
d = R.diagonal()
print("\nR:\nLength: {} Width: {}\nPerimeter: {}\nArea:{}".format(R.getLength(),
R.getWidth(),
p,
a))
R:
Length: 10 Width: 6
Perimeter: 32
Area:60
Setter methods can be used to change the values of attributes after initialization.
Example: Let’s define a Person class with the following attributes: name, surname, telephone number and address. All attributes are private. The address and phone numbers might change, so we need a method to change them.
[4]:
class Person:
def __init__(self, name, surname, birthdate):
self.__n = name
self.__s = surname
self.__dob = birthdate
self.__a = "unknown"
self.__t = "unknown"
def setAddress(self, address):
self.__a = address
def setTelephone(self, telephone):
self.__t = telephone
def getName(self):
return self.__n
def getSurname(self):
return self.__s
def getDoB(self):
return self.__dob
def getAddress(self):
return self.__a
def getTel(self):
return self.__t
Joe = Person("Joe", "Page", "20/5/1980")
Joe.setAddress("Somerset Rd.,Los Angeles, CA 90016")
print("{} {}\nDate of Birth: {}\nPhone: {}\nAddress: {}".format(Joe.getName(),
Joe.getSurname(),
Joe.getDoB(),
Joe.getTel(),
Joe.getAddress()
))
#Joe moves to Trento
Joe.setAddress("via Sommarive, Povo, Trento")
print("\nNew address: {}".format(Joe.getAddress()))
Joe Page
Date of Birth: 20/5/1980
Phone: unknown
Address: Somerset Rd.,Los Angeles, CA 90016
New address: via Sommarive, Povo, Trento
The setAddress
method is a setter method that is used to change the value of the attribute __a
.
Special methods¶
As seen in the lecture, it is possible to redefine some operators by redefining the corresponding special methods through a process called overriding.
These are useful to define things like the sum of two objects (__add__
), the equality (__eq__
), which one is the smallest (__lt__
that is less than) or the way the object should be translated into a string (__str__
), for example for printing purposes.
More information on these special methods can be found here.
Example A decimal number \(T\) can be expressed in base \(X\) (for simplicity we will consider \(X \in [1,9]\)) as: \(T = aX^{N} + bX^{N-1}+...+ (n+1) X^{0}\). Such a number can be represented as two elements: \((X, (a,b,...,n+1))\) (the base and the tuple of all the values). Let’s define a class MyNumber that can represent numbers in any base (from 1 to 9). The class has two attributes, the base
(which is a number representing the base) and the values
a tuple of
numbers. Let’s redefine some operators (i.e. add, lt, str) and implement some methods to covert these numbers into decimals.
[ ]:
class MyNumber:
def __init__(self, base, values):
self.base = base
warn = False
for v in values:
if(v >= base):
print("Error.Values must be lower than base")
print("Can't create n. with base {} and values {}".format(base,values))
warn = True
if(not warn):
self.values = values
else:
self.values = None
def toDecimal(self):
res = 0
L = len(self.values)
for i in range(L):
res += self.values[i] * self.base**(L-1 - i)
return res
def __str__(self):
return "Base: {}\nValues:{}".format(self.base, self.values)
def __add__(self, other):
return self.toDecimal() + other.toDecimal()
def __lt__(self, other):
return self.toDecimal() < other.toDecimal()
def toDecimalString(self):
L = len(self.values)
res = str(self.values[0]) + "*" +str(self.base ** (L-1))
for i in range(1,L):
res += " + " + str(self.values[i]) + "*" + str(self.base**(L-1 - i))
return res
mn = MyNumber(10,(1,2,3))
print(mn)
print("{} = {}".format(mn.toDecimal(), mn.toDecimalString()))
mn2 = MyNumber(4, (1,2,3))
print("\n{}".format(mn2))
print("{} = {}".format(mn2.toDecimal(), mn2.toDecimalString()))
mn3 = mn + mn2
print("\nmn+mn2:{}".format(mn3))
print("\n")
mn4 = MyNumber(3,(7,1,1))
print("\n")
print("{} < {}? {}".format(mn.toDecimal(),mn2.toDecimal(),mn < mn2))
print("{} == {}? {}".format(mn.toDecimal(),mn2.toDecimal(),mn == mn2))
print("{} > {}? {}".format(mn.toDecimal(),mn2.toDecimal(),mn > mn2))
Inheritance and overriding¶
One object can inherit the attributes and methods from another object. This establishes a “Is-a” relationship between the two objects. The first object is called subclass of the original class. A subclass inherits all the methods and attributes of the superclass, but it can also redefine some methods through a process called overriding.
The syntax to define a subclass is the following:
class MySuperClass:
...
def myMethod(self,...):
...
class MySubClass(MySuperClass):
...
def myMethod(self,...):
...
basically, we just specify the superclass after the name of the subclass we are defining.
Consider the following example:
[ ]:
class Person:
def __init__(self, name, surname, age):
self.name = name
self.surname = surname
self.age = age
def getInfo(self):
return "{} {} is aged {}".format(self.name,
self.surname,
self.age)
class Dad(Person):
children = []
def addChild(self,child):
self.children.append(child)
def getChildren(self):
return self.children
def getInfo(self):
personalInfo = "{} {} is aged {}".format(self.name,
self.surname,
self.age)
childrInfo = ""
for son in self.getChildren():
childrInfo += " - {}'s child is {} {}".format(
self.name, son.name, son.surname) +"\n"
return personalInfo + "\n" + childrInfo
jade = Person("Jade", "Smith",5)
print(jade.getInfo())
john = Person("John", "Smith",4)
tim = Person("Tim", "Smith",1)
dan = Dad("Dan", "Smith", 45)
dan.addChild(jade)
dan.addChild(john)
dan.addChild(tim)
print(dan.getInfo())
Note that the object Dad
(subclass) is-a Person
(superclass) but has a further attribute that is the list of children, each of which are of type Person
. The getInfo
method of the subclass Dad
overrides the corresponding method of the superclass Person
and prints some information on the children.
[ ]:
class Person:
def __init__(self, name, surname, age):
self.name = name
self.surname = surname
self.age = age
def __str__(self):
return "{} {} is aged {}".format(self.name,
self.surname,
self.age)
class Dad(Person):
children = []
def addChild(self,child):
self.children.append(child)
def setChildren(self, children):
self.children = children
def getChildren(self):
return self.children
def __str__(self):
personalInfo = "{} {} is aged {}".format(self.name,
self.surname,
self.age)
childrInfo = ""
for son in self.getChildren():
childrInfo += " - {}'s child is {} {}".format(
self.name, son.name, son.surname) +"\n"
return personalInfo + "\n" + childrInfo
jade = Person("Jade", "Smith",5)
#print(jade.getInfo())
john = Person("John", "Smith",4)
tim = Person("Tim", "Smith",1)
dan = Dad("Dan", "Smith", 45)
dan.setChildren([jade,john])
#dan.addChild(john)
dan.addChild(tim)
print(jade)
print(dan)
Exercise¶
Implement a superclass Polygon
with two attributes:
shape
sides’ length.
Add a method to return the perimeter and a method to return the number of edges of the polygon. Implement a __str__
method to print an object of the class with its shape. Then create two subclasses of poylgon: Rectangle and Triangle. For each subclass modify the initializer accordingly and also override the __str__
to print the object id.
Show/Hide Solution
Lambda functions¶
Computing in the functional programming paradigm is obtained by applying functions to a set of inputs. To have an idea about the differences with OOP, see this link: https://dev.to/documatic/when-to-use-functional-programming-vs-oop-122n
Three mechanisms are available in python:
map :
map(f, input_list)
applies the functionf
to all the elements ofinput_list
;filter :
filter(f, input_list)
filtersinput_list
based on a functionf
that returns true or false for each of the input elements;reduce :
reduce(f, input_list)
applies the functionf
to the first two elements of the input list, then it applies it to the result and to the third element and so on until the end of the list is reached and one value only is returned.
Note that the reduce
function is part of the functools
module and needs to be imported with:
from functools import reduce
Let’s see some examples of map and filter.
Example: Let’s define two functions to convert temperatures from fahrenheit to celsius and vice-versa. Let’s finally use them to work out the temperatures that are above and below freezing.
[ ]:
def fahrenheit(T):
return ((float(9)/5)*T + 32)
def celsius(T):
return (float(5)/9)*(T-32)
def freezingFiltF(T):
return celsius(T) < 0
def freezingFiltC(T):
return T < 0
farTmp = (27.5, 29, 37.5,12, 44, 72,100)
C = map(celsius, farTmp)
print(type(C))
C = tuple(C)
for i in range(len(C)):
print("{:.2f}°F --> {:.2f}°C".format(farTmp[i],C[i]))
print("\n")
print("Freezing temperatures:")
print(type(filter(freezingFiltF, farTmp)))
ftF = tuple(filter(freezingFiltF, farTmp))
ftC = tuple(filter(freezingFiltC, C))
for i in range(len(ftF)):
print("{:.2f}°F --> {:.2f}°C".format(ftF[i],ftC[i]))
Note that map
and filter
respectively return an object map
and an object filter
which we need to convert into lists or tuples if we want to work with the results.
Examples
Use reduce
to:
sum the elements of a list
count how many non-space characters a string has
[ ]:
from functools import reduce
lst = [4,5,6,7]
reducedLst = reduce(int.__add__, lst)
print(reducedLst)
[ ]:
from functools import reduce
myText = "Testing shows the presence, not the absence of bugs"
words = myText.split()
print("Word sizes: {}".format(list(map(len, words))))
cnt = reduce(int.__add__, list(map(len, words)))
print("Dijkstra's quote has {} characters".format(cnt))
All the above examples required the specification of a function. We do not necessarily need to specify a function name in Python thanks to the anonimous functions also known as lambda functions.
Their basic syntax is:
lambda input-parameters: expression
where the keyword lambda
proceeds the comma separated list of input parameters and a colon :
marks the beginning of the expression. We can anyway give a name to the lambda function with:
myfunct = lambda input-parameters: expression
Some examples of lambda functions follow:
[ ]:
sum_lambda = lambda x, y : x+y
mult_lambda = lambda x,y : x*y
cap_lambda = lambda x : x.capitalize()
print(sum_lambda(10,20))
print(sum_lambda("Hi ", "there!"))
print("\n")
print(mult_lambda(10,20))
print(mult_lambda("Hi! ", 3))
print("\n")
txt = "hi there from luca!"
print(cap_lambda(txt))
print(" ".join(map(cap_lambda, txt.split())))
More interesting uses of lambda involve sorting of tuples through the key
parameter:
Example: Let’s count how many occurrences we have for each non-space character in the string “Testing shows the presence, not the absence of bugs”. And print the least and most frequent character, as well as the first and last in alphabetic order.
[3]:
myText = "testing shows the presence, not the absence of bugs"
cnts = [(x,myText.count(x)) for x in myText if x != " " and x != ","]
uniqueCnts = []
for e in cnts:
if(e not in uniqueCnts):
uniqueCnts.append(e)
print(uniqueCnts)
get_first = lambda x : x[0]
get_second = lambda x : x[1]
#Get the most and least frequent character(s).
uniqueCnts.sort(key = get_second)
all_least_f = list(filter(lambda x: x[1] == uniqueCnts[0][1], uniqueCnts))
all_most_f = list(filter(lambda x: x[1] == uniqueCnts[-1][1], uniqueCnts))
print("All least frequent: {}".format(all_least_f))
print("All most frequent: {}".format(all_most_f))
#Get the most and least frequent character.
uniqueCnts.sort(key = get_first)
print("The first in alphabetic order: {}".format(uniqueCnts[0]))
print("The last in alphabetic order: {}".format(uniqueCnts[-1]))
[('t', 5), ('e', 8), ('s', 6), ('i', 1), ('n', 4), ('g', 2), ('h', 3), ('o', 3), ('w', 1), ('p', 1), ('r', 1), ('c', 2), ('a', 1), ('b', 2), ('f', 1), ('u', 1)]
[('i', 1), ('w', 1), ('p', 1), ('r', 1), ('a', 1), ('f', 1), ('u', 1), ('g', 2), ('c', 2), ('b', 2), ('h', 3), ('o', 3), ('n', 4), ('t', 5), ('s', 6), ('e', 8)]
All least frequent: [('i', 1), ('w', 1), ('p', 1), ('r', 1), ('a', 1), ('f', 1), ('u', 1)]
All most frequent: [('e', 8)]
[('a', 1), ('b', 2), ('c', 2), ('e', 8), ('f', 1), ('g', 2), ('h', 3), ('i', 1), ('n', 4), ('o', 3), ('p', 1), ('r', 1), ('s', 6), ('t', 5), ('u', 1), ('w', 1)]
The first in alphabetic order: ('a', 1)
The last in alphabetic order: ('w', 1)
Exercises¶
Create a Consumer class with:
wealth as attribute (amount of money)
earn method, where earn(m) increments the consumer’s wealth by m
a spend method, where spend(m) either decreases wealth by m or returns an error if there are not enough money
a
__str__
will print the wealth at the current time
Show/Hide Solution
Define a 3D point class (
Point3D
) which contains three attributes that are the (x,y,z) coordinates in the 3D space and a string (label). Implement acomputeDistance
method that computes the distance between the point and another point. Remember that if \(a = (x_a,y_a,z_a)\) and \(b = (x_b,y_b,z_b)\), \(distance(a,b) = \sqrt{{(x_a-x_b)}^2 + {(y_a-y_b)}^2 + {(z_a-z_b)}^2}\).
Given the following points:
p = Point3D(0,10,0, "alfa")
p1 = Point3D(10,20,10, "point")
p2 = Point3D(4,9, 10, "other")
p3 = Point3D(8,9,11, "zebra")
p4 = Point3D(0,10,10, "label")
p5 = Point3D(0,10,10, "last")
p6 = Point3D(42,102,10, "fifth")
Write some code to find out the closest pair of 3D points;
Sort the points by distance to the origin (0,0,0) and print them in this order;
Sort the points alphabetically by label (and print them).
Show/Hide Solution
Write a Person class with the following attributes: name, surname and mailbox. A mailbox is a list that contains string messages sent to the person. Each message entry should be a tuple
(name, surname, message)
wherename
andsurname
are the name and surname of the sender of the message, whilemessage
is a string with the text of the message. Implement the following methods:getName
: gets the name of the Person;getSurname
: gets the surname of the Person;sendMessage
: that has aPerson
and the string with the message in input and sends the message to the specified Person;checkMailbox
: returns the number of messages in the mailbox;readMessages
: returns all the messages in the mailbox as a list;checkMessagesFrom
: checks and prints any messages coming from a specific Person (in input);clearMailbox
: clears the mailbox (does not return anything);
Test the class with the following conversation:
luca = Person("Luca", "Bianco")
alberto = Person("Alberto", "Montresor")
david = Person("David", "Leoni")
stranger = ""
luca.sendMessage(alberto, "Hi Alberto, hope things are fine.")
alberto.sendMessage(luca, "I am fine, thanks. Yourself?")
luca.sendMessage(alberto, "Great. Cheers. How about David?")
alberto.sendMessage(david, "You OK?")
david.sendMessage(alberto, "Yep. Thanks")
alberto.sendMessage(luca, "All OK")
luca.sendMessage(stranger, "Who are you?")
and check the mailbox of all the Persons in [luca,alberto,david]
.
Show/Hide Solution