Pyton OOP Tutorial – 3. Methods overriding

Derived classes allow code reuse this make code more easy to mantain and help in avoiding codding errors. However in derived class appear often casses when inherited method from parrent class isn't appropriate. 

For example let's presume we have an Employee class which have 3 attributes name, salary, phone and a method printEmployee which will display employee name and phone.

We derive from this a class Freelancer which have in plus atttribute domain. We need that for freelacer, printEmployee method to print name, phone and in plus domain. In this case printEmployee from base class is not apropriate and we need to extend it. Below is sample code:

class Employee:

   def __init__(self,nameValue, salaryValue, phoneValue):

      self.name=nameValue

      self.salary=salaryValue

      self.phone=phoneValue

   def getName(self):

      return self.name

   def getSalary(self):

      return self.salary

   def printEmployee(self):

       print(f"Employee: {self.name} have phone {self.phone}")


class Freelancer(Employee):

   def __init__(self,nameValue, salaryValue, phoneValue, domainValue):

      super().__init__(nameValue, salaryValue, phoneValue)

      self.domain=domainValue

   def printEmployee(self):

      print(f"Freelancer: {self.name} have phone {self.phone} and works in domain {self.domain}")


#create Employee object and use it printEmployee

emp1=Employee('Jd',4000,'555-342345')

emp1.printEmployee()

#create Freelancer object and use it overriding printEmployee method

fr1=Freelancer("TomH", 4200,'555-346534', 'Graphic design')

fr1.printEmployee()


#output:

Employee: Jd have phone 555-342345

Freelancer: TomH have phone 555-346534 and works in domain Graphic design


  • Observe in sample code that printEmployee method is implemented again (is overwritted) in Freelancer derived class. 
  • When we use printEmployee with a base class object, it will use method from base class see emp1.printEmployee() which will print "Employee: Jd have phone 555-342345".
  • When we use printEmployee with derived class object fr1, see fr1.printEmployee() it will check first in derived class if printEmployee is overriding and if yes, it will use it. Output of fr1.printEmployee() will be "Freelancer: TomH have phone 555-346534 and works in domain Graphic design".
  • Observation: If in derived class method is not override, then method from parent class will be used. 

Python OOP Tutorial – 1. Objects in OOP

Initially it was procedural programing, which just uses methods or functions that do specific tasks. In procedural programing program logic flow linear, from start to end, sometime there point from functions that go to the program end. When programs become complex and there are many relationships between components working and maintaining procedural programs become difficult. From that reason appear object-oriented programing.

OOP is necessary especially when code becomes too complex, the code logic becomes too bushy. If we need to write a program for a car, imagine this have a logic for maintaining direction, for engine use, for lights, etc.  When writing programs for complex systems the solution is to identify entities and define classes for them. For each entity we have attributes and for those we use in classes variables. Each entity has specific functionalities and those correspond in classes to methods or functions. That is in simple mode OOP idea, obviously things are more complex, OOP means more like constructors, inheritance, interfaces, etc. We will discuss those later during tutorial.  Click here  for an exhaustive description of OOP. 

Let's take in this tutorial an example related to horticulture plants. What are some attributes of those plants? Some attributes are: lifetime, biological characteristics, color, type of seed, type of root, etc. What are some methods? Let's put here: germination, greeding, pollination, fruit ripening. The class is the blueprint that model OOP paradigm. First, we need to define the class. Then generate based on that many more objects, "instantiating" that class.


Simple example of class definition in python:

class Plant:

    def __init__(self, lifetime, biological_characteristic, fruit_weight):

        self.lifetime = lifetime

        self.biological_characteristic = biological_characteristic

        self.fruit_weight = fruit_weight

    def printplant(self):

        print(self.lifetime, self.biological_characteristic, self.fruit_weight)



mybrocolly = Plant("biennial", "vegetable", 250)

apple1 = Plant("perennial", "tree", 70)


print("Brocolly is a " + mybrocolly.lifetime + " plant")

print("Apple is a " + apple1.biological_characteristic)

mybrocolly.printplant()


#output 

Brocolly is a biennial plant

Apple is a tree

biennial vegetable 250


Explanation about above: 


The class name is Plant. It begin with capital letter ("P"). If class name is composed from multiple words, each first letter of the words should be in capital for example if we desire to name class "plantblueprint" correct name should be "PlantBlueprint", this  notation is known as "Pascal case" Class has three attributes: lifetime, biological_characteristic and fruit_weight.

__init__ is a special class method, the class constructor, inside it we initialize class attributes. After class definition we create two objects: 

mybrocolly using  line -> mybrocolly=Plant("biennial", "vegetable", 250) 

apple1 using line -> apple1=Plant("perennial", "tree", 70) 

In both cases mybrocolly and apple1 are objects and are created from the same "blueprint", class Plant

Observation: fruit_weight is in grams


Now some explanation about how __init__ (in some sources named constructor) works. When we run mybrocolly=Plant("biennial", "vegetable", 250)  constructor __init__ is appealed and it initialize object attributes, there will be: 

lifetime="biennial" which is realized by self.lifetime = lifetime constructor instruction 

biological_characteristic="vegetable" which is realized by  self.biological_characteristic = biological_characteristic

fruit_weight = 250 which is realized by  self.fruit_weight = fruit_weight


To access lifetime attribute of the mybrocolly object we used mybrocolly.lifetime, means in general to access atribute we 

use "objectname.attributename". Similar to acccess apple1 biological_characteristic attribute we used apple1.biological_characteristic. 

Create dataframe in Pandas

One of the simple modes to create dataframe in Python, pandas is to create it from a dictionary. Below example create a dataframe from dictionary.

import pandas as pd

dict1 = {

    'Ford': [120, 230, 120, 431],

    'Renault': [320, 233, 547, 622],

    'Audi': [230, 123, 457, 232],

    'Toyota': [230, 123, 457, 232],

    'Opel': [230, 123, 457, 232]

}

print(dict1.keys())

print("Ford key is:", end=' ')

print(dict1['Ford'])

sells = pd.DataFrame(dict1) # create sells dataframe from 'dict1' dictionary

print('DataFrame is:')

print(sells)


#output:

dict_keys(['Ford', 'Renault', 'Audi', 'Toyota', 'Opel'])

Ford key is: [120, 230, 120, 431]

DataFrame is:

   Ford  Renault  Audi  Toyota  Opel

0   120      320   230     230   230

1   230      233   123     123   123

2   120      547   457     457   457

3   431      622   232     232   232

Comments about above code: "dict1" dictionary contain situation with car sales for a machine dealer, for first 4 month of the year.

Dictionary keys are 'Ford', 'Renault' 'Audi' 'Toyota' 'Opel'.

Dictionary values are lists, means for 'Ford' key value is list [120, 230, 120, 431].

Recalling from dictionaries theory print(dict1.keys()), which will show: 

dict_keys(['Ford', 'Renault', 'Audi', 'Toyota', 'Opel'])

and print(dict1['Ford']) will show [120, 230, 120, 431].

And finally, method that create sells dataframe from dictionary is: sells = pd.DataFrame(dict1).


There is a possibility to create dataframe from dictionary using method from_dict of class DataFrame, which works similar like in previous example, but it has more options.

Example: 

import pandas as pd

dict2 = {

    'candy':['80%', '60%', '45%'],

    'chocolate':['12%', '24%', '7%'],

    'wafer':['14%', '18%', '16%']

}


sells2=pd.DataFrame.from_dict(dict2)

print(sells2)


#output: 

  candy chocolate wafer

0   80%       12%   14%

1   60%       24%   18%

2   45%        7%   16%

We see using DataFrame.from_dict specifying only dictionary from which we create dataframe, it will create a dataframe in which keys become dataframe columns.


Using from_dict with parameter orient='index' will create a dataframe in which keys are first values in row like below example: 

import pandas as pd

dict2 = {

    'candy':['80%', '60%', '45%'],

    'chocolate':['12%', '24%', '7%'],

    'wafer':['14%', '18%', '16%']

}


sells2i=pd.DataFrame.from_dict(dict2, orient='index')

print(sells2i)


#output:

                   0       1       2

candy         80%  60%  45%

chocolate  12%   24%   7%

wafer         14%   18%  16%


Creating dataframe from a list of tuples. 

This can be achieved using method DataFrame.from_records

Example: 

import pandas as pd

marks = [('Mike', 9), ('Debora', 10), ('Steve', 9), ('Tim', 8)]

marks_df = pd.DataFrame.from_records(marks, columns=['Student', 'Mark'])


#Output:

      Student  Mark

0    Mike         9

1    Debora    10

2    Steve        9

3    Tim           8


Other example for dataframe from list of tuples with more elements: 

import pandas as pd

marks = [('Mike', 9, 8), ('Debora', 10, 9), ('Steve', 9, 10), ('Tim', 8, 9)]

marks_df = pd.DataFrame.from_records(marks, columns=['Student', 'Mark1','Mark2'])

print(marks_df)


#output:

  Student     Mark1  Mark2

0    Mike       9         8

1   Debora    10       9

2   Steve        9        10

3   Tim           8        9


from_records can be used similar to create dataframe from a list of dictionaries,

Example:

import pandas as pd

marks = [{'Student':'Mike' , 'Mark':9},

               {'Student':'Debora' , 'Mark':10},

       {'Student':'Steve' , 'Mark':9},

       {'Student':'Tim' , 'Mark':8}

        ]

marks_df1 = pd.DataFrame.from_records(marks)

print(marks_df1)


#Output

      Student   Mark

0    Mike         9

1    Debora    10

2    Steve        9

3    Tim           8


We can create dataframe from numpy array

Example:

import pandas as pd

import numpy as np

data = np.array([('Student','Mark'),('Mike', 9), ('Debora', '10'), ('Steve', 9), ('Tim', 8)])

marks_df2= pd.DataFrame.from_records(data)

print(marks_df2)


#output:

         0            1

0     Student  Mark

1     Mike         9

2     Debora    10

3     Steve        9

4     Tim           8

Strings in Python

To create a string variable, enclose characters with " " or ' ' 

Example:

string1 = "This is Python string 1"

string2 = 'This is Python string 2'

print(string1)

print(string2)


#output: 

This is Python string 1

This is Python string 2


This is when defining strings on a single line. There exists a possibility to define a string that spread on multiple lines using triple double quotes or triple single quotes i.e. """ or '''. 

Example: 

string_m = """Today is Monday and 

the sky is perfect clear"""


To understand the way to access elements in a string we start from analogy that Python strings are like a list of characters, hence elements can be accessed using index, negative index or even slicing notation. Exist also __getitem__() method which is a little bit similar with accessing using index. 


Example using index: 

mystr = "Today is Monday"

print(mystr[0]) 

print(mystr[1]) 


#output: 

T

o


Example using negative index:

mystr = "Today is Monday"

print(mystr[-1])

print(mystr[-3])


#output: 

Y

d

We observe that negativ index "count back" from the end of the string elements.


Example using slicing notation: 

mystr = "Today is Monday"

print(mystr[0:2]) 

print(mystr[1:5]) 


#output: 

To

oday

We observe that in slicing it count till "right slice value -1" means mystr[0:2] will show mystr[0], mystr[1] but will not show mystr[2]


Example using __getitem__()

mystr = "Today is Monday"

print(mystr.__getitem__(0))


#output

T


String comparison

It is performed with == operator

example: 

s1="alfa"

s2="Alfa"

s3="alfa"

if(s1==s2):

    print("s1 is equal with s2")

else:

    print("s1 is not equal with s2")

#

if(s1==s3):

    print("s1 is equal with s3")

else:

    print("s1 is not equal with s3")


#output: 

s1 is not equal with s2

s1 is equal with s3

We observe that == with strings is case sensitive.


The other way to compare strings is using __eq__() or __ne__()

Example: 

s1="alfa"

s2="Alfa"

s3="alfa"

print(s1.__eq__(s2))

print(s1.__eq__(s3))

print(s1.__ne__(s2))


#output:

False

True

True

Thus method __eq__() return True if strings are equal, while __ne__() return True if strings are not equal. Basically == operator automatically call __eq__() method.


Concatenating or joining strings

Strings can be concatenated using "+"  operator

Example: 

s1 = "Alfa"

s2 = "Beta"

print(s1+s2)


#output:

AlfaBeta

Built in functions: next, bool

next(iterator)

Python built-in function next is used to retrieve next item from an iterator, if it reaches iterator end built-in exception StopIteration is raised. 

Example:

list1 = [23, True, 'yes']

myIterator = iter(list1)

#

item = next(myIterator)

print(f"First item is: {item}")

#

item = next(myIterator)

print(f"Item is: {item}")

#

item = next(myIterator)

print(f"Last item is: {item}")

item = next(myIterator)


#Output

First item is: 23

Item is: True

Last item is: yes

Traceback (most recent call last):

  File "test.py", line 9, in <module>

    item = next(myIterator)

StopIteration

In this example we create iterator myIterator from list1. Then we run next funtion trice using item = next(myIterator) and print item with a f string. myIterator have 3 items, after running trice next function we are at the end of it, once try to run the fourth time next function, StopIteration built-in exception is raised. 


class bool(x=False)

The method return a boolean value based on standard "truth testing procedure". Basically this procedure will return True if argument is a number (excepting 0), a string or True itself. It will return False if argument is False, 0 or None, empty. "Truth testing procedure" in python area is comprehensive described here


Examples:

myarg1=12

print(bool(myarg1))

# output: True


myarg2='string123'

print(bool(myarg2))

# output: True


myarg3=True

print(bool(myarg3))

# output: True


myarg4=False

print(bool(myarg4))

# output: False


myarg5=0

print(bool(myarg5))

# output: False


myarg6=None

print(bool(myarg6))

# output: False


print(bool())

# output: False

Built in functions: all, filter and slice

all(iterable)

Function accepts like argument an iterable structure (like list, tuple, dictionary, etc), function returns True if all elements of the iterable structure are true.

Example:

mylist1 = [1, 'two', 'xyz', 8.35, True]

mylist2 = [1, 'two', 'xyz', 8.35, 'yhg']

mylist3 = [1, 0, 'xyz', 8.35, 'test']

mylist4 = [1, 'tt', False, 8.35, 'test']


print(f'all(mylist1) give: {all(mylist1)}')

print(f'all(mylist2) give: {all(mylist2)}')

print(f'all(mylist3) give: {all(mylist3)}')

print(f'all(mylist4) give: {all(mylist4)}')


output: 

all(mylist1) give: True

all(mylist2) give: True

all(mylist3) give: False

all(mylist4) give: False

Here all(mylist1) and all(mylist2) return True because all elements of mylist1 and mylist2 are True (anything that is not False or 0 is considered true).

all(mylist3) return False because mylist3 contain 0 element. all(mylist4) return False because mylist4 contain False.


filter(function, iterable)

Python built-in filter function construct an itertor based on an iterable parameter like list, tuple, etc, taking from it only elements that fulfil function parameter condition. Description sounds a little bit complicated, but some examples show that filter function is simple. 

Let's presume we have list1 = [45, 12, 5, 7, 32, 20, 2, 18, 21, 9, 16]

We need to obtain from list1 a new list that should contain only numbers greater than 15. Code for this is:

list1 = [45, 12, 5, 7, 32, 20, 2, 18, 21, 9, 16]

# filter function for number greater than 15

def fg15(x):

    if x>15:

        return x

# apply filter function

result=filter(fg15, list1)

list2=list(result)

print(list2)

 

output:

[45, 32, 20, 18, 21, 16]

We observe here that list2 contains elements from list1 that are greater than 15 (which fulfil filter restriction).


class slice ([start], stop[, step]) 

Explanation of slice according with python documentation https://docs.python.org/3/library/functions.html#slice is a little bit complicated. 

Slice function returns a "slice object" representing a set of indices similar to range(start, stop, step). Step parameter is optional, if not specified it is 1. Start is optional, if it is not specified start is first element. It is easy to understand slice by examples:

list1 = (23, True, 5, 12, 'yes', 'cat', 'dog', 34)

slc1=slice(1,6,2)


print(slc1) # here slc1 is the slice objet

print(list1[slc1])

print(list1[1:6:2]) 


output: 

slice(1, 6, 2)

(True, 12, 'cat')

(True, 12, 'cat')


For

   The for statement iterates over a structure ("a sequence of something", list, tuple, set, dictionary, etc.)

A general syntax is like:

for ELEMENT in STRUCTURE:

statememn1

statement2

...


Example for iterating over a list:

mylist = [1, 'two', 'xyz', 8.35, True]

for element in mylist:

print(element)


#output

1

two

xyz

8.35

True


   We can use range() function in for to iterate over a sequence of numbers, or over a part of structure elements. For example, if we need to iterate/touch over first 2 elements:

mylist = [1, 'two', 'xyz', 8.35, True]

for i in range(2):

print(mylist[i])


#output

1

two

   General syntax for range function is range(start, stop, step=1), based on this we can iterate with other step different than 1, also iterate not only from the beginning of the structure but over a subset of elements, example:

mylist = [1, 'two', 'xyz', 8.35, True]

for i in range(1,3):

print(mylist[i])


#output

two

xyz

Here range function return values from start value till end value minus 1, i.e. range(1,3) give 1,2 values.

Tuples

   Tuples are python data structures similar to lists. A tuple is composed of values separated by commas and enclosed within parenthesis. Like comparison to remember that lists are enclosed in brackets.

Tuple example:

myTuple=('mnp', 5, 145.34, False, 'Blue sky')
print(myTuple)

#output
(False, 'Blue sky', 145.34, 5, 'mnp')


   Tuple elements are ordered and immutable (tuple is like a read-only list). Tuple elements are accessed similar like lists. Below are examples of accessing tuple elements:

myTuple=('mnp', 5, 145.34, False, 'Blue sky')


print(myTuple[1])     #accessing by index

#output

5


print(myTuple[2:4])     #accessing with slice notation

#output

(145.34, False)


print(myTuple[1:])     #accessing with slice notation

#output

(5, 145.34, False, 'Blue sky')


   Tuples elements are immutables (tuple is like a read-only list). If we try to modify an element from tuple similar like we do in lists case we will get an error. For example: 

myTuple = (5, 'test123', 23.54, False, 'xyz')

myTuple[0]=10


#output

Traceback (most recent call last):

  File "<pyshell#1>", line 1, in <module>

    myTuple[0]=10

TypeError: 'tuple' object does not support item assignment


   Even if tuple is immutable, a tuple can contain like elements a list, elements of which can be modified. For example:

tuple123 = (10, [12, 7.5, True], 34.54, 'abc')

tuple123[1][0]=11

print(tuple123)


#output

(10, [11, 7.5, True], 34.54, 'abc')

Here we see tuple123 contain like second element a list, i.e. [12, 7.5, True], means tuple123[1]= [12, 7.5, True]. There tuple123[1][0] access first element of list [12, 7.5, True]


In previous example we have a nested list in a tuple, or a tuple that contains elements like a list. 

Obvious we can have a tuple that contains other tuple(s) and so on..., nested tuples.

List In Python – II

In today's post I will present some of the most used list functions.


- len, display list length. Example:

mylist = [1, 'two', 'xyz', 8.35, True]

print(len(mylist))


#output

5


- insert, add an item to the list specified. Example:

mylist = [1, 'two', 'xyz', 8.35, True]

mylist.insert(2, False)

print(mylist)


#output

[1, 'two', False, 'xyz', 8.35, True]


- append, add an item to the list at the end of list. Example:

mylist = [1, 'two', 'xyz', 8.35, True]

mylist.append('abc')

print(mylist)


#output

[1, 'two', 'xyz', 8.35, True, 'abc']


- remove, delete an item from list. Example:

mylist = [1, 'two', 'xyz', 8.35, True, 'abc']

mylist.remove('two')

print(mylist)


#output

[1, 'xyz', 8.35, True, 'abc']


- del, delete entire list. Example:

mylist = [1, 'xyz', 8.35, True, 'abc']

del mylist

print(mylist)


#output

Traceback (most recent call last):

File "<PATH>\test.py", line 3, in <module> print(mylist)

NameError: name 'mylist' is not defined. Did you mean: 'list'?


- clear, empty the list. Example:

mylist = [1, 'xyz', 8.35, True, 'abc']

mylist.clear()

print(mylist)


#output

[]


- pop, remove last item from list, and return it. Example:

mylist = [1, 'xyz', 8.35, True, 'abc']

last_item=mylist.pop()

print(last_item)

print(mylist)


#output

abc

[1, 'xyz', 8.35, True]


- copy, make a copy of list. Example:

mylist = [1, 'xyz', 8.35, True, 'abc']

listNo2 = mylist.copy()

print(listNo2)


[1, 'xyz', 8.35, True, 'abc']


- extend, add item of other list2 to the end of list1. Example:

list1 = [1, 'xyz', 8.35, True, 'abc']

list2 = ['bb', 25]

list1.extend(list2)

print(list1)


#output

[1, 'xyz', 8.35, True, 'abc', 'bb', 25]


- count, return the number of occurrences of item in list. Example:

mylist = [1, 'xyz', 8.35, True, 'xyz', 'abc']

nb=mylist.count('xyz')

print(nb)


#output

2


- index, return index of the first items with specified value. Example:

mylist = [1, 'xyz', 8.35, True, 'xyz', 'abc']

ni=mylist.index('xyz')

print(ni)


#output

1


- reverse, this method reverses the list items. Example:

mylist = [1, 'xyz', 8.35, True, 'xyz', 'abc']

mylist.reverse()

print(mylist)


#output

['abc', 'xyz', True, 8.35, 'xyz', 1]


- sort, this method sorts the items of list. Example: 

list1 = [5, 53, 7, 22, 18, 31]

list1.sort()

print(list1)


#output

[5, 7, 18, 22, 31, 53]

Observation: list items need to be sortable, means if we have a list with mixed items, numbers, strings, etc, it can't be sorted. Example:

mylist = [1, 'xyz', 8.35, True, 'xyz', 'abc']

mylist.sort()

print(mylist)


#output

Traceback (most recent call last):

File "<PATH>\test.py", line 2, in <module> mylist.sort()

TypeError: '<' not supported between instances of 'str' and 'int'


#-> error is expected 

Lists In Python – I

Lists are compound data types, items of lists are enclosed with [], square brackets and are separated by comma. 

A list definition:

mylist = [1, 'two', 'xyz', 8.35, True]

#printing list:

print(mylist)


#output: 

[1, 'two', 'xyz', 8.35, True]


We saw items in the list can be different data types, in our example: integer, string, float, boolean.


In some way, list is similar with arrays from C (C++, C#) hence items can be accessed based on index i.e. 

mylist = [1, 'two', 'xyz', 8.35, True]

print(mylist[0]) #first item from list have index 0

print(mylist[2])


#output:

1

xyz


Supplmentary, lists have slice operator, examples:

mylist = [1, 'two', 'xyz', 8.35, True]

print(mylist[0:3]) #it will print items 0, 1, 2 

print(mylist[2:])  #it will print items from 2 to the end

print(mylist[:3])  #it will print items from the biginning till item with index 3-1


#output: 

[1, 'two', 'xyz']

['xyz', 8.35, True]

[1, 'two', 'xyz']


Lists support -1 like index and it represent last item from list, example:

mylist = [1, 'two', 'xyz', 8.35, True]

print(mylist[-1])


#output:

True


+ Sign can be used to concatenate lists, example:

mylist = [1, 'two', 'xyz', 8.35, True]

thelist = [2, False, 'bb']

print(mylist+thelist)


#output:

[1, 'two', 'xyz', 8.35, True, 2, False, 'bb']


* Asterisk sign in list case is repetition operator, example: 

list2 = [2, True]

print(list2*2)

print(list2[1]*2)


#output:

[2, True, 2, True]