Lecture ten - Python - continuation
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.
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)
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.
Essentially a Python module is a script, so it can be run as a script:
python single.py
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 :
'c'
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).
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).
Assigments in python :
>>> colours1 = ["red", "green"]
>>> colours2 = colours1
>>> colours2[1] = "blue"
>>> colours1
Output :
['red', 'blue']
Assignment statements in Python do not copy objects, they create bindings between a target and an object.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']]
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']]
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
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])
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.
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]
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()
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 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]
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]
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}
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
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 :
{'house': 'Haus', 'blau': 'bleu', 'red': 'rouge', 'cat': 'Katze'}
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']
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'}
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'}
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'
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!
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].
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
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)
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]
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]
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
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
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.
As seen from the above example, we have a closure in Python when a nested function references a value in its enclosing scope.
Closures can avoid the use of global values and provides some form of data hiding.
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
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.
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:
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.")
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.
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