Exercise session 1

print((2 ** 63) % 13)
8
import math
print(math.pi ** math.e > math.e ** math.pi)
False
d = float(input("diameter ? "))
area = math.pi * d**2 / 4.0
circumference = math.pi * d
print("the circle has area", area, "and circumference", circumference)
diameter ? 1.111
the circle has area 0.9694334464429017 and circumference 3.4903094381382602
import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

This is a list of core principles the Python language follows and also serves as a recommendation for Python developers.

Although that way may not be obvious at first unless you're Dutch is a reference to Guido van Rossum, the inventor of Python who is dutch and developed the first version of Python when being bored over christmas.

Close to Readability counts. the sentence you read code more often than you write it if often cited within the Python community making a clear point that readable code is important.

In the face of ambiguity, refuse the temptation to guess. together with Errors should never pass silently. implicate that your code should not try to correct invalid input, usually it is better to report an error back to the user.

Optional exercises

p = float(input("p = ? "))
q = float(input("q = ? "))

d = math.sqrt(p**2 / 4.0 - q)
x1 = - p / 2.0 + d
x2 = - p / 2.0 - d

print(x1, x2)
p = ? 2
q = ? 1
-1.0 -1.0
import cmath

p = float(input("p = ? "))
q = float(input("q = ? "))

d = cmath.sqrt(p**2 / 4.0 - q)
x1 = - p / 2.0 + d
x2 = - p / 2.0 - d

print(x1, x2)
p = ? 1
q = ? 1
(-0.5+0.8660254037844386j) (-0.5-0.8660254037844386j)
import math

x = 1e300
print(math.hypot(x, x))
print(math.sqrt(x ** 2 + x ** 2))
1.4142135623730952e+300
---------------------------------------------------------------------------
OverflowError                             Traceback (most recent call last)
<ipython-input-1-68cab9427697> in <module>()
      3 x = 1e300
      4 print(math.hypot(x, x))
----> 5 print(math.sqrt(x ** 2 + x ** 2))

OverflowError: (34, 'Result too large')

The "manual" implementation overflows when computing the intermediate value x ** 2 although the result is still in the available range for float values:

print(x ** 2)
---------------------------------------------------------------------------
OverflowError                             Traceback (most recent call last)
<ipython-input-6-2b2533ba49a1> in <module>()
----> 1 print(x ** 2)

OverflowError: (34, 'Result too large')

If you rewrite $\sqrt{a^2 + b^2}$ as $a \sqrt{1 + \frac{b}{a}^2}$ under the assumption $b < a$ the value in the square root is between $0$ and $2$. (If $b>a$ just swap $a$ and $b$).

In our case we have $a = b = x$:

print(x * math.sqrt(1 + 1))
1.4142135623730952e+300

Take home message: Equivalent mathematical formulas can behave differently when implemented for a computer. This affects:

  • overflow / underflow
  • numerical precission
  • error propagation

Exercise session 2

the .rstrip(..) method removes charachters from the right side of a string. Without an argument it removes so called "white spaces" which are "symbols" you actually don't see as characters in the output. Thus this include spaces, tabs and line breaks:

print("abcd \n\t  ".rstrip() + "!")
abcd!

Or if you provide an argument as follows the characters from the argument are stripped:

"abcdedee".rstrip("de")
'abc'

Python has also lstrip for stripping from the left side, and strip for both sides:

"   abcde  ".strip() + "!"
'abcde!'

Exercise session 3

import math

def area_and_circumference(d):
    area = math.pi * d**2 / 4.0
    circumference = math.pi * d
    return area, circumference

d = 1.0
area, circumference = area_and_circumference(d)
print("the circle with diameter {d:.2f} has area {a:.4f} and circumference {c:.4f}"
      .format(d=d, a=area, c=circumference))
the circle with diameter 1.00 has area 0.7854 and circumference 3.1416
def product(a, b=1, c=1):
    return a * b * c

print(product(2))
print(product(2, 3))
print(product(2, 3, 4))
2
6
24

avg_at_1_2 is an example, that you also can pass functions to functions. This can be used to implement a function which determines zeros or maxima of a given function.

compose is a function which takes two functions and returns a new function. The returend function evaluates an argument x as f(g(x)).

Such functions opearting on functions and maybe returning functions are also called "higher level functions".

Another example:

def double_result(f):
    def modified_function(x):
        return 2 * f(x)
    return modified_function

def inc(x):
    return x + 1

double_inc = double_result(inc)

# double_inc is now the "modified_function" using f=inc which is 
# created within "double_result"
# calling double_inc(x) is the same as modified_function(x)
# thus it will return 2 * inc(1)

print(double_inc(1))
4

Exercise block 4

def double_if_even(n):
    if n % 2 == 0:
        return 2 * n
    return n

print(double_if_even(4))
print(double_if_even(2))
8
4
def check_number(n):
    if n % 3 == 0 and n % 4 == 0:
        print("{} is a multiple of 3 and 4".format(n))
    elif n % 3 == 0:
        print("{} is a multiple of 3 but not of 4".format(n))
    elif n % 4 == 0:
        print("{} is a multiple of 4 but not of 4".format(n))
    else:
        print("{} is a neither a multiple of 3 nor of 4".format(n))
        
check_number(12)
check_number(8)
check_number(6)
check_number(7)
12 is a multiple of 3 and 4
8 is a multiple of 4 but not of 4
6 is a multiple of 3 but not of 4
7 is a neither a multiple of 3 nor of 4

Recursion: compute computes the product of the first n natural numbers, 1 * 2 * ... * n.

For adding, we here also introduce a one line version of "if/else":

def sumup(n):
    assert n>=0, "only works for arguments >= 0"
    return 0 if n == 0 else n + sumup(n - 1)

sumup(3)
6
sumup(-1)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-7-d66d8ca199fa> in <module>()
----> 1 sumup(-1)

<ipython-input-6-0750150d6abe> in sumup(n)
      1 def sumup(n):
----> 2     assert n>=0, "only works for arguments >= 0"
      3     return 0 if n == 0 else n + sumup(n - 1)
      4 
      5 sumup(3)

AssertionError: only works for arguments >= 0

Exercise block 5

def average(numbers):
    if len(numbers) == 0:
        return None  # average of empty list is not defined
    
    sum_ = 0.0
    for number in numbers:
        sum_ += number
        
    # an empty list would trigger a "DivisionByZero" error:
    return sum_ / len(numbers)

print(average([]))
print(average([1, 2, 3]))
None
2.0

Btw: there is a builtin function sum:

def average(numbers):
    if len(numbers) == 0:
        return None  # average of empty list is not defined
    return sum(numbers) / len(numbers)

print(average([]))
print(average([1, 2, 3]))
None
2.0
def average_of_even_numbers(numbers):
    count = 0
    sum_ = 0.0
    for number in numbers:
        if number % 2 == 0:
            count += 1
            sum_ += number
    if count > 0:
        return sum_ / count
    return None   # averag of empty list is not defined

print(average_of_even_numbers([]))
print(average_of_even_numbers([1, 2, 3, 4, 5, 6]))
None
4.0

Optional exercises

import random

def number_guessing_game(upper_limit=100):
    secret = random.randint(0, upper_limit)
    while True:
        guess = int(input("your guess ? "))
        if guess == secret:
            print("you got it")
            break
        elif guess > secret:
            print("your guess is too high")
        else:
            print("your guess is too low")
        
number_guessing_game()
your guess ? 50
your guess is too high
your guess ? 25
your guess is too high
your guess ? 12
your guess is too high
your guess ? 6
your guess is too low
your guess ? 9
you got it
def is_prime(n):
    i = 2
    while i * i <= n:
        if n % i == 0:
            return False
        i += 1
    return True

print(is_prime(49))
print(is_prime(79))
False
True

Excercise block 6

def squares_of_odd_numbers(numbers):
    result = []
    for number in numbers:
        if number % 2 == 1:
            result.append(number ** 2)
    return result

print(squares_of_odd_numbers([1, 2, 3, 4, 5]))
[1, 9, 25]
def fib(n):
    if n == 1:
        return [1]
    result = [1, 1]
    while len(result) < n:
        result.append(result[-1] + result[-2])
        
    return result

print(fib(1))
print(fib(11))
[1]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Exercises block 7

num_to_square = {}
for i in range(1, 101):  # usually iteration starts with 0, upper limits are always exclusive
    num_to_square[i] = i * i
    
print(num_to_square)
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100, 11: 121, 12: 144, 13: 169, 14: 196, 15: 225, 16: 256, 17: 289, 18: 324, 19: 361, 20: 400, 21: 441, 22: 484, 23: 529, 24: 576, 25: 625, 26: 676, 27: 729, 28: 784, 29: 841, 30: 900, 31: 961, 32: 1024, 33: 1089, 34: 1156, 35: 1225, 36: 1296, 37: 1369, 38: 1444, 39: 1521, 40: 1600, 41: 1681, 42: 1764, 43: 1849, 44: 1936, 45: 2025, 46: 2116, 47: 2209, 48: 2304, 49: 2401, 50: 2500, 51: 2601, 52: 2704, 53: 2809, 54: 2916, 55: 3025, 56: 3136, 57: 3249, 58: 3364, 59: 3481, 60: 3600, 61: 3721, 62: 3844, 63: 3969, 64: 4096, 65: 4225, 66: 4356, 67: 4489, 68: 4624, 69: 4761, 70: 4900, 71: 5041, 72: 5184, 73: 5329, 74: 5476, 75: 5625, 76: 5776, 77: 5929, 78: 6084, 79: 6241, 80: 6400, 81: 6561, 82: 6724, 83: 6889, 84: 7056, 85: 7225, 86: 7396, 87: 7569, 88: 7744, 89: 7921, 90: 8100, 91: 8281, 92: 8464, 93: 8649, 94: 8836, 95: 9025, 96: 9216, 97: 9409, 98: 9604, 99: 9801, 100: 10000}

Optional exercises block 7

def invert_unique(d):
    inverted = {}
    for k in d.keys():
        v = d[k]
        inverted[v] = k
    return inverted

print(invert_unique({1:2, 3:4}))      
{2: 1, 4: 3}
def invert_general(d):
    inverted = {}
    for k in d.keys():
        v = d[k]
        if v not in inverted.keys():
            inverted[v] = []
        inverted[v].append(k)
    return inverted

print invert_general({1:2, 3:4, 4: 4})

An alternative is to used the defaultdict from the collections module. Such a defaultdict behaves like an ordinary dictionary, but you can specify a function to create default values if you access unknown keys:

from collections import defaultdict 

# this creates an empty list in dd if you access unknown keys:
dd = defaultdict(list)

dd[2].append(1)
print(dd)
defaultdict(<class 'list'>, {2: [1]})
from collections import defaultdict 

def invert_general(d):
    """alternative solution using setdefault"""
    inverted = defaultdict(list)
    for k in d.keys():
        v = d[k]
        inverted[v].append(k)
    return inverted

print(invert_general({1:2, 3:4, 4: 4}))
defaultdict(<class 'list'>, {2: [1], 4: [3, 4]})

Exercise block 8

with open("numbers.txt", "w") as fp:
    for i in range(1, 11):
        print(i * i, file=fp)

sum_ = 0
with open("numbers.txt", "r") as fp:
    for line in fp:
        sum_ += int(line.strip()) ** 2

print(sum_)
25333

Optional exercises

We use list comprehensions here, which are explained later in the script:

import csv

with open("table.csv", "w") as fh:
    csv_fh = csv.writer(fh, delimiter=";")
    for row_idx in range(1, 11):
        row = [row_idx * col_idx for col_idx in range(1, 11)]
        csv_fh.writerow(row)  # entries in row can have arbitrary type, here: ints !
        
#show output of table.csv









sum_ = 0

with open("table.csv", "r") as fh:
    csv_fh = csv.reader(fh, delimiter=";")
    for row in csv_fh:
        for cell in row:
            sum_ += int(cell)    # when reading the row is always a list of strings !
print(sum_)
3025

Exercise block 9

numbers = [2, 3, 5, 7, 11]
print([n **2 for n in numbers if n < 7])
[4, 9, 25]
def my_key(s):
    return s.upper()

data = ["ab", "ABc", "abC", "AB"]
print("regular sort:", sorted(data))
print("special sorg:", sorted(data, key=my_key))
regular sort: ['AB', 'ABc', 'ab', 'abC']
special sorg: ['ab', 'AB', 'ABc', 'abC']

Exercise block 10

def detect_type(value):
    for t in [float, int, str]:
        try:
            t(value)
            return t
        except ValueError:
            pass
    return None

for d in ["1.23", "123", "abc"]:
    print(d, detect_type(d)) 
1.23 <class 'float'>
123 <class 'float'>
abc <class 'str'>
class InvalidTerm(ValueError):
    pass


def to_float(s):
    try:
        return float(s)
    except ValueError:
        raise InvalidTerm("'{}' is not a number".format(s))
    
    
def add(x, y):
    return x + y


def sub(x, y):
    return x - y


def mul(x, y):
    return x * y


def div(x, y):
    return x / y


def evaluate(term):
    parts = term.split(" ")
    if len(parts) != 3:
        raise InvalidTerm("term '{}' does not have form N1 op N2".format(term))
    
    # unpack list of length 3:
    v1, op, v2 = parts
   
    v1 = to_float(v1)
    v2 = to_float(v2)

    operations = {"+" : add,
                  "-" : sub,
                  "*" : mul,
                  "/" : div,
                  "**": pow}  # pow is builtin function

    if op not in operations.keys():
        raise InvalidTerm("op '{}' not known".format(op))
        
    return operations[op](v1, v2)


for term in ["2 + 3", 
             "2 - 3",
             "2 * 3",
             "2 / 3",
             "2 ** 3",
             "2+3",
             "a + 1",
             "1 + a",
             "1 x 2"
             ]:
    try:
        value = evaluate(term)
        print("value of", term, "is", value)
    except InvalidTerm as e:
        print("evaluating", term, "resulted in error", str(e))
value of 2 + 3 is 5.0
value of 2 - 3 is -1.0
value of 2 * 3 is 6.0
value of 2 / 3 is 0.6666666666666666
value of 2 ** 3 is 8.0
evaluating 2+3 resulted in error term '2+3' does not have form N1 op N2
evaluating a + 1 resulted in error 'a' is not a number
evaluating 1 + a resulted in error 'a' is not a number
evaluating 1 x 2 resulted in error op 'x' not known

Exercise block 11

def merge(*dicts, **kwargs):
    if not dicts:    # an empty dict is evaluated as 'False' !
        return kwargs
    
    result = dicts[0]
    for d in dicts[1:]:
        result.update(d)   # dicts have an update method
        
    # don't forget to merge the kwargs:
    result.update(kwargs)
    return result

print(merge(a=3, b=4))
print(merge({"a":7}, {"c": 5}, a=3, b=4))
{'a': 3, 'b': 4}
{'a': 3, 'c': 5, 'b': 4}

Exercise block 12

def chain(*iterators):
    for iterator in iterators:
        for value in iterator:
            yield value
            
print(list(chain(range(3), range(3), range(2))))
[0, 1, 2, 0, 1, 2, 0, 1]
# even shorter, not in the script

def chain(*iterators):
    for iterator in iterators:
        yield from iterator
            
print(list(chain(range(3), range(3), range(2))))
[0, 1, 2, 0, 1, 2, 0, 1]

Exercise block 13

data = ["ab", "AB", "abc", "ABc", "ABC"]
print(sorted(data))
print(sorted(data, key=lambda s:s.upper()))
['AB', 'ABC', 'ABc', 'ab', 'abc']
['ab', 'AB', 'abc', 'ABc', 'ABC']

Exercise block 14

def double(fun):
    def wrapped(*a, **kw):   # this captures all use cases how fun can be called
        return 2 * fun(*a, **kw)
    return wrapped

@double
def inc(x):
    return x + 1

print(inc(6))
14

Exercise block ???

# copy pasted from the script, then modified to solve the exercise:

import math

class Point2D:
    
    def __init__(self, x0, y0):
        self.x = x0
        self.y = y0
        
    def length(self):
        return math.hypot(self.x, self.y)
    
    def __str__(self):
        return "Point2D(x=%s, y=%s, lenght=%s)" % (self.x, self.y, self.length()) # attribute and method access here !
    
    
class Vector2D(Point2D):
        
    def __str__(self):
        """ overrides __str__ from Vector2D """
        return "Vector2D(x=%s, y=%s)" % (self.x, self.y)
    
    def __add__(self, other):
        """ is called when you execute self + other """
        return Vector2D(self.x + other.x, self.y + other.y)
    
    
    # here come the new methods from the exercise:
    
    def scale(self, factor): 
        """ scales vector self by factor fac """
        
        assert isinstance(factor, (float, int))  # checks if factor is int or float
        self.x *= factor
        self.y *= factor
    
    def __mul__(self, other):
        """ is called when you execute self * other """
        return self.x * other.x +  self.y * other.y

    
v1 = Vector2D(1, 2)
v2 = Vector2D(2, 2)
v1.scale(2)
print(v1)
print(v1 * v2)
Vector2D(x=2, y=4)
12
class ComplexNumber(Vector2D):
    
    def __str__(self):
        if self.y == 0:
            return str(self.x)
        elif self.x == 0:
            return "{} i".format(self.y)
        elif self.y > 0:
            return "{} + {} i".format(self.x, self.y)
        else:
            return "{} - {} i".format(self.x, -self.y)
            
    def __mul__(self, other):
        re = self.x * other.x - self.y * other.y
        im = self.x * other.y + self.y * other.x
        return ComplexNumber(re, im)

        
c1 = ComplexNumber(1, 1)
c2 = ComplexNumber(1, -1)
c3 = ComplexNumber(2, 0)

print(c1)
print(c2)
print(c3)

print(c1 * c1)
print(c1 * c2)
print(c1 * c3)

# but we did not override __add__; so the result is Vector2D again.
print(c1 + c1)
1 + 1 i
1 - 1 i
2
2 i
2
2 + 2 i
Vector2D(x=2, y=2)