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 objects themselves called 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:140390070991744 id R1:140390070991792
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)
Cell In[2], line 22
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,
25 a))
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.
[6]:
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.
[7]:
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))
Base: 10
Values:(1, 2, 3)
123 = 1*100 + 2*10 + 3*1
Base: 4
Values:(1, 2, 3)
27 = 1*16 + 2*4 + 3*1
mn+mn2:150
Error.Values must be lower than base
Can't create n. with base 3 and values (7, 1, 1)
123 < 27? False
123 == 27? False
123 > 27? True
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:
[8]:
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())
Jade Smith is aged 5
Dan Smith is aged 45
- Dan's child is Jade Smith
- Dan's child is John Smith
- Dan's child is Tim Smith
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.
[9]:
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)
Jade Smith is aged 5
Dan Smith is aged 45
- Dan's child is Jade Smith
- Dan's child is John Smith
- Dan's child is Tim Smith
Exercise¶
MicroRNAs (miRNAs) are a class of non-coding RNAs that play important roles in regulating gene expression. The majority of miRNAs are transcribed from DNA sequences into primary miRNAs and processed into precursor miRNAs, and finally mature miRNAs. In most cases, miRNAs interact with the 3’ untranslated region (3’ UTR) of target mRNAs to induce mRNA degradation and translational repression. (doi: 10.3389/fendo.2018.00402)
Implement:
a Gene class that can hold gene name, symbol, organism and chromosomal coordinates
override the less than operator to compare the location of two genes and say which comes first in the genome (same chromosome only)
a MiRNAGene child class that inherits from the Gene class and allows to store the mature miRNA sequence and a list of known target mRNAs
getters / setters / print methods
Show/Hide Solution