17.1. Fallunterscheidungen#

Für eine Fallunterscheidung können wir für jeden Fall \(i\) einen bestimmten Codeblock \(B_i\) definieren. Dieser wird genau dann ausgeführt sofern ein logischer Ausdruck \(P_i\) wahr, d.h. True ergibt und kein anderer logischer Ausdruck \(P_j\) für \(j < i\) bereits True ergeben hat. In anderen Worten, der Fall \(i\) trifft zu und kein Fall davor ist bereits eingetreten.

Damit führt eine Fallunterscheidung zur Ausführung von höchstens einer der Codeblöcke \(B_0, \ldots, B_n\). Trifft keiner der Fälle zu, d.h. kein \(P_i\) ergibt True und es ist kein Alternativfall (else) definiert, so wird keiner der Codeblöcke ausgeführt. Eine Fallunterscheidung beginnt immer mit dem if-Signalwort!

17.1.1. if#

Die einfachste Form der Fallunterscheidung prüft ob ein logischer Ausdruck \(P_0\) eingetreten. Nur wenn dies zutrifft, wird der Codeblock \(B_0\) ausgeführt:

if P0:
    B0
x = 2

if x <= 2:
    x += 1
    print(f'x: {x}')
x: 3

17.1.2. if-else#

In der nächsten Variante wird ein Codeblock \(B_0\) nur dann ausgeführt, sofern ein logischer Ausdruck \(P_0\) True ergibt. Ist dies nicht der Fall, so wird ein Alternativblock (else) \(B_1\) ausgeführt.

if P0:
    B0
else:
    B1
x = 2

if x <= 2:
    x += 1
    print(f'x: {x}')
else:
    print(f'x > 2')
print(x)
x: 3
3

17.1.3. if-elif#

In der nächsten Variante wird höchstens ein Codeblock \(B_i\) nur dann ausgeführt, sofern ein logischer Ausdruck \(P_i\) True ergibt. Gibt es mehr als einen logischen Ausdruck \(P_i\) welcher True ergibt, so wird der erste Codeblock (der mit dem kleinsten \(i\)) ausgeführt.

if P0:
    B0
elif P1:
    B1
elif P2
    B2
...
x = 2

if x <= 2:
    print(f'x <= 2')
    x += 1
elif x <= 5:
    print(f'x <= 5')
    x += 2
elif x <= 6:
    print(f'x <= 4')
    x += 6
print(x)
x <= 2
3

17.1.4. if-elif-else#

In der nächsten Variante wird genau ein Codeblock \(B_i\) nur dann ausgeführt, sofern ein logischer Ausdruck \(P_i\) True ergibt. Gibt es mehr als einen logischen Ausdruck \(P_i\) welcher True ergibt, so wird der erste Codeblock (der mit dem kleinsten \(i\)) ausgeführt. Trifft kein Fall zu, wird der Alternativblock \(B_n\) ausgeführt.

if P0:
    B0
elif P1:
    B1
elif P2
    B2
...
else:
    Bn
x = 2

if x <= 2:
    print(f'x <= 2')
    x += 1
elif x <= 5:
    print(f'x <= 5')
    x += 2
elif x <= 7:
    print(f'x <= 7')
    x += 10
else:
    print(f'x > 2 and x > 5 and x > 7')
    x = 2   
print(x)
x <= 2
3

17.1.5. if-if-else#

Aufeinanderfolgende if-Statements sind nicht eine sondern mehrere Fallunterscheidungen, denn jedes if-Statement leitet eine neue Fallunterscheidung ein!

x = 2

if x <= 2:
    print(f'x <= 2')
    x += 1
if x <= 5:
    print(f'x <= 5')
    x += 2
if x <= 7:
    print(f'x <= 7')
    x += 10
else:
    print(f'x > 2 and x > 5 and x > 7')
    x = 2   
print(x)
x <= 2
x <= 5
x <= 7
15

Aufeinander folgende Fallunterscheidungen

Es ist zu empfehlen aufeinanderfolgende if-Statements durch eine Leerzeile zu trennen.

x = 2

if x <= 2:
    print(f'x <= 2')
    x += 1
    
if x <= 5:
    print(f'x <= 5')
    x += 2
    
if x <= 7:
    print(f'x <= 7')
    x += 10
else:
    print(f'x > 2 and x > 5 and x > 7')
    x = 2   
print(x)
x <= 2
x <= 5
x <= 7
15

17.1.6. Verschachtelung#

Selbstverständlich kann ein Codeblock \(B_i\) erneut eine oder mehrere Fallunterscheidungen enthalten. Und selbstverständlich können wir Fallunterscheidungen in Funktionen einbauen.

def nested_branching(x, y):
    if x > 2:
        if y < 2:
            out = x + y
        else:
            out = x - y
    else:
        if y > 2:
            out = x * y
        else:
            out = 0
    return out

print(nested_branching(3, 1))
print(nested_branching(3, 2))
print(nested_branching(1, 3))
print(nested_branching(1, 1))
4
1
3
0

Wir können häufig verschachtelte Fallunterscheidungen auflösen. Zum Beispiel können wir die Funktion auch wie folgt definieren:

def nested_branching(x, y):
    if x > 2 and y < 2:
        out = x + y
    elif x > 2:
        out = x - y
    elif x <= 2 and y > 2:
        out = x * y
    else:
        out = 0
    return out

print(nested_branching(3, 1))
print(nested_branching(3, 2))
print(nested_branching(1, 3))
print(nested_branching(1, 1))
4
1
3
0

Es ist eine Frage der Lesbarkeit, welche Variante besser ist. In der zweiten Version haben wir zwar eine niedrigere Verschachtellung, allerdings sehen wir nicht sofort, dass die ersten Fälle

if x > 2 and y < 2:
        out = x + y
elif x > 2:
    out = x - y
...

und die beiden letzten Teile

...
elif x <= 2 and y > 2:
    out = x * y
else:
    out = 0

eine Entweder-Oder-Beziehung besitzen. Zudem haben wir zweimal den logischen Ausdruck x > 2.

Einen Block der Art:

if P0:
    if P1:
        if P2:
            if P3:
                ...

lässt sich immer in

if P0 and P1 and P2 and P3 ...:

umwandeln.

17.1.7. Schnellschreibweise#

Python erlaubt es uns einen Ausdruck der Form

if P0:
    x = A0
else:
    x = A1

als

x = A0 if P0 else A1

zu schreiben.

y = 12
x = 0 if y % 2 == 0 else 1
x
0

Diese Schreibweise lässt sich gut mit sog. Comprehension verbinden:

numbers = list(range(10))
even = [True if x % 2 == 0 else False for x in numbers]
print(f'numbers: {numbers}')
print(f'even: {even}')
numbers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
even: [True, False, True, False, True, False, True, False, True, False]

17.1.8. Beispiel (quadratische Gleichungen)#

Sei \(f(x) = ax^2 + bx + c\) mit konstanten Zahlen \(a, b, c \in \mathbb{R}\). Wir wissen, dass \(f(x) = 0\) für

\[x_{1,2} = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}\]

sofern \(b^2 - 4ac >= 0\). Lassen Sie uns eine Funktion solve_quadratic(a, b, c) entwerfen, welche uns alle \(x_{1,2}\) berechnet. Es gibt eine, zwei oder keine Lösung.

Lassen Sie uns als erstes eine Funktion entwerfen, die testet ob eine Fließkommazahl annähernd gleich null ist. Beachten Sie: Fließkommazahlen sind lediglich eine Annäherung des echten Wertes und damit ist der exakte Vergleich == ungeeignet. Stattdessen ist unsere Fließkommazahl x gleich null, falls sie annähernd gleich null ist.

def is_zero(x):
    epsilon = 1.0e-12
    return -epsilon < x < 1.0e-12

Lassen Sie uns nun die quadratische Gleichung lösen. Dazu müssen wir lediglich die Formel für \(x_{1,2}\) implementieren:

def solve_quadratic(a, b, c):    
    disc = b**2 - (4*a*c)
            
    # disc < 0 => no solution
    if disc < 0:
        return ()
    
    # disc == 0? => one solution
    if is_zero(disc):
        return -b / (2*a),
    
    # default case => 2 solutions
    return (-b + disc**0.5) / (2*a), (-b - disc**0.5) / (2*a)

Der Code scheint zur richtigen Lösung zu führen:

print(f'solution for x^2 -4x + 1 = 0: {solve_quadratic(1, -4, 1)}')
print(f'solution for x^2 + x = 0: {solve_quadratic(1, 1, 0)}')
print(f'solution for x^2 = 0: {solve_quadratic(1, 0, 0)}')
solution for x^2 -4x + 1 = 0: (3.732050807568877, 0.2679491924311228)
solution for x^2 + x = 0: (0.0, -1.0)
solution for x^2 = 0: (0.0,)

Was passiert allerdings wenn a==0 ist?

print(f'solution for -4x + 1 = 0: {solve_quadratic(0, -4, 1)}')
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[14], line 1
----> 1 print(f'solution for -4x + 1 = 0: {solve_quadratic(0, -4, 1)}')

Cell In[12], line 13, in solve_quadratic(a, b, c)
     10     return -b / (2*a),
     12 # default case => 2 solutions
---> 13 return (-b + disc**0.5) / (2*a), (-b - disc**0.5) / (2*a)

ZeroDivisionError: float division by zero

In diesem Fall teilen wir durch null und erhalten (zum Glück) einen Fehler. Sofern a==0 gilt müssen wir die lineare Gleichung

\[bx + c = 0 \Rightarrow x = c/b\]

lösen. Hier wartet ein weiterer Sonderfall für b==0! In diesem Fall gibt es kein Ergebnis sofern c != 0, andernfalls gibt es unendlich viele Lösungen. Nutzen wir die Fallunterscheidung um all diese Sonderfälle abzudecken:

def solve_quadratic(a, b, c):
    """"
    solves the quadratic equation ax^2 + bx + c = 0.
    a == b == c == 0 is not allowed!
    """
    
    disc = b**2 - (4*a*c)
    epsilon = 1.0e-12
    
    # a == 0? => line => one or none solution
    if is_zero(a):
        # b == 0? 
        if is_zero(b):
            # c == 0 => infinitely many solutions
            if is_zero(c):
                raise AttributeError('Invalid arguments a == b == c == 0! This quadratic equation has infinitely many solutions.')
            else:
                return ()
        else:
            return -c/b,
        
    # disc < 0 => no solution
    if disc < 0:
        return ()
    
    # disc == 0? => one solution
    if is_zero(disc):
        return -b / (2*a),
    
    # default case => 2 solutions
    return (-b + disc**0.5) / (2*a), (-b - disc**0.5) / (2*a)
print(f'solution for x^2 -4x + 1 = 0: {solve_quadratic(1, -4, 1)}')
print(f'solution for x^2 + x = 0: {solve_quadratic(1, 1, 0)}')
print(f'solution for x^2 = 0: {solve_quadratic(1, 0, 0)}')
print(f'solution for -4x + 1 = 0: {solve_quadratic(0, -4, 1)}')
print(f'solution for 1 = 0: {solve_quadratic(0, 0, 1)}')
solution for x^2 -4x + 1 = 0: (3.732050807568877, 0.2679491924311228)
solution for x^2 + x = 0: (0.0, -1.0)
solution for x^2 = 0: (0.0,)
solution for -4x + 1 = 0: (0.25,)
solution for 1 = 0: ()

Gilt a == b == c == 0 wird unsere Funktion zu \(f(x) = 0\) und demnach gilt für jedes \(x_0 \in \mathbb{R}\) dass es eine Nullstelle ist. Das wären unendlich viele Zahlen, deshalb werfen wir (raise) in diesem Fall einen Fehler.

print(f'solution for 0 = 0: {solve_quadratic(0, 0, 0)}')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[17], line 1
----> 1 print(f'solution for 0 = 0: {solve_quadratic(0, 0, 0)}')

Cell In[15], line 16, in solve_quadratic(a, b, c)
     13 if is_zero(b):
     14     # c == 0 => infinitely many solutions
     15     if is_zero(c):
---> 16         raise AttributeError('Invalid arguments a == b == c == 0! This quadratic equation has infinitely many solutions.')
     17     else:
     18         return ()

AttributeError: Invalid arguments a == b == c == 0! This quadratic equation has infinitely many solutions.