Foundations of programming

Lecture ten - Python - continuation

Work Plan

  1. Introduction, types of programming languages, pseudocode
  2. Variables, data types
  3. Simple instructions and tricks
  4. Pointers and arrays
  5. Functions and Procedures
  6. Input / Output
  7. Data Structures
  8. Modern C++
  9. Error Types /Debug
  10. Python
  11. Python II
  12. Object Oriented / Functional programming
  13. Software development in XXI century
  14. Final exam / Test

Welcome to 2020

image

Modules

image

Modules

Modular design means that a complex system is broken down into smaller parts or components, i.e. modules. There is hardly any product nowadays, which doesn't heavily rely on modularisation, like cars, mobile phones.

These components can be independently created and tested.

In many cases, they can be even used in other systems as well.

Modules

If you import a module in Python using "import abc" the interpreter searches for this module in the following locations and in the order given:
The directory of the top-level file, i.e. the file being executed.
The directories of PYTHONPATH, if this global variable is set.
Standard installation path Linux/Unix e.g. in /usr/lib/python3.5.

Modules

A module in Python is just a file containing Python definitions and statements.

The module name is made from the file name by removing the suffix .py. For example, if the file name is fibonacci.py, the module name is fibonacci.

Example, save code in file named fibonnaci.py

def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return f

Execution:

import fibonacci

fibonacci.fib(7)

Modules

Usually, modules contain functions or classes, but there can be "plain" statements in them as well.

These statements can be used to initialize the module. They are only executed when the module is imported.

Save a module as single.py with a single line

print("The module is imported now!")

We save with the name "one_time.py" and import it two times in the interpreter session:

>>> import single
The module is imported now!
>>> import single
>>>

We can see that it was only imported once. Each module can only be imported once per interpreter session or in a program or script.

Modules

Essentially a Python module is a script, so it can be run as a script:

python single.py 
There are different kind of modules:
Those written in Python - They have the suffix: .py
Dynamically linked C modules - Suffixes are: .dll, .pyd, .so, .sl, ...
C-Modules linked with the Interpreter (if the main program (the Python interpreter) is compiled and linked by the C compiler)

Immutable

image

Lists

Lists in Python can have various data types:

group = ["Bob", 23, "George", 72, "Myriam", 29]

Lists can have sublists as elements. These sublists may contain sublists as well, i.e. lists can be recursively constructed by sublist structures.

>>> complex_list = [["a",["b",["c","x"]]]]
>>> complex_list = [["a",["b",["c","x"]]],42]
>>> complex_list[0][1]
['b', ['c', 'x']]
>>> complex_list[0][1][1][0]

Output :

Immutable again

As we discussed before in Python we have immutable types (simple types) and mutable (list,dict,set,bytearray,user-defined classes).

string_build = ""
for data in container:
    string_build += str(data)

Because strings are immutable, concatenating two strings together actually creates a third string which is the combination of the previous two (every time you perform addition).

Mutable

We can use the list to obtain the same result.

builder_list = []
for data in container:
    builder_list.append(str(data))
"".join(builder_list)

This code takes advantage of the mutability of a single list object to gather your data together and then allocate a single result string to put your data in (only single new string is created).

Copies of objects

Assigments in python :

>>> colours1 = ["red", "green"]
>>> colours2 = colours1
>>> colours2[1] = "blue"
>>> colours1

Output :

Copies of objects

It's possible to make sth which is called shallow list copy

>>> list1 = ['a','b','c','d']
>>> list2 = list1[:]
>>> list2[1] = 'x'
>>> print list2
['a', 'x', 'c', 'd']
>>> print list1
['a', 'b', 'c', 'd']

it is shallow because for example this does not work for sublists:

>>> lst1 = ['a','b',['ab','ba']]
>>> lst2 = lst1[:]
>>> lst2[0] = 'c'
>>> lst2[2][1] = 'd'
>>> print(lst1)
['a', 'b', ['ab', 'd']]

Shallow and deep copy

In order to make deep and shallow copies you should use module copy.
A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.
A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.

Input:

from copy import *

lst1 = ['a','b',['ab','ba']]

lst2 = deepcopy(lst1)
lst3 = copy(lst1)

lst2[2][0] = "d2"
lst3[2][1] = "d3"
lst2[0] = "c2";
lst3[0] = "c3";

lst1
lst2
lst3

Output:

['a', 'b', ['ab', 'd3']]
['c2', 'b', ['d2', 'ba']]
['c3', 'b', ['ab', 'd3']]

Stuff to deal with

Another pitfall related to mutability is the following scenario:

def my_function(param=[]):
    param.append("thing")
    return param

my_function() # returns ["thing"]
my_function() # returns ["thing", "thing"]

What you might think would happen is that by giving an empty list as a default value to param, a new empty list is allocated each time the function is called and no list is passed in. But what actually happens is that every call that uses the default list will be using the same list.

This is because Python (a) only evaluates functions definitions once, (b) evaluates default arguments as part of the function definition, and (c) allocates one mutable list for every call of that function.

Not pretty workaround:

def my_function2(param=None):
    if param is None:
        param = []
    param.append("thing")
    return param

Tuple

Tuples are immutable collections in Python and are defined by parentheses

our_tuple = (5, 6, 7, 8)

# We can index them, just like strings
our_tuple[2]
>>> 7

# And iterate through them:
for item in our_tuple:
    print item
>>> 5
>>> 6
>>> 7
>>> 8

Iterate through tuple:

for index in range(len(new_tuple)):
    print("Index is:", index)
    print("Value at that index is:", our_tuple[index])

Tuple

We can also do something called tuple unpacking

new_tuple = (5, 6, 7, 8)
(a, b, c, d) = new_tuple
print ("a is:", a)
a is: 5
print ("b is:", b)
b is: 6
print ("c is:", c)
c is: 7
print ("d is:", d)
d is: 8

Make sure that you always have the same number of variables when you unpack a tuple!

Tuples are immutable. To change a tuple, we would need to first unpack it, change the values, then repack it.

Data Structures

image

Lists

Lists are mutable collections in Python! So, if we want we can change the value of one element

new_list = [0,1,2]
new_list[2] = 100
print("new_list is:", new_list)

>> new_list is: [0, 1, 100]

# Or, add on a new element with append:
new_list.append(87)
print("new_list is:", new_list)

>> new_list is: [0, 1, 100, 87]


# Or insert
new_list.insert(0, 200) # insert at position 0 the element 200
print("new_list is:", new_list)

>> new_list is: [200, 0, 1, 100, 87]

# Or even delete an element using remove
new_list.remove(100) # Write in the item that you want to remove from the list
print("new_list is:", new_list)

>> new_list is: [200, 0, 1, 87]

Lists

There are following methods which can be executed on list:

l.append()
l.count()
l.extend()
l.index()
r.insert()
r.pop()
r.remove()
r.reverse()
r.sort()

The ‘in’ Operator

Operator in is boolean test whether a value is inside a collection (often called a container in Python):

t = [1, 2, 4, 5]
3 in t
4 in t
4 not in t

Output:

False
True
False

For strings, it tests for substrings.

Be careful: the in keyword is also used in the syntax of for loops and list comprehensions.

List Comprehensions

List comprehension is a mechanism similar to math notation.

S = {x^2 : x in {0 ... 9}}

Generate a new list by applying a function to every member of an original list.

[ expression for name in list ]

li = [3, 6, 2, 7]
[ elem *2 for elem in li]

Output:

[6, 12, 4, 14]

If the elements of list are other collections, then name can be replaced by a collection of names that match the “shape” of the list members.

li = [(‘a’, 1), (‘b’, 2), (‘c’, 7)]
[ n * 3 for (x, n) in li ]

Output:

[3, 6, 21]

Filtered comprehensions

Filter determines whether expression is performed on each member of the list.

When processing each element of list, first check if it satisfies the filter condition.

If the filter condition returns False, that element is omitted from the list before the list comprehension is evaluated.

li = [3, 6, 2, 7, 1, 9]
[ elem * 2 for  elem in  li  if elem > 4]

Output:

[12, 14, 18]

Dictionary

Dictionaries are also like lists, except that each element is a key-value pair.

They are like a map container in C++.

The syntax for dictionaries is {key1 : value1}

params = {"parameter1" : 1.0,
        "parameter2" : 2.0,
        "parameter3" : 3.0}

print(type(params))
print(params)
params["parameter1"] = "A"
params["parameter2"] = "B"

Output:

<class 'dict'>
{'parameter1': 1.0, 'parameter2': 2.0, 'parameter3': 3.0}    

Dictionary

No special method is needed to iterate over a dictionary:

for key in d:
    print key

But it's possible to use the method iterkeys():

for key in d.iterkeys():
    print key

The method itervalues() is a convenient way for iterating directly over the values:

for val in d.itervalues():
    print val

Dictionary

The update method merges the keys and values of one dictionary into another, overwriting values of the same key:

>>> w={"house":"Haus","cat":"Katze","red":"rot"}
>>> w1 = {"red":"rouge","blau":"bleu"}
>>> w.update(w1)
>>> print w

Output :

List from Dictionaries

It's possible to create lists from dictionaries by using the methods items(), keys() and values().

>>> w={"house":"Haus","cat":"Katze","red":"rot"}
>>> w.items()
[('house', 'Haus'), ('red', 'rot'), ('cat', 'Katze')]
>>> w.keys()
['house', 'red', 'cat']
>>> w.values()
['Haus', 'rot', 'Katze']    

Dictionaries from List

We have two lists, one containing the dishes and the other one the corresponding countries:

>>> dishes = ["pizza", "sauerkraut", "paella", "Hamburger"]
>>> countries = ["Italy", "Germany", "Spain", "USA"]

Now we will create a dictionary, which assigns a dish to a country. For this purpose we need the function zip().

>>> country_specialities = zip(countries, dishes)
>>> print country_specialities
[('Italy', 'pizza'), ('Germany', 'sauerkraut'), ('Spain', 'paella'), ('USA', 'Hamburger')]

The variable country_specialities contains now the "dictionary" in the 2-tuple list form. This form can be easily transformed into a real dictionary with the function dict().

>>> country_specialities_dict = dict(country_specialities)
>>> print country_specialities_dict
{'Germany': 'sauerkraut', 'Spain': 'paella', 'Italy': 'pizza', 'USA': 'Hamburger'}

Set

The data type "set", which is a collection type, has been part of Python since version 2.4. A set contains an unordered collection of unique and immutable objects.

If we want to create a set, we can call the built-in set function with a sequence or another iterable object.

>>> x = set("A Python Tutorial")
>>> x
set(['A', ' ', 'i', 'h', 'l', 'o', 'n', 'P', 'r', 'u', 't', 'a', 'y', 'T'])
>>> type(x)
<type 'set'>

We can define sets (since Python2.6) without using the built-in set function. We can use curly braces instead:

x = {"A","b","C"}
print(x)
y = {"Abc"}
print(y)
z = set(["asd","ads"])
print(z)
{'A', 'b', 'C'}
{'Abc'}
{'asd', 'ads'}

Set

Sets are implemented in a way, which doesn't allow mutable objects. The following example demonstrates that we cannot include for example lists as elements:

>>> cities = set((("Python","Perl"), ("Paris", "Berlin", "London")))


>>> cities = set((["Python","Perl"], ["Paris", "Berlin", "London"]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
Set Operations:
add(element)
clear()
copy()
difference(set)
discard(element)
remove(element)
intersection(s)
issubset()
issuperset()
pop() / remove an element of the sets

Matrix

Matrix is a two-dimensional table. In python, matrix is a nested list. A list is created by placing all the items (elements) inside a square bracket [ ], separated by commas.

# a is 2-D matrix with integers
a = [['Roy',80,75,85,90,95],
    ['John',75,80,75,85,100],
    ['Dave',80,80,80,90,95]]

#b is a nested list but not a matrix
b= [['Roy',80,75,85,90,95],
    ['John',75,80,75],
    ['Dave',80,80,80,90,95]]

Size of the list matter here!

Create matrix in a loop

A possible way: you can create a matrix of n*m elements by first creating a list of n elements (say, of n zeros) and then make each of the elements a link to another one-dimensional list of m elements:

n = 3
m = 4
a = [0] * n
for i in range(n):
    a[i] = [0] * m
print(a)
>> [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

Similar to list we can access elements of a matrix by using square brackets [] after the variable like a[row][col].

Negative indexing

Python allows negative indexing for sequences.

Example:

lst1 = ['Ajay', 'Bobby','Ashok', 'Vijay', 'Anil', 'Rahul','Alex', 'Christopher']
print(lst1[-1])
print(lst1[-2])
print(lst1[-3])

Output:

Christopher
Alex
Rahul

Comparing sequences

Sequence objects may be compared to other objects with the same sequence type.

The comparison uses lexicographical ordering: first the first two items are compared, and if they differ this determines the outcome of the comparison; if they are equal, the next two items are compared, and so on, until either sequence is exhausted.

If two items to be compared are themselves sequences of the same type, the lexicographical comparison is carried out recursively. If all items of two sequences compare equal, the sequences are considered equal.

If one sequence is an initial sub-sequence of the other, the shorter sequence is the smaller (lesser) one.

Examples:

(1, 2, 3)              < (1, 2, 4)
[1, 2, 3]              < [1, 2, 4]
'ABC' < 'C' < 'Pascal' < 'Python'
(1, 2, 3, 4)           < (1, 2, 4)
(1, 2)                 < (1, 2, -1)
(1, 2, 3)             == (1.0, 2.0, 3.0)
(1, 2, ('aa', 'ab'))   < (1, 2, ('abc', 'a'), 4)

Functions

image

Call by value/references

Python uses a mechanism, which is known as "Call-by-Object", sometimes also called "Call by Object Reference" or "Call by Sharing".

If we pass a list to a function, we have to consider two cases: Elements of a list can be changed in place, i.e. the list will be changed even in the caller's scope. If a new list is assigned to the name, the old list will not be affected, i.e. the list in the caller's scope will remain untouched.

def func1(list):
    print list
    list = [47,11]
    print list
tab = [0,1,2]
func1(fib)
print(fib)

Output :

[0, 1, 2]
[47, 11]

[0, 1, 2]

Call by value/references

This changes drastically, if we include something in the list by using +=. To show this, we have a different function func2() in the following example:

def func2(list):
    print list
    list += [47,11]
    print list

tab = [0,1,2]
func1(tab)
print(tab)

Output :

[0, 1, 2]
[0, 1, 2, 47, 11]

[0, 1, 2, 47, 11]

Functions

Remainder: function names are references to functions and in Python we can assign multiple names to the same function.

def foo():
    return

baz = bar = foo

In pyhton we can define functions inside of a function:

def f():

    def g():
        print("Hi, it's me 'g'")
        print("Thanks for calling me")

    print("This is the function 'f'")
    print("I am calling 'g' now:")
    g()


f()

Output:

This is the function 'f'
I am calling 'g' now:
Hi, it's me 'g'
Thanks for calling me

Functions as parameters

Due to the fact that every parameter of a function is a reference to an object and functions are objects as well, we can pass functions - or better "references to functions" - as parameters to a function.

def g():
    print("Hi, it's me 'g'")
    print("Thanks for calling me")

def f(func):
    print("Hi, it's me 'f'")
    print("I will call 'func' now")
    func()

f(g)

Output:

Hi, it's me 'f'
I will call 'func' now
Hi, it's me 'g'
Thanks for calling me

Closures

A function defined inside another function is called a nested function.

def print_msg(msg):
# This is the outer enclosing function

    def printer():
    # This is the nested function
        print(msg)

    return printer  

another = print_msg("Hello")
another()

The print_msg() function was called with the string "Hello" and the returned function was bound to the name another. On calling another(), the message is still remembered although we had already finished executing the print_msg() function.

This technique by which some data ("Hello") gets attached to the code is called closure in Python.

Closure

As seen from the above example, we have a closure in Python when a nested function references a value in its enclosing scope.

The criteria that must be met to create closure in Python are summarized in the following points.
We must have a nested function (function inside a function).
The nested function must refer to a value defined in the enclosing function.
The enclosing function must return the nested function.

Closures can avoid the use of global values and provides some form of data hiding.

More practical?

Encode polynomial of the form a*x^2 + b*x + c in a function.

def polynomial_creator(a, b, c):
    def polynomial(x):
        return a * x**2 + b * x + c
    return polynomial

p1 = polynomial_creator(2, 3, -1)
p2 = polynomial_creator(-1, 2, 1)

for x in range(-2, 2, 1):
    print(x, p1(x), p2(x))

Output:

-2 1 -7
-1 -2 -2
0 -1 1
1 4 2

Exceptions

image

Exceptions

An exception is an event which occurs during the execution of a program and disrupts the normal flow of the program’s instructions.

Like many languages, in Python, a try statement is available for exception handling.

try:
    You do your operations here;
    ......................
except ExceptionI:
    If there is ExceptionI, then execute this block.
except ExceptionII:
    If there is ExceptionII, then execute this block.
    ......................
else:
    If there is no exception then execute this block.

A single try statement can have multiple except statements depending on the requirement. In this case, a try block contains statements that can throw different types of exceptions.

We can also add a generic except clause which can handle all possible types of exceptions.

Exceptions

With try block, we also have the option to define the “finally” block. This clause allows defining statements that we want to execute, no matters whether the try block has raised an exception or not.

try:
    You do your operations here;
    ......................
    Due to any exception, this may be skipped.
finally:
    This would always be executed.
    ......................

Example

Example:

try:
    x = float(input("Your number: "))
    inverse = 1.0 / x
except ValueError:
    print("You should have given either an int or a float")
except ZeroDivisionError:
    print("Infinity")
except:
    print("Unexpected error:", sys.exc_info()[0])
finally:
    print("There may or may not have been an exception.")

Raise Exception

We can forcefully raise an exception using the raise keyword. We can also optionally pass values to the exception and specify why this exception should occur. Here is the syntax for calling the “raise” method.

raise [Exception [, args [, traceback]]]

where,

The exception is the name of the exception.
The “args” is optional and represents the value of the exception argument.
The final argument, traceback, is also optional and if present, is the traceback object used for the exception.

References

https://www.python-course.eu/python3_re.php

https://codehabitude.com/2013/12/24/python-objects-mutable-vs-immutable/

https://www.python-course.eu/python3_dictionaries.php

https://www.python-course.eu/python3_deep_copy.php

https://www.python-course.eu/python3_sequential_data_types.php

https://www.python-course.eu/python3_sets_frozensets.php

https://www.python-course.eu/python3_decorators.php

https://www.programiz.com/python-programming/closure

https://www.programiz.com/python-programming/matrix

http://www.techbeamers.com/python-try-except-beginners/

https://www.python-course.eu/python3_modules_and_modular_programming.php

https://docs.python.org/3/tutorial/datastructures.html#looping-techniques