6Ergänzendes Wissen

In diesem Kapitel schauen wir uns einige Themen wie Benutzereingaben, Zufallszahlen, Besonderheiten bei Parameterübergaben sowie den Ternary-Operator an und erweitern unsere Python-Kenntnisse etwa zu break und continue in Schleifen sowie der dynamischen Auswertung von Ausdrücken. Den Abschluss bildet dann die Beschreibung einer »Rekursion« genannten Technik, mit der sich diverse Problemstellungen elegant lösen lassen.

6.1Benutzereingaben input()

Programme verarbeiten in der Regel diverse Daten. Diese können etwa aus Dateien eingelesen werden (vgl. Kapitel 8) oder aber aus grafischen Benutzeroberflächen oder Datenbanken stammen.1 Im einfachsten Fall werden zu verarbeitende Daten vom Benutzer auf der Kommandozeile eingegeben. Dafür besitzt Python die Funktion input(), die wir nun kennenlernen wollen.

Beispiel

Der Funktion input() gibt man eine Zeichenkette mit, die den Benutzer zu einer Eingabe auffordert. Der zurückgelieferte Wert ist ein String. Im folgenden Beispiel erfragen wir zunächst den Namen und dann das Alter. Dieses wandeln wir mit int() in eine Ganzzahl, um Berechnungen anstellen zu können, hier, um den Wert 10 zu addieren:

name = input("Wie heißen Sie? ")

age = input("Wie alt sind Sie? ")

age_plus_10 = int(age) + 10

print("Herzlich willkommen", name + "!")

print("In 10 Jahren sind Sie", age_plus_10, "Jahre alt!")

Auf der Konsole sieht das Ganze in etwa so aus:

Wie heißen Sie? Michael

Wie alt sind Sie? 50

Herzlich willkommen Michael!

In 10 Jahren sind Sie 60 Jahre alt!

TypumwandlungWeil input() einen String als Ergebnis liefert, man aber immer mal wieder Zahlenwerte benötigt, sieht man oftmals eine Typumwandlung direkt bei der Eingabe wie folgt sowohl für Ganzzahlen als auch Gleitkommazahlen:

>>> zahl = int(input("Bitte geben Sie eine Ganzzahl ein: "))

Bitte geben Sie eine Ganzzahl ein: 42

>>> print(zahl)

42

>>> floatzahl = float(input("Bitte geben Sie eine Gleitkommazahl ein: "))

Bitte geben Sie eine Gleitkommazahl ein: 42.195

>>> print(floatzahl)

42.195

Tipp: Unsichtbare Eingabe etwa für Passwörter

Manchmal möchte man die Benutzereingabe nicht lesbar auf der Konsole durchführen, um beispielsweise geheime Informationen wie Passwörter auch geheim zu halten. Dabei hilft das Modul getpass und die Funktion getpass():

from getpass import getpass

user = input("User: ")

pwd = getpass("Password: ")

6.2Zufallswerte und das Modul random

Das Modul random bietet einige Funktionalitäten zur Erzeugung von Zufallszahlen. Schauen wir uns zum Einstieg ein paar Aufrufe auf der Konsole an. Im Anschluss gehe ich dann auf die einzelnen Möglichkeiten etwas genauer ein.

>>> import random

>>>

>>> random.random()

0.16364941375885966

>>>

>>> random.randint(1, 49)

33

>>>

>>> random.randrange(10, 100)

13

>>>

>>> random.choice(["Wasser", "Bier", "Apfelschorle", "Cola"])

'Cola'

>>>

>>> values = ["AB", "BC", "CD", "DE"]

>>> random.shuffle(values)

>>> values

['DE', 'CD', 'AB', 'BC']

Zufallswerte im Bereich 0 bis 1

Die Funktion random.random() liefert eine Zufallszahl zwischen 0,0 (inklusive) und 1,0 (exklusive): Um mehr Kontrolle über den Wertebereich der Zufallszahl zu bekommen, z. B. wenn Sie nur eine Zufallszahl zwischen 0 und 100 benötigen, können Sie die folgende Formel verwenden:

zufallszahl = random.random() * 100

Zufallswerte aus einem Bereich

Manchmal benötigt man Zufallszahlen aus einem fixen Wertebereich mit unterer und oberer Grenze. Dazu bietet Python die Funktion random.randrange():

>>> random.randrange(10, 100)

24

>>> random.randrange(10, 100)

63

>>> random.randrange(10, 100)

41

Dabei kann man optional noch eine Schrittweite mitgeben. Dann werden ausgehend vom Startwert nur Zufallswerte mit der angegebenen Schrittweite erstellt:

>>> random.randrange(200, 5000, 100)

2900

>>> random.randrange(200, 5000, 100)

4000

>>> random.randrange(200, 5000, 100)

2800

Werte aus einer Liste zufällig wählen

Immer mal wieder ist es praktisch, aus einem Datenbestand einen Wert zufällig zu wählen. Dafür bietet sich die Funktion choice() an, exemplarisch für drei Pizzavarianten gezeigt:

>>> random.choice(["Napoli", "Funghi", "Diavolo"])

'Funghi'

>>> random.choice(["Napoli", "Funghi", "Diavolo"])

'Diavolo'

>>> random.choice(["Napoli", "Funghi", "Diavolo"])

'Napoli'

Das Ganze funktioniert natürlich auch mit Listen von Zahlen. Nachfolgend wählen wir dreimal zufällig eine Zahl aus den Primzahlen bis 20:

>>> random.choice([2, 3, 5, 7, 11, 13, 17, 19])

2

>>> random.choice([2, 3, 5, 7, 11, 13, 17, 19])

13

>>> random.choice([2, 3, 5, 7, 11, 13, 17, 19])

5

Listen durcheinanderwürfeln

Ab und an möchte man einen bestehenden Datenbestand durcheinanderwürfeln bzw. mischen, etwa bei einem Kartenspiel. Dazu bietet Python praktischerweise die Funktion shuffle(). Im Beispiel werden die Zahlen von 1 bis 15 durcheinandergewürfelt:

>>> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

>>> random.shuffle(numbers)

>>> numbers

[2, 8, 13, 12, 5, 10, 1, 15, 6, 3, 7, 11, 14, 4, 9]

6.3Besonderheiten von Parametern

Wir haben bereits diverse Funktionen aufgerufen und dort auch Parameter übergeben. Tatsächlich wurde dabei zuvor sogar schon ein benannter Parameter verwendet, nämlich end bei print(). Schauen wir uns nachfolgend noch ein paar Details zur Parameterübergabe in Python an.

6.3.1Parameter mit Position bzw. Name

Gehen wir von der folgenden einfachen Funktion aus, die zwei Parameter entgegennimmt und deren Wert ausgibt:

>>> def parameter_example(first, second):

... print("first:", first)

... print("second:", second)

...

Bekanntermaßen und wie gewohnt lässt sich diese Funktion auf folgende Weise aufrufen:

>>> parameter_example(1, 22)

first: 1

second: 22

Als Variation ermöglicht Python auch eine Parameterübergabe per Name auf folgende Art:

>>> parameter_example(second = 22, first = 1)

first: 1

second: 22

Parameter per Name

Man kann also Werte entweder positionsorientiert oder basierend auf deren Namen angeben. Bei Letzterem ist die Reihenfolge egal. Zwar lassen sich die Angaben nicht beliebig mixen, aber man kann einige Werte erst positionsbasiert und danach die restlichen namensbasiert übergeben. Betrachten wir dazu folgende Funktion:

>>> def parameter_example(first, second, third):

... print("first:", first)

... print("second:", second)

... print("third:", third)

...

Diese Funktion lässt sich dann auf folgende Weisen aufrufen:

>>> # positionsbasiert

>>> parameter_example(1, 22, 333)

first: 1

second: 22

third: 333

>>>

>>> # namensbasiert

>>> parameter_example(second = 22, first = 1, third = 333)

first: 1

second: 22

third: 333

>>>

>>> # erst positionsbasiert, dann namensbasiert

>>> parameter_example(1, third = 333, second = 22)

first: 1

second: 22

third: 333

6.3.2Parameter mit Defaultwert

Mitunter bietet es sich an, für einige Parameter vordefinierte Werte angeben zu können. Das erlaubt es beim Aufruf, für diese speziellen Parameter entsprechende Angaben wegzulassen oder nur bei tatsächlichem Bedarf zu spezifizieren. Dadurch entstehen optionale Parameter. Diese dürfen jedoch nur hinten in der Parameterliste auftreten.

Betrachten wir wiederum ein Beispiel mit drei Parametern, wobei der letzte eine optionale Information transportieren kann, die standardmäßig einen leeren Text enthält:

>>> def parameter_with_default(x, y, opt_info=""):

... print("(%d, %d)" % (x,y))

... if opt_info:

... print("Info:", opt_info)

...

Diese Funktion lässt sich dann auf folgende Weisen mit und ohne optionalen Parameter aufrufen:

>>> parameter_with_default(2, 7)

(2, 7)

>>> parameter_with_default(2, 7, "Special Info")

(2, 7)

Info: Special Info

6.3.3Var Args – variable Anzahl an Argumenten

Mitunter ist es hilfreich, eine beliebige Anzahl von Werten an eine Funktion (oder auch Methode) übergeben zu können. Tatsächlich kennen wir bereits eine Funktion, die genau das unterstützt: nämlich die Funktion print(). Im Verlauf des Buchs wurde diese schon mehrmals mit einer unterschiedlichen Anzahl an kommaseparierten Werten aufgerufen.

Eine solche variable Anzahl an Argumenten wird auch Var Args als Kurzform für Variable Arguments genannt. Möchte man dies für eigene Funktionen erlauben, so muss dazu vor dem Parameter ein Stern, etwa *args, angegeben werden. Das bedeutet dann, dass man als Aufrufer beliebig viele Werte übergeben kann. Demnach muss beim Schreiben der Funktion nicht bekannt sein, wie viele Argumente später beim Aufruf übergeben werden.

Schauen wir uns nun an, wie man eine derartige Funktion unter Verwendung des Sterns vor dem Parameternamen selbst definieren und dann flexibel nutzen kann:

>>> def flexi_print(*values):

... print(*values)

...

>>> flexi_print("ONE")

ONE

>>> flexi_print("ONE", "TWO")

ONE TWO

>>> flexi_print("ONE", "TWO", "THREE")

ONE TWO THREE

Nach diesem Beispiel zum Einstieg schauen wir uns an, wie man auf den übergebenen Parameterwerten eine Aktion ausführt. Nachfolgend wird dies für eine Summenberechnung inklusive der Ausgabe einer Meldung vor dem Ergebnis gezeigt. Die einzelnen Werte lassen sich praktischerweise sehr leicht mit einer for-in-Schleife durchlaufen:

>>> def var_args_sum(info, *args):

... result = 0

... for num in args:

... result += num

... return info + str(result)

...

>>> var_args_sum("Summe: ", 1, 2, 3, 4, 5, 6, 7)

'Summe: 28'

Am Beispiel sieht man, dass neben den Var Args weitere Parameter möglich sind. Allerdings muss der Var-Arg-Parameter am Ende stehen.

Trickreiche Nutzung

Manchmal existiert für den Fall einer leeren Eingabe im Gegensatz zum Beispiel der Summe kein sinnvoller Defaultwert. Das trifft etwa dann zu, wenn man das Minimum oder Maximum einer leeren Wertemenge berechnen möchte.

Um eine aufwendige Spezialbehandlung zu vermeiden, bietet es sich an, die Funktionalität so zu definieren, dass diese einen erforderlichen Parameter und danach eine variable Argumentliste erhält:

>>> def my_min(firstvalue, *othervalues):

... min_ = firstvalue

... for val in othervalues:

... if val < min_:

... min_ = val

... return min_

...

Welchen Vorteil bringt diese Definition gegenüber der reinen Definition per Var Args?

Schauen wir uns kurz eine Verwendung an – eine leere Parameterliste wird von Python (natürlich) direkt beim Aufruf zurückgewiesen.

>>> my_min()

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

TypeError: my_min() missing 1 required positional argument: ' firstvalue'

>>> my_min(47)

47

>>> my_min(47, 25, 17, 7, 50)

7

Bitte beachten Sie noch folgende zwei Punkte:

  1. Eine Berechnung des Minimums sollten Sie nicht selbst vornehmen, da es dazu bereits vorgefertigte Funktionalität in Python gibt – hier dient das nur der Demonstration von notwendigen und optionalen Argumenten inklusive einer variablen Anzahl an Parametern.
  2. Weil der Name min schon in Python definiert ist, empfiehlt es sich, bei einem derartigen Namenskonflikt einer Variablen ein _ hinten anzufügen. Dies wird im zweiten nachfolgenden Praxistipp als Shadowing thematisiert.

Tipp: Besonderheiten für Schlüssel-Wert-Paare

Eine variable Anzahl an Schlüssel-Wert-Paaren lässt sich mit ** vor den Parametern zur Übergabe wie folgt definieren:

>>> def process_values(**kwargs):

... for key, value in kwargs.items():

... print("{0} = {1}".format(key, value))

...

>>> programmer_languages = {"Tim": ["Java"], "Michael": ["Java", "Python"], "Peter": ["C#"]}

>>> process_values(**programmer_languages)

Tim = ['Java']

Michael = ['Java', 'Python']

Peter = ['C#']

Diese Art der Parameterübergabe ist allerdings ein fortgeschritteneres Thema und wird hier daher nicht weiter vertieft.

Achtung: Fallstrick Shadowing: Verdecken von Built-ins

Im Zusammenhang mit der Definition von einzelnen temporären Variablen möchte ich Sie auf einen Fallstrick aufmerksam machen. Mitunter fällt es schwer, einen sinnvollen Namen für eine Variable zu finden. Man könnte versucht sein, einfach den Namen des Typs zu verwenden, etwa wie folgt:

>>> list = []

Doch diese (reine) Nutzung des Typs ist problematisch, weil hierdurch ein sogenanntes Shadowing erfolgt und danach der Built-in-Typ nicht mehr funktioniert. Schauen wir uns ein Beispiel an, wie etwa die Listenfunktionalität durch eine unbedachte Namensgebung unzugreifbar wird.

Achtung: Fallstrick Shadowing: Verdecken von Built-ins II

Zunächst definieren wir ganz normal eine Liste, dann verwenden wir (versehentlich) den puren Namen list für eine Zuweisung und schließlich wollen wir wieder eine Liste definieren:

>>> list("ABCDEF")

['A', 'B', 'C', 'D', 'E', 'F']

>>> list = []

>>> list("ABCDEF")

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

TypeError: 'list' object is not callable

Das Ganze ist nicht auf die Built-in-Typen beschränkt, sondern wirkt sich auch auf Built-in-Funktionen aus. Nachfolgend ist dies für die eingebaute Standardfunktionalität min() gezeigt, die plötzlich durch einen Lambda-Ausdruck (vgl. Abschnitt 7.5) zur Addition mutiert:

>>> min = lambda a, b: a + b

>>> min(7, 2)

9

Sofern man sich der Tatsache bewusst ist, kann es etwa bei der Verwendung innerhalb einer Funktion kein Problem darstellen. Möchte man diese Unschönheit jedoch klarer aufzeigen, sieht man als Abhilfe häufig, das Anhängen eines _:

>>> min_ = lambda a, b: a + b

>>> list_ = []

Oftmals lässt sich mit ein wenig Nachdenken jedoch ein besserer Name finden:

>>> add = lambda a, b: a + b

>>> characters = []

Aber Achtung: Falls Sie bei der Addition an sum gedacht haben, dann hätten Sie gleich die nächste Built-in-Funktionalität verdeckt.

6.4Ternary-Operator

Manchmal ist eine Kombination aus if und else leicht ungelenk bzw. etwas länglich. Als Abhilfe gibt es eine Kurzform, die als ternärer Operator bekannt ist. Er heißt ternär, weil das Ganze aus drei Teilen besteht:

variable = expressionTrue if condition else expressionFalse

Damit lassen sich mehrere Zeilen einer if-/else-Konstruktion durch eine einzige Zeile ersetzen.

Beispiel

Das Konstrukt

>>> time = 20

>>> if time < 18:

... print("Good day")

... else:

... print("Good evening")

...

Good evening

kann wie folgt vereinfacht werden:

>>> time = 20

>>> result = "Good day" if time < 18 else "Good evening"

>>> print(result)

Good evening

Schauen wir uns ein weiteres Beispiel an:

>>> age = 49

>>> "old enough" if age >= 18 else "too young"

'old enough'

Im Beispiel ist die 49 größer gleich der 18 und damit die Bedingung erfüllt, somit wird »old enough« ausgegeben. Wäre der Wert von age beispielsweise 7, dann würde die Bedingung zu False ausgewertet und »too young« ausgegeben.

Tipp: Der ternäre Operator in der Praxis

Obwohl es verlockend ist, Platz zu sparen, bedenken Sie für eine gute Lesbarkeit und Verständlichkeit Folgendes: Sie sollten sich wirklich auf einfache Abfragen beschränken, sonst wird der ternäre Operator recht schnell unleserlich.

6.5Aufzählungen mit Enum

Für diverse Anwendungsfälle gibt es einige vordefinierte, zusammengehörende Werte, etwa Jahreszeiten (Frühling, Sommer, Herbst, Winter) oder T-Shirt-Größen (XS, S, M, L, XL, XXL).

Erste Idee: Listen mit fixen Werten

Überlegen wir kurz: Wie würden wir Derartiges mit unserem bisherigen Wissen implementieren? Eine ziemlich naheliegende Idee wäre es, dafür Listen mit fixen Werten wie folgt zu definieren:

>>> jahreszeiten = ["Frühling", "Sommer", "Herbst", "Winter"]

>>> tshirt_sizes = ["XS", "S", "M", "L", "XL", "XXL"]

Betrachten wir mal einen Einsatz:

>>> print(jahreszeiten[0])

Frühling

>>> print("Ich mag den", jahreszeiten[1])

Ich mag den Sommer

FallstrickeDas Ganze wirkt noch etwas unhandlich. Aber Moment! Es birgt auch richtige Fallstricke – man kann nämlich unerwartet in der Liste ändern und so wird aus einer Jahreszeit versehentlich eine Stadt!

>>> jahreszeiten[1] = "Zürich"

>>> print("Ich mag den", jahreszeiten[1])

Ich mag den Zürich

Das ist unerwünscht und sicher nicht gewollt. Als Abhilfe und Alternative könnte man die unveränderlichen Tupel (vgl. Abschnitt 2.9.2) zur Definition der möglichen Werte nutzen:

>>> jahreszeiten = ("Frühling", "Sommer", "Herbst", "Winter")

>>> tshirt_sizes = ("XS", "S", "M", "L", "XL", "XXL")

Eine (versehentliche) Zuweisung wird dann folgendermaßen mit einem TypeError (vgl. Kapitel 9) unterbunden:

>>> jahreszeiten[1] = "Zürich"

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

TypeError: 'tuple' object does not support item assignment

Der unschöne Indexzugriff verbleibt aber auch bei Tupeln als Manko. Schauen wir uns nun an, wie man die Definition von zusammenhängenden Werten mit Enums lösen kann.

Aufzählungen mit Enum als Abhilfe

Gerade haben wir mit Listen und Tupeln mit fixen Werten zur Repräsentation von Aufzählungen etwas getrickst, aber diese Modellierungsvarianten offenbaren schnell ihre Nachteile. Besser wäre es doch, wenn wir die Werte fix definieren und in Form eines Typs bereitstellen könnten. Dabei helfen Aufzählungen. Eine solche erstellt man mithilfe des Basistyps Enum und der Angabe der einzelnen Konstanten in jeweils eigenen Zeilen. Damit die Konstanten automatisch eindeutige Werte erhalten, bietet sich ein Aufruf von auto() an. Beachten Sie, dass die Konstanten per Konvention in Großbuchstaben geschrieben werden sollten:

>>> from enum import Enum, auto

>>>

>>> class Jahreszeiten(Enum):

... FRÜHLING = auto()

... SOMMER = auto()

... HERBST = auto()

... WINTER = auto()

Es entsteht ein eigener Typ, hier je einer namens Jahreszeiten. Gleiches gilt für die nachfolgenden Größenangaben namens Size.

>>> class Size(Enum):

... XS = auto()

... S = auto()

... M = auto()

... L = auto()

... XL = auto()

... XXL = auto()

...

Die in den Enums vorgegebenen Werte können nicht mehr modifiziert werden und es können auch keine Aufzählungswerte ergänzt oder umdefiniert werden. Eine Aufzählung ist somit eine geordnete Sammlung konstanter Werte.

Möchte man eine solche Konstante verwenden, so geschieht das durch Angabe des Typnamens und des Konstantennamens etwa wie folgt:

>>> shirt_size = Size.XL

>>> shirt_size

<Size.XL: 5>

Der Enum-Typ lässt sich mit einer for-Schleife durchlaufen und jede Enum-Konstante erhält durch auto() einen numerischen Wert, der per value zugreifbar ist:

>>> for name in Jahreszeiten:

... print(name)

... print(name.value)

...

Jahreszeiten.FRÜHLING

1

Jahreszeiten.SOMMER

2

Jahreszeiten.HERBST

3

Jahreszeiten.WINTER

4

Besonderheit von Enum: Werte für Konstanten

Manchmal ist es wünschenswert, nicht nur den Namen der Konstanten, sondern auch beschreibende Eigenschaften vorgeben zu können. Das lernen wir am Beispiel der Modellierung von Himmelsrichtungen kennen. Diese begegnen uns insbesondere im Kontext von zweidimensionalen Datenstrukturen. Dann trägt es zur Lesbarkeit und Verständlichkeit bei, wenn in der Aufzählung neben allen wesentlichen Himmelsrichtungen auch Offsets in x- und y-Richtung definiert sind. Dazu verwenden wir statt auto() ein passendes Tupel:

class Direction(Enum):

N = (0, -1)

NE = (1, -1)

E = (1, 0)

SE = (1, 1)

S = (0, 1)

SW = (-1, 1)

W = (-1, 0)

NW = (-1, -1)

Greifen wir einmal auf die Werte zu:

>>> print(Direction.NE.value)

(1, -1)

>>> ne = Direction.NE

>>> print(ne.value[0], "/", ne.value[1])

1 / -1

6.6Fallunterscheidungen mit match

In vielen Sprachen gibt es zur Definition von Fallunterscheidungen neben der if- auch die switch-Anweisung. Letztere fehlte lange Zeit in Python. In Python 3.10 kommt mit match eine noch mächtigere Variante zur Fallunterscheidung, mit der wir jetzt endlich auch die switch-Anweisung realisieren können. Darüber hinaus ermöglicht match das sogenannte »Pattern Matching«, ein Verfahren, das die Prüfung komplexerer Ausdrücke erlaubt.

Schauen wir uns einige Möglichkeiten von match in ein paar Beispielen an.

Python 3.9.x

Nehmen wir an, wir wollten HTTP-Statuscodes auf ihre Bedeutung abbilden. Das kann man mit einer if-Kaskade wie folgt lösen (hier nur auszugsweise gezeigt):

http_code = 201

if http_code == 200:

print("OK")

elif http_code == 201:

print("CREATED")

elif http_code == 404:

print("NOT FOUND")

elif http_code == 418:

print("I AM A TEAPOT")

else:

print("UNMATCHED CODE")

Allerdings fällt auf, dass dies nicht wirklich gut lesbar ist.

Verbesserung mit Python 3.10

Betrachten wir einmal, wie viel klarer das obige Konstrukt durch den Einsatz von match und der Angabe von Werten hinter case wird – insbesondere kann man mit _ auch einen Wildcard-Fall aufnehmen, der immer dann angesprungen wird, wenn die anderen case nicht passen:

match http_code:

case 200:

print("OK")

case 201:

print("CREATED")

case 404:

print("NOT FOUND")

case 418:

print("I AM A TEAPOT")

case _:

print("UNMATCHED CODE")

Kombination von Werten

Durch den Pipe-Operator (|) ist es möglich, mehrere Werte anzugeben, für die die nachfolgende Aktion ausgeführt werden soll, hier für Donnerstag und Freitag in Kombination sowie für Samstag und Sonntag gezeigt:

def get_info(day):

match day:

case 'Monday':

return "I don't like..."

case 'Thursday' | 'Friday':

return 'Nearly there!'

case 'Saturday' | 'Sunday':

return 'Weekend!!!'

case _:

return 'In Between...'

Komplexeres Matching I

Wir haben gerade gesehen, dass man Werte mit dem Pipe-Operator als Alternativen angeben kann. Es lassen sich dort auch Werteaufzählungen auf Übereinstimmung prüfen:

>>> values = (2,3,4)

>>>

>>> match values:

... case [1,2,3,4]:

... print("4 in a row")

... case [1,2,3] | [2,3,4]:

... print("3 in a row")

... case [1,2,4] | [1,3,4]:

... print("3 but not connected")

... case _:

... print("SINGLE OR DOUBLE")

...

3 in a row

Komplexeres Matching II

Die Möglichkeiten von match sind aber noch mächtiger, was ich hier lediglich andeuten möchte – mit dem Hinweis, dass man nach den Werten beim case noch zusätzliche Bedingungen angeben kann:

def classify(person):

match person:

case (name, age, "male" | Gender.MALE):

print(f"{name} is a man and {age} years old")

case (name, age, "female" | Gender.FEMALE):

print(f"{name} is a woman and {age} years old")

case (name, _, gender) if gender is not None:

print(f"no age specified: {name} is {gender}")

case (name, age, _) if age is not None:

print(f"no gender specified: {name} is"

f" {age} years old.")

Beim match findet das bereits erwähnte Pattern Matching statt. Im case wird nicht nur geprüft, ob eine Übereinstimmung mit dem Pattern vorliegt, sondern es werden auch die angegebenen Variablen mit entsprechenden Werten belegt (Details finden Sie unter https://www.python.org/dev/peps/pep-0622/). Hier dient _ als »Wildcard«-Operator und matcht mit allem.

Der Vollständigkeit halber schauen wir uns noch den Enum für das Geschlecht an:

class Gender(Enum):

MALE = auto()

FEMALE = auto()

Die obige Klassifikation rufen wir einmal mit folgenden Wertekombinationen auf:

classify(("Micha", 50, "male"))

classify(("Lili", 42, Gender.FEMALE))

classify(("NO GENDER", 42, None))

classify(("NO AGE", None, "ALL"))

Das führt zu folgenden Ausgaben:

Micha is a man and 50 years old

Lili is a woman and 42 years old

no gender specified: NO GENDER is 42 years old.

no age specified: NO AGE is ALL

6.7break, continue und else in Schleifen

In diesem Abschnitt wollen wir uns mit break und continue als zwei Möglichkeiten beschäftigen, Schleifendurchläufe abzubrechen bzw. zu überspringen. Zudem stelle ich mit else in Kombination mit Schleifen eine Python-Besonderheit vor.

6.7.1Funktionsweise von break und continue

Mit break kann man aus einer Schleife beim Eintreten einer bestimmten Bedingung herausspringen. Mithilfe von continue wird der aktuelle Durchlauf einer Schleife abgebrochen und diese wird mit der Prüfung im Schleifenkopf fortgeführt.

break in for

Schauen wir uns als Beispiel für ein break eine for-Schleife von 0 bis 10 an. Durch das if und das anschließende break springt man aus der Schleife (diese wird nicht weiter durchlaufen), hier, wenn die Variable i den Wert 4 erreicht:

>>> for i in range(10):

... if i == 4:

... break

... print(i)

...

Dadurch kommt es zu folgenden Ausgaben:

0

1

2

3

continue in for

Mit continue bricht man die aktuelle Iteration (einen Durchlauf) der Schleife ab. Das geschieht oftmals in Kombination mit if, also wenn eine bestimmte Bedingung eintritt. Alle Anweisungen des Schleifenrumpfs nach continue werden übersprungen (nicht mehr ausgeführt) und die Schleife wird mit der nächsten Iteration fortgesetzt.

Wir nutzen wieder eine for-Schleife von 0 bis 10. Durch das if und das continue erfolgt keine Ausgabe für die Werte 2, 4, 6 und 8:

>>> for i in range(10):

... if i == 2 or i == 4 or i == 6 or i == 8:

... continue

... print(i)

...

Eleganter schreibt man diese Prüfung folgendermaßen:

>>> for i in range(10):

... if i in [2, 4, 6, 8]:

... continue

... print(i)

...

In beiden Fällen kommt es zu folgenden Ausgaben:

0

1

3

5

7

9

break in while

Konsistenterweise kann man break und continue auch in while-Schleifen verwenden. Während break analog wie bei for-Schleifen arbeitet, gibt es bei continue einen kleinen Unterschied, den wir nachfolgend am Beispiel sehen werden.

Beginnen wir wieder mit dem break für den Wert 4 wie folgt:

>>> i = 0

>>> while i < 10:

... print(i)

... i += 1

... if i == 4:

... break

...

Dadurch kommt es erwartungsgemäß zu folgenden Ausgaben:

0

1

2

3

Besonderheit: EndlosschleifeAls Besonderheit existiert die Endlosschleife while True. Weil diese Bedingung niemals zu False ausgewertet wird, läuft sie für immer – in diesem Fall stellt break ein geeignetes Mittel dar, die Schleife zu beenden.2

while True:

# Anweisungen

if condition

break

continue in while

Wir nutzen wieder eine Schleife von 0 bis 10. Durch das if und das continue erfolgt keine Ausgabe für die Werte 2, 4, 6 und 8:

>>> i = 0

>>> while i < 10:

... if i == 2 or i == 4 or i == 6 or i == 8:

... i += 1

... continue

... print(i)

... i += 1

...

Dadurch kommt es erwartungsgemäß zu folgenden Ausgaben:

0

1

3

5

7

9

Bitte beachten Sie, dass continue den aktuellen Schleifendurchlauf sofort abbricht. Wenn Sie also eine Schleifenvariable hochzählen wollen, dann müssen Sie das explizit vor dem continue manuell erledigen, weil bei while das Inkrementieren oder Dekrementieren der Schleifenvariablen im Gegensatz zur for-Schleife nicht automatisch erfolgt.

Welcher Implementierungsfehler passiert leicht bei continue?Schnell wird als Implementierungsfehler die Veränderung der Schleifenvariablen vergessen – hier ist zur Verdeutlichung die Veränderung des Schleifenzählers noch als Kommentar verblieben:

>>> i = 0

>>> while i < 10:

... if i == 2 or i == 4 or i == 6 or i == 8:

... # fehlendes Inkrement

... # i += 1

... continue

... print(i)

... i += 1

...

Es kommt dann zu folgender Ausgabe, weil Python in einer Endlosschleife verharrt – die Variable i verweilt ewig beim Wert 2. Es hilft nur noch ein Programmabbruch.

0

1

6.7.2Wie macht man es besser?

Wenn man sich die Konstrukte anschaut, bergen diese doch Potenzial für Missverständnisse. Für das break und das continue kann man zumindest für die while-Schleife eine deutliche Vereinfachung erzielen. Schauen wir uns das Vorgehen an.

Beim break und while

Beim break wird die Schleife abgebrochen. Falls es sich um eine einfache Abfrage handelt, bietet es sich an, die obere Grenze der Schleife anzupassen, also die Schleifenabbruchbedingung passend zu korrigieren.

Diese Ausgangsbasis

>>> i = 0

>>> while i < 10:

... print(i)

... i += 1

... if i == 4:

... break

...

wird dann folgendermaßen transformiert:

>>> i = 0

>>> while i < 4:

... print(i)

... i += 1

...

In beiden Fällen kommt es erwartungsgemäß zu folgenden Ausgaben:

0

1

2

3

Beim break und for

Für die for-Schleife gilt diese Anpassung der oberen Grenze analog. Damit lässt sich

>>> for i in range(10):

... if i == 4:

... break

... print(i)

...

wie folgt vereinfachen:

>>> for i in range(4):

... print(i)

...

In beiden Fällen kommt es erwartungsgemäß zu folgenden Ausgaben:

0

1

2

3

Beim continue und while

Im Fall von continue kann man die Prüfung invertieren und damit auf das continue verzichten. Bedenken Sie, dass beim Invertieren aus or dann and und natürlich aus == dann != wird. Das folgt den sogenannten Regeln von De Morgan (https://de.wikipedia.org/wiki/De-morgansche_Gesetze).

Als Ausgangsbasis dient folgende Schleife:

>>> i = 0

>>> while i < 10:

... if i == 2 or i == 4 or i == 6 or i == 8:

... i += 1

... continue

... print(i)

... i += 1

...

Die ursprüngliche Bedingung

if i == 2 or i == 4 or i == 6 or i == 8:

ändert sich der Argumentation oben zufolge dann zu:

if i != 2 and i != 4 and i != 6 and i != 8:

Setzen wir dies in die ursprüngliche Schleife ein und entfernen auch das continue:

>>> i = 0

>>> while i < 10:

... if i != 2 and i != 4 and i != 6 and i != 8:

... print(i)

... i += 1

...

Wir sehen, wie die Implementierung ein wenig an Klarheit gewinnt und funktional gleich ist, weil es in beiden Fällen erwartungsgemäß zu folgenden Ausgaben kommt:

0

1

3

5

7

9

Weitere VereinfachungBekanntermaßen kann man die or-Verknüpfung eleganter mithilfe einer in-Prüfung schreiben:

if i in [2, 4, 6, 8]:

In dem Fall wird statt der Negation und der and-Verknüpfung eine Negation der in-Prüfung genutzt:

if i not in [2, 4, 6, 8]:

Lassen Sie uns das Ganze einmal ausprobieren: Dann erhalten wir die korrekten Ausgaben, haben aber die Logik in der Schleife vereinfachen und damit leichter nachvollziehbar gestalten können.

>>> i = 0

>>> while i < 10:

... if i not in [2, 4, 6, 8]:

... print(i)

... i += 1

...

0

1

3

5

7

9

Beim continue und for

Verbleibt noch for und continue: Hier lässt sich kein Gewinn durch eine Umformung erzielen.

6.7.3Besonderheit: else in Schleifen

In Python gibt es für Schleifen die Möglichkeit, mit dem Schlüsselwort else einen Block anzugeben, der ausgeführt wird, sofern die Schleife nicht mit break verlassen wurde.

Schauen wir uns dazu ein Beispiel an:

>>> for i in range(1, 7):

... print(i)

... else: # Folgendes print() wird ausgeführt, da kein break in der Schleife

... print("Executed because no break in for loop")

...

Es kommt dann zu folgender Ausgabe:

1

2

3

4

5

6

Executed because no break in for loop

Nun integrieren wir ein break in die Schleife:

>>> for i in range(1, 7):

... print(i)

... if i == 4:

... break

... else: # Folgendes print() wird NICHT ausgeführt, da break in der Schleife

... print("Not executed due to break")

...

Es kommt dann zu folgender Ausgabe:

1

2

3

4

Das Ganze gilt übrigens auch für while-Schleifen:

>>> count = 0

>>> while count < 7:

... count = count + 1

... print(count)

... if count == 4:

... break

... else:

... print("No Break")

...

1

2

3

4

6.8Ausdrücke mit eval() auswerten

Python bietet die Funktion eval() zum dynamischen Auswerten von einfachen Ausdrücken. Somit kann man innerhalb eines Python-Programms wiederum Python ausführen, nämlich den Sourcecode, den man an eval() als Parameter übergibt:

>>> eval("2 + 5")

7

>>> eval("7 > 42")

False

Es ist ebenfalls möglich, eingebaute Funktionalität wie min() oder wie nachfolgend sum() aufzurufen, hier mit einer Liste von Argumenten:

>>> eval("sum([2, 3, 5, 8])")

18

Interessant wird das Ganze, wenn man auch noch Variablen miteinbezieht:

>>> x = 14

>>> y = 2

>>> eval("x / y")

7.0

Das geht natürlich auch in Kombination mit einer Funktion:

>>> eval("min(x, y)")

2

Und sogar wie folgt:

>>> eval("print('Minimum: ', min(x, y))")

Minimum: 2

Ergänzend kann man Variablen auch in Form eines Dictionaries übergeben:

>>> eval("x + y + z", {"x": 1, "y": 2, "z": 3})

6

Abschließend noch ein Wort der Warnung: Bitte bedenken Sie, dass durch die Ausführung beliebigen Sourcecodes natürlich auch Sicherheitsrisiken entstehen. Nutzen Sie eval() daher mit besonderer Vorsicht und nur im lokalen Kontext mit Werten, die Sie unter Kontrolle haben.

6.9Rekursion

Rekursion ist eine Methode, bei der eine Funktion sich selbst aufruft. Das mag zunächst merkwürdig klingen, aber diese Vorgehensweise ermöglicht es, komplizierte Probleme in einfachere Teilprobleme zu zerlegen, die leichter zu lösen sind.

Rekursion ist möglicherweise anfangs ein wenig schwierig zu verstehen. Der beste Weg zur Erkenntnis ist, damit zu experimentieren.

6.9.1Einführendes Beispiel

Die Berechnung der Fakultät ist ein Beispiel für eine einfache rekursive Definition. Mathematisch ist die Fakultät für eine positive Zahl n als das Produkt (also die Multiplikation) aller natürlichen Zahlen von 1 bis einschließlich n definiert. Zur Notation wird das Ausrufezeichen der entsprechenden Zahl nachgestellt. Beispielsweise steht 5! für die Fakultät der Zahl 5:

image

Dies lässt sich wie folgt verallgemeinern:

image

Basierend darauf ergibt sich die rekursive Definition:

image

Dabei steht das umgedrehte »A« () für »für alle«.

Dies lässt sich ziemlich direkt in Python übertragen:

>>> def fac(n):

... if n == 0 or n == 1:

... return 1

... print("calling fac(" + str(n - 1) + ")")

... return n * fac(n - 1)

...

Rufen wir das mal für den Wert 5 auf und vollziehen die Selbstaufrufe anhand der Konsolenausgaben nach:

>>> fac(5)

calling fac(4)

calling fac(3)

calling fac(2)

calling fac(1)

120

Was passiert unter der Motorhaube? Wenn fac() aufgerufen wird, dann wird der aktuelle Wert mit der Fakultät für den um eins verringerten Wert multipliziert. Damit ergeben sich etwa folgende Schritte:

5 * fac(4)

5 * 4 * fac(3)

...

5 * 4 * 3 * 2 * fac(1)

5 * 4 * 3 * 2 * 1

6.9.2Weiterführendes Beispiel: Fibonacci-Zahlen

Auch die Fibonacci-Zahlen lassen sich hervorragend rekursiv definieren, wobei die Formel ein klein wenig komplexer ist:

image

Es ergibt sich für die ersten n folgender Werteverlauf:

image

Wenn man sich die Berechnungsvorschrift grafisch verdeutlicht, dann wird schnell klar, wie weit sich die Hierarchie der Selbstaufrufe potenziell aufspannt – für größere n wäre das Ganze viel ausladender, wie es durch die gestrichelten Pfeile angedeutet ist:

image

Abb. 6–1: Fibonacci rekursiv

Selbst bei diesem exemplarischen Aufruf erkennt man, dass diverse Aufrufe mehrmals erfolgen, etwa für fib(n − 4) und fib(n − 2), aber insbesondere dreimal für fib(n − 3). Das führt sehr schnell zu aufwendigen und langwierigen Berechnungen. Wie sich dies optimieren lässt, erfahren Sie in meinem Buch »Python Challenge« [2]. Dieses Buch behandelt die Thematik sehr detailliert und zeigt neben einführenden Beispielen auch richtig vertrackte Problemstellungen und wie man diese mit Rekursion lösen kann.

6.9.3Praxisbeispiel: Flächen füllen

Wir wollen das bislang gesammelte Wissen zu mehrdimensionalen Listen und vor allem das gerade erworbene Wissen über Rekursion nun einsetzen, um eine Funktion flood_fill() zu implementieren, die in einem Spielfeld, gegeben durch eine zweidimensional verschachtelte Liste, alle freien Felder mit einem bestimmten Wert befüllt.

Beispiel

Nachfolgend ist der Füllvorgang für das Zeichen '*' gezeigt. Das Füllen beginnt an einer vorgegebenen Position, etwa in der linken oberen Ecke, und wird dann so lange in alle vier Himmelsrichtungen fortgesetzt, bis die Grenzen der Liste oder eine Begrenzung in Form eines anderen Zeichens gefunden wird:

" # " "***# " " # #" " #******#"

" #" "****#" " # #" " #******#"

"# #" => "#***#" "# # #" => "# #*****#"

" # # " " #*# " " # # #" " # #*****#"

" # " " # " " # #" " #*****#"

Algorithmus

Um die Füllung zu realisieren, beginnen wir mit dem Startfeld. Ist das Feld leer, füllen wir es und prüfen wiederum dessen vier Nachbarn in den vier Himmelsrichtungen. Erreichen wir die Grenzen oder ein gefülltes Feld, so stoppen wir:

def flood_fill(values2dim, x, y):

# rekursiver Abbruch

if x < 0 or y < 0 or \

x >= len(values2dim[0]) or y >= len(values2dim):

return

if values2dim[y][x] == ' ':

values2dim[y][x] = '*'

# rekursiver Abstieg: fülle in alle 4 Richtungen

flood_fill(values2dim, x, y - 1)

flood_fill(values2dim, x + 1, y)

flood_fill(values2dim, x, y + 1)

flood_fill(values2dim, x - 1, y)

Prüfung

Nun wollen wir das rechte der einleitend gezeigten Muster als Ausgangsbasis definieren und dann eine Füllung von Position (5, 0) ausgehend vornehmen:

second_world = [list(" # # "),

list(" # #"),

list("# # # "),

list(" # # # "),

list(" # # ")]

print2dnice(second_world)

print("-------- Filling ---------")

flood_fill(second_world, 5, 0)

print2dnice(second_world)

Zum leichteren Nachvollziehen wird die Ausgabemethode hier nochmals gezeigt:

def print2dnice(values):

for line in values:

print("".join(line))

Dadurch kommt es zu folgenden Ausgaben, die die korrekte Funktionalität des Flächenfüllens zeigen:

# #

# #

# # #

# # #

# #

-------- Filling ---------

#******#

#******#

# #*****#

# #*****#

#*****#