python-control_flow-iterations-functions
October 18, 2019
1 Python for Data Science
Control Flow, Iterations, Functions, Classes, Errors, …
2 Control Flow
• Without control flow, programs are sequences of statements
• With control flow you execute code
• conditionally (if, else)
• repeatedly (for, while)
2.1 Conditional Statements: if-elif-else:
[26]: x = inf
if x == 0:
print(x, "is zero")
elif x > 0:
print(x, "is positive")
elif x < 0:
print(x, "is negative")
else:
print(x, "is unlike anything I've ever seen...")
␣
,→---------------------------------------------------------------------------
NameError Traceback (most recent call␣
,→ last)
<ipython-input-26-87def6aed850> in <module>
----> 1 x = inf
2
3 if x == 0:
1
4 print(x, "is zero")
5 elif x > 0:
NameError: name 'inf' is not defined
2.2 for loops
• Iterate over each element of a collection
• Python makes this look like almost natural language:
for [each] value in [the] list
[ ]: for N in [2, 3, 5, 7]:
print(N, end=' ') # print all on same line
[ ]: for N in range(5):
print(N, end=' ') # print all on same line
2.3 while loops
Iterate until a condition is met
[ ]: i = 0
while i < 10:
print(i, end=' ')
i += 1
3 Functions
Remember the print statement
print('abc')
print is a function and 'abc' is an argument.
[27]: # multiple input arguments
print('abc','d','e','f','g')
abc d e f g
[28]: # keyword arguments
print('abc','d','e','f','g', sep='--')
2
abc--d--e--f--g
3.1 Defining Functions
[29]: def add(a, b):
"""
This function adds two numbers
Input
a: a number
b: another number
Returns sum of a and b
"""
result = a + b
return result
[30]: add(1,1)
[30]: 2
[31]: def add_and_print(a, b, print_result):
"""
This function adds two numbers
Input
a: a number
b: another number
print_result: boolean, set to true if you'd like the result printed
Returns sum of a and b
"""
result = a + b
if print_result:
print("Your result is {}".format(result))
return result
[32]: add_and_print(1, 1, True)
Your result is 2
[32]: 2
3
3.2 Default Arguments
[33]: def add_and_print(a, b, print_result=True):
"""
This function adds two numbers
Input
a: a number
b: another number
print_result: boolean, set to true if you'd like the result printed
Returns sum of a and b
"""
result = a + b
if print_result:
print("Your result is {}".format(result))
return result
[34]: add_and_print(1, 1)
Your result is 2
[34]: 2
3.3 *args and **kwargs: Flexible Arguments
[35]: def add_and_print(*args, **kwargs):
"""
This function adds two numbers
Input
a: a number
b: another number
print_result: boolean, set to true if you'd like the result printed
Returns sum of a and b
"""
result = 0
for number in args:
result += number
if 'print_result' in kwargs.keys() and kwargs['print_result']:
print("Your result is {}".format(result))
return result
[36]: add_and_print(1, 1, 1, print_result=True, unknown_argument='ignored')
4
Your result is 3
[36]: 3
[37]: list_of_numbers = [1,2,3,42-6]
add_and_print(*list_of_numbers)
[37]: 42
3.4 Anonymous (lambda) Functions
[38]: add = lambda x, y: x + y
add(1, 2)
[38]: 3
4 Classes
• Python is an object oriented language
• Classes provide a means of bundling data and functionality together
• Classes allow for inheriting functionality
[39]: class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def is_adult(self):
return self.age > 18
[40]: p1 = Person("John", 36)
print(p1.name)
print(p1.age)
print(p1.is_adult())
John
36
True
[41]: class Student(Person):
"""A class inheriting fields and methods from class Person"""
p2 = Student("Peter", 20)
5
p2.is_adult()
[41]: True
4.1 Some Convenient Special Functions
• Printing a String representation of an object: __repr__
• For calling an object: __call__
• Many more for specialized objects like iterables (just create an object and type .__ + <TAB>)
4.1.1 Nice String Representations of Objects with __repr__
[42]: # the string representation of the Person class is not very informative
p1
[42]: <__main__.Person at 0x10ed44210>
[43]: # defining a __repr__ function that returns a string can help
class PrintableStudent(Student):
def __repr__(self):
return f"A student with name {self.name} and age {self.age}"
p3 = PrintableStudent("Michael Mustermann", 25)
p3
[43]: A student with name Michael Mustermann and age 25
4.1.2 Clean APIs using __call__ for obvious usages of Objects
[44]: # defining a __call__ function can help to keep APIs simple
class CallableStudent(PrintableStudent):
def __call__(self, other_student):
print(f"{self.name} calls {other_student.name}")
p4 = CallableStudent("Michael Mustermann", 25)
p4(p2)
Michael Mustermann calls Peter
5 List Comprehensions
A simple way of compressing a list building for loop into single statement
6
[45]: L = []
for n in range(12):
L.append(n ** 2)
L
[45]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
[46]: [n ** 2 for n in range(12)]
[46]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
5.1 Conditional List Comprehensions
Including an if statement in list comprehensions
[47]: [n ** 2 for n in range(12) if n % 3 == 0]
[47]: [0, 9, 36, 81]
6 Set Comprehensions
Same as for lists, but for sets
[48]: {n**2 for n in range(12)}
[48]: {0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121}
7 Dict Comprehensions
Same as for lists, but for dictionaries
[49]: {n:n**2 for n in range(12)}
[49]: {0: 0,
1: 1,
2: 4,
3: 9,
4: 16,
5: 25,
6: 36,
7: 49,
8: 64,
9: 81,
7
10: 100,
11: 121}
8 Generator Comprehensions
Generators generate values one by one. More on this later.
[50]: (n**2 for n in range(12))
[50]: <generator object <genexpr> at 0x10e25be50>
[51]: # generators can be turned into lists
list((n**2 for n in range(12)))
[51]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
9 Iterators
• An object over which Python can iterate are called Iterators
• Iterators
– have a __next__ method that returns the next element
– have an __iter__ method that returns self
• The builtin function iter turns any iterable in an iterator
[52]: my_iterator = iter([1,2])
print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))
1
2
␣
---------------------------------------------------------------------------
,→
StopIteration Traceback (most recent call␣
last)
,→
<ipython-input-52-bdcbfa3d0082> in <module>
2 print(next(my_iterator))
3 print(next(my_iterator))
----> 4 print(next(my_iterator))
8
StopIteration:
9.1 Custom Iterators
[53]: class Squares(object):
def __init__(self, start, stop):
self.start = start
self.stop = stop
def __iter__(self): return self
def __next__(self):
if self.start >= self.stop:
raise StopIteration
current = self.start * self.start
self.start += 1
return current
iterator = Squares(1, 5)
[i for i in iterator]
[53]: [1, 4, 9, 16]
9.2 Useful Builtin Iterators
9.2.1 enumerate
Often you need not only the elements of a collection but also their index
[54]: L = [2, 4, 6]
for i in range(len(L)):
print(i, L[i])
0 2
1 4
2 6
Instead you can write
[55]: L = [2, 4, 6]
for idx, element in enumerate(L):
print(idx, element)
9
0 2
1 4
2 6
9.2.2 zip
Zips together two iterators
[56]: L = [2, 4, 6, 8, 10]
R = [3, 5, 7, 9, 11]
for l, r in zip(L, R):
print(l, r)
2 3
4 5
6 7
8 9
10 11
9.2.3 Unzipping with zip
An iterable of tuples can be unzipped with zip, too:
[57]: zipped = [('a',1), ('b',2)]
letters, numbers = zip(*zipped)
print(letters)
print(numbers)
('a', 'b')
(1, 2)
9.2.4 map
Applies a function to a collection
[58]: def power_of(x, y=2):
return x**2
for n in map(power_of, range(5)):
print(n)
0
1
4
9
16
10
9.2.5 filter
Filters elements from a collection
[59]: def is_even(x):
return x % 2 == 0
for n in filter(is_even, map(power_of, range(5))):
print(n)
0
4
16
[60]: # compressing the above for loop
print(*filter(is_even, map(power_of, range(5))))
0 4 16
9.3 Specialized Iterators: itertools
9.3.1 Permutations
Iterating over all permutations of a list
[61]: from itertools import permutations
my_iterator = range(3)
p = permutations(my_iterator)
print(*p)
(0, 1, 2) (0, 2, 1) (1, 0, 2) (1, 2, 0) (2, 0, 1) (2, 1, 0)
9.3.2 Combinations
Iterating over all unique combinations of N values within a list
[62]: from itertools import combinations
c = combinations(range(4), 2)
print(*c)
(0, 1) (0, 2) (0, 3) (1, 2) (1, 3) (2, 3)
9.3.3 Product
Iterating over all combinations of elements in two or more iterables
11
[63]: from itertools import product
my_iterator = range(3)
another_iterator = iter(['a', 'b'])
yet_another_iterator = iter([True, False])
p = product(my_iterator, another_iterator, yet_another_iterator)
print(*p)
(0, 'a', True) (0, 'a', False) (0, 'b', True) (0, 'b', False) (1, 'a', True) (1,
'a', False) (1, 'b', True) (1, 'b', False) (2, 'a', True) (2, 'a', False) (2,
'b', True) (2, 'b', False)
9.3.4 Chaining
Use Case: Chaining multiple iterators allows to combine file iterators
[64]: from itertools import chain
my_iterator = range(3)
another_iterator = iter(['a', 'b'])
yet_another_iterator = iter([True, False])
p = chain(my_iterator, another_iterator, yet_another_iterator)
print(*p)
0 1 2 a b True False
9.3.5 Chaining for Flattening
Turning a nested collection like [['a','b'],'c'] into a flat one like ['a','b','c'] is called
flattening
[65]: from itertools import chain
my_nested_list = [['a','b'],'c']
p = chain(*my_nested_list)
print(*p)
a b c
10 Generators - A Special Kind of Iterator
Generators make creation of iterators simpler.
Generators are built by calling a function that has one or more yield expression
[66]: def squares(start, stop):
for i in range(start, stop):
yield i * i
12
generator = squares(1, 10)
[i for i in generator]
[66]: [1, 4, 9, 16, 25, 36, 49, 64, 81]
11 When to use Iterators vs Generators
• Every Generator is an Iterator - but not vice versa
• Generator implementations can be simpler: python generator = (i*i for i in
range(a, b))
• Iterators can have rich state
12 Errors
Bugs come in three basic flavours:
• Syntax errors:
– Code is not valid Python (easy to fix, except for some whitespace things)
• Runtime errors:
– Syntactically valid code fails, often because variables contain wrong values
• Semantic errors:
– Errors in logic: code executes without a problem, but the result is wrong (difficult to
fix)
12.1 Runtime Errors
12.1.1 Trying to access undefined variables
[67]: # Q was never defined
print(Q)
␣
,→---------------------------------------------------------------------------
NameError Traceback (most recent call␣
last)
,→
<ipython-input-67-7ba079dc2063> in <module>
1 # Q was never defined
----> 2 print(Q)
13
NameError: name 'Q' is not defined
12.1.2 Trying to execute unsupported operations
[76]: 1 + 'abc'
␣
,→---------------------------------------------------------------------------
TypeError Traceback (most recent call␣
last)
,→
<ipython-input-76-a51a3635a212> in <module>
----> 1 1 + 'abc'
TypeError: unsupported operand type(s) for +: 'int' and 'str'
12.1.3 Trying to access elements in collections that don’t exist
[ ]: L = [1, 2, 3]
L[1000]
12.1.4 Trying to compute a mathematically ill-defined result
[ ]: 2 / 0
12.2 Catching Exceptions: try and except
[77]: try:
print("this gets executed first")
except:
print("this gets executed only if there is an error")
this gets executed first
[78]: try:
print("let's try something:")
x = 1 / 0 # ZeroDivisionError
except:
14
print("something bad happened!")
let's try something:
something bad happened!
[79]: def safe_divide(a, b):
"""
A function that does a division and returns a half-sensible
value even for mathematically ill-defined results
"""
try:
return a / b
except:
return 1E100
[80]: print(safe_divide(1, 2))
print(safe_divide(1, 0))
0.5
1e+100
12.2.1 What about errors that we didn’t expect?
[81]: safe_divide (1, '2')
[81]: 1e+100
12.2.2 It’s good practice to always catch errors explicitly:
All other errors will be raised as if there were no try/except clause.
[82]: def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return 1E100
[83]: safe_divide(1, '2')
␣
,→---------------------------------------------------------------------------
TypeError Traceback (most recent call␣
,→last)
15
<ipython-input-83-cbb3eb91a66d> in <module>
----> 1 safe_divide(1, '2')
<ipython-input-82-57f0d324952e> in safe_divide(a, b)
1 def safe_divide(a, b):
2 try:
----> 3 return a / b
4 except ZeroDivisionError:
5 return 1E100
TypeError: unsupported operand type(s) for /: 'int' and 'str'
12.3 Throwing Errors
• When your code is executed, make sure that it’s clear what went wrong in case of errors.
• Throw specific errors built into Python
• Write your own error classes
[84]: raise RuntimeError("my error message")
␣
---------------------------------------------------------------------------
,→
RuntimeError Traceback (most recent call␣
last)
,→
<ipython-input-84-b1834d213d3b> in <module>
----> 1 raise RuntimeError("my error message")
RuntimeError: my error message
12.4 Specific Errors
[ ]: def safe_divide(a, b):
if (not issubclass(type(a), float)) or (not issubclass(type(b), float)):
raise ValueError("Arguments must be floats")
try:
return a / b
except ZeroDivisionError:
return 1E100
16
[ ]: safe_divide(1, '2')
12.5 Accessing Error Details
[85]: import warnings
def safe_divide(a, b):
if (not issubclass(type(a), float)) or (not issubclass(type(b), float)):
raise ValueError("Arguments must be floats")
try:
return a / b
except ZeroDivisionError as err:
warnings.warn("Caught Error {} with message {}".format(type(err),err) +
" - will just return a large number instead")
return 1E100
[86]: safe_divide(1., 0.)
/Users/felix/anaconda3/envs/pdds_1920/lib/python3.7/site-
packages/ipykernel_launcher.py:10: UserWarning: Caught Error <class
'ZeroDivisionError'> with message float division by zero - will just return a
large number instead
# Remove the CWD from sys.path while we load stuff.
[86]: 1e+100
13 Loading Modules: the import Statement
• Explicit imports (best)
• Explicit imports with alias (ok for long package names)
• Explicit import of module contents
• Implicit imports (to be avoided)
13.1 Creating Modules
• Create a file called [somefilename].py
• In a (i)python shell change dir to that containing dir
• type
import [somefilename]
Now all classes, functions and variables in the top level namespace are available.
17
Let’s assume we have a file mymodule.py in the current working directory with the content:
mystring = 'hello world'
def myfunc():
print(mystring)
[87]: import mymodule
mymodule.mystring
[87]: 'hello world'
[88]: mymodule.myfunc()
hello world
13.2 Explicit module import
Explicit import of a module preserves the module’s content in a namespace.
[89]: import math
math.cos(math.pi)
[89]: -1.0
13.3 Explicit module import with aliases
For longer module names, it’s not convenient to use the full module name.
[90]: import numpy as np
np.cos(np.pi)
[90]: -1.0
13.4 Explicit import of module contents
You can import specific elements separately.
[91]: from math import cos, pi
cos(pi)
[91]: -1.0
18
13.5 Implicit import of module contents
You can import all elements of a module into the global namespace. Use with caution.
[92]: cos = 0
from math import *
sin(pi) ** 2 + cos(pi) ** 2
[92]: 1.0
14 File IO and Encoding
• Files are opened with open
• By default in 'r' mode, reading text mode, line-by-line
14.1 Reading Text
[93]: path = 'umlauts.txt'
f = open(path)
lines = [x.strip() for x in f]
f.close()
lines
[93]: ['Eichhörnchen', 'Flußpferd', '', 'Löwe', '', 'Eichelhäher']
[94]: # for easier cleanup
with open(path) as f:
lines = [x.rstrip() for x in f]
lines
[94]: ['Eichhörnchen', 'Flußpferd', '', 'Löwe', '', 'Eichelhäher']
14.2 Detour: Context Managers
Often, like when opening files, you want to make sure that the file handle gets closed in any case.
file = open(path, 'w')
try:
# an error
1 / 0
finally:
file.close()
Context managers are a convenient shortcut:
19
with open(path, 'w') as opened_file:
# an error
1/0
14.3 Writing Text
[95]: with open('tmp.txt', 'w') as handle:
handle.writelines(x for x in open(path) if len(x) > 1)
[x.rstrip() for x in open('tmp.txt')]
[95]: ['Eichhörnchen', 'Flußpferd', 'Löwe', 'Eichelhäher']
14.4 Reading Bytes
[96]: # remember 't' was for text reading/writing
with open(path, 'rt') as f:
# just the first 6 characters
chars = f.read(6)
chars
[96]: 'Eichhö'
[97]: # now we read the file content as bytes
with open(path, 'rb') as f:
# just the first 6 bytes
data = f.read(6)
[98]: # byte representation
data.decode('utf8')
␣
,→---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call␣
last)
,→
<ipython-input-98-9981ac9de387> in <module>
1 # byte representation
----> 2 data.decode('utf8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 in position 5:␣
unexpected end of data
,→
20
[99]: # decoding error, utf-8 has variable length character encodings
data[:4].decode('utf8')
[99]: 'Eich'
21