Kontrollstrukturen sind das A und O der Programmierung. Wir unterscheiden die Strecke (aufeinanderfolgende Anweisungen), Verzweigungen und Wiederholungen. Die Strecke kennst Du schon. Das sind aufeinander folgende Anweisungen, die in der Reihenfolge ihrer Notation ausgeführt werden. Spannend wird es, wenn wir in Abhängigkeit von Bedingungen alternative Ausführungspfade beschreiten und Anweisungen wiederholt ausführen können.

Während wir uns die grundlegenden Datentypen vor allem mithilfe der Python-Shell angesehen haben, schreiben wir ab jetzt vorwiegend richtige Programme. Vorerst sind das kurze Programme. Du wirst aber sehen, in Bälde auch umfangreichere Programme schreiben zu können, Programme, die wirklich sinnvolle Dinge leisten. – Die Grundzutat sind Kontrollstrukturen.

Fallunterscheidungen

Python bietet uns zwei Möglichkeiten, Fallunterscheidungen zu implementieren. Das sind die klassische if-Anweisung und der bedingte Ausdruck.

if-Anweisung

Bei der if-Anweisung wird eine Bedingung formuliert, in deren Abhängigkeit eine oder mehrere Anweisungen ausgeführt werden. Die abhängigen Anweisungen werden ausgeführt, wenn die Bedingung wahr ist. Die Bedingung kann beliebig komplex sein.

Einrückungen: Einrückungen haben in Python eine syntaktische Bedeutung. Sie drücken Abhängigkeiten aus. Einrückungen sollten stets vier Leerzeichen betragen und sind einheitlich im Programmcode zu handhaben.

Syntax der einfachen if-Anweisung:

if BEDINGUNG:
    ANWEISUNG
    ...

Hinter der Bedingung ist ein Doppelpunkt zu notieren. Die von der Bedingung abhängigen Anweisungen sind eingerückt.

Kommt der Programmablauf zur if-Verzweigung, wird deren Bedingung geprüft. Ist die Bedingung wahr, wird der eingerückte Programmblock ausgeführt. Er kann aus einer oder auch mehreren Anweisungen bestehen. Sind die Anweisungen abgearbeitet, wird das Programm hinter der if|Verzweigung fortgesetzt. Ist die if-Bedingung nicht zutreffend, wird der eingerückte Block übersprungen und es geht direkt hinter der if-Verzweigung weiter.

Wenn mehrere Bedingungen sequentiell zu prüfen sind, im Sinne einer Wenn-Dann-Sonst-Kette, die jeweils alternative Programmabläufe zur Folge haben sollen, kann die if-Bedingung um beliebig viele elif-Verzweigungen ergänzt werden.

Syntax der erweiterten if-Anweisung:

if BEDINGUNG_1:
    ANWEISUNG
    ...
elif BEDINGUNG_2:
    ANWEISUNG
    ...
elif BEDINGUNG_3:
    ANWEISUNG
    ...
...

Zuletzt gibt es einen optionalen else-Zweig. Die darin notierten Anweisungen werden ausgeführt, wenn keine der vorher formulierten Bedingungen zutreffend war. else kann sowohl mit der einfachen wie auch der erweiterten if-Verzweigung genutzt werden.

Syntax des optionalen else-Zweiges am Beispiel der erweiterten if-Anweisung:

if BEDINGUNG_1:
    ANWEISUNG
    ...
elif BEDINGUNG_2:
    ANWEISUNG
    ...
...
else:
    ANWEISUNG
    ...

Implementieren wir ein einfaches Beispielprogramm. Es liest eine ganze Zahl über die Kommandozeile ein und prüft, ob diese Zahl gerade oder ungerade ist. Gerade Zahlen lassen sich ganzzahlig ohne Rest durch 2 teilen.

Der Zugriff auf sys.argv[1] könnte zu einem Fehler führen, wenn kein Kommandozeilenargument angegeben wurde. Das kann jedoch nicht passieren. Fehlt ein Kommandozeilenargument, so ist die Bedingung, welche die Länge des Kommandozeilenvektors prüft, wahr. – Du erinnerst Dich: Die Länge von sys.argv ist 2, wenn ein Kommandozeilenargument angegeben wurde. – Wurde nicht mindestens ein Argument übergeben, so bricht das Programm ab.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

# primitive Prüfung, ob Programmaufruf (halbwegs) korrekt erfolgt ist
if len(sys.argv) < 2:
        print("Aufruf: gerade.py GANZZAHL")
        sys.exit(1)  # Programm beenden (Status: 1)


# wenn hier angekommen, wurde mindestens ein Argument übergeben
# - lässt sich das Kommandozeilenargument in keine Ganzzahl wandeln,
#   kommt es zu einem Fehler!
zahl = int(sys.argv[1])
if zahl % 2 == 0:    # Zahl gerade?
    print("gerade")
else:                # sonst
    print("ungerade")

sys.exit() bewirkt ein sofortiges Programmende. Dabei kann ein ganzzahliger Statuscode übergeben werden, den die aufrufende Shell auswerten kann. 0 signalisiert üblicherweise einen korrekten Programmablauf. Für die Signalisierung von Fehlern gibt es keine weiteren Konventionen. Du kannst Dir eigene Fehlernummern ausdenken, um verschiedene Gründe einer Programmbeendigung analysieren zu können.

Bedingter Ausdruck

Bei einfachen Fallunterscheidungen, bei denen nur zwei Fälle zu unterscheiden sind, und je Fall ein bestimmter Wert zugewiesen werden soll, können wir statt einer if-Verzweigung einen bedingten Ausdruck verwenden. Dabei sei x eine ganze Zahl.

if x >= 0:
    y = x
else:
    y = -x

Augenscheinlich wird y der Betrag von x zugewiesen. Das geht mit einem bedingten Ausdruck auch kürzer:

y = x if x >= 0 else -y

Ein bedingter Ausdruck ist letztlich nur eine Kurzschreibweise für eine einfache ifelse-Anweisung in Verbindung mit einer Zuweisung.

Schleifen

Grundform der while-Schleife

Die spannendsten Kontrollstrukturen sind sicherlich Schleifen. Sie sind aber eventuell auch kritisch. Schleifen bergen die potenzielle Gefahr, dass wir Programme formulieren, die nicht terminieren.

Die while-Schleife ist syntaktisch der if-Verzweigung sehr ähnlich. Statt if notieren wir jedoch while. Das bedeutet dann, den eingerückten Anweisungsblock so oft wiederholt auszuführen, wie die Schleifenbedingung erfüllt ist. Nach jedem Durchgang des eingerückten Anweisungsblocks wird die Schleifenbedingung geprüft. Die Schleife bricht ab, wenn die Bedingung falsch ist. Die Schleife wird gar nicht erst betreten, wenn die Schleifenbedingung von Beginn an falsch ist.

Ein einfaches Spiel zahlenraten.py

verdeutlicht die Verwendung der while-Schleife. Das Programm erzeugt eine pseudo-zufällige Ganzzahl im Intervall \([1, 1000]\). Dazu wird die Funktion randint() aus dem Modul random genutzt. Die Funktion erwartet einen Start- und einen Endwert. Letzterer muss um 1 größer sein als die größte zu generierende Zahl.

Der Anwender unseres Beispielprogramms soll die Zahl raten, welche vorher zufällig generiert wurde. Seine Rateversuche lesen wir mit input() ein. Nach jedem Versuch geben wir dem Ratenden aus, ob sein Tipp zu groß oder zu klein war. Die Anzahl der Rateversuche wird protokolliert und am Ende ausgegeben.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import random                          # Pseudo-Zufallszahlen

zufallszahl = random.randint(1, 1001)  # Zahl im Intervall [1, 1000]
zaehler = 0                            # noch kein Versuch
geratene_zahl = 0                      # Initialisierung

# weil 'geratene_zahl' = 0  und 'zufallszahl' in [1, 1000]
# ist die Schleifenbedinung zu Beginn garantiert erfüllt
while geratene_zahl != zufallszahl:
    geratene_zahl = int(input("Ihr Tipp: "))
    zaehler += 1                       # Anzahl der Versuche zählen
    if geratene_zahl > zufallszahl:
        print("Die gesuchte Zahl ist kleiner.")
    elif geratene_zahl < zufallszahl:
        print("Die gesuchte Zahl ist größer.")

print("Anzahl der Versuche: " + str(zaehler))

Die zum Verständnis des Programms erforderlichen Erläuterungen sind als Kommentare im Programmcode eingefügt. Weil sich Zeichenketten nicht mittels +-Operator mit einer ganzen Zahl verknüpfen lassen, nutzen wir str(). Der Konstruktor für Zeichenketten konstruiert aus seinem Argument eine Zeichenkette.

Hier eine Beispielsitzung die zeigt, wie ein Programmlauf aussehen kann.

$ zahlenraten.py
Ihr Tipp: 500
Die gesuchte Zahl ist größer.
Ihr Tipp: 750
Die gesuchte Zahl ist größer.
Ihr Tipp: 875
Die gesuchte Zahl ist kleiner.
Ihr Tipp: 816
Die gesuchte Zahl ist größer.
Ihr Tipp: 845
Die gesuchte Zahl ist kleiner.
Ihr Tipp: 831
Die gesuchte Zahl ist größer.
Ihr Tipp: 838
Die gesuchte Zahl ist kleiner.
Ihr Tipp: 835
Die gesuchte Zahl ist größer.
Ihr Tipp: 836
Anzahl der Versuche: 9

Hinweis: Computer können keine Zufallszahlen generieren. Mithilfe entsprechender Algorithmen lassen sich jedoch Zahlen generieren, die sich wie Zufallszahlen verhalten. Wir nennen sie Pseudo-Zufallszahlen.

whileelse

Im Gegensatz zu den meisten anderen Programmiersprachen, weist die while-Schleife in Python eine Besonderheit auf. Sie kann einen optionalen else-Zweig besitzen. Dieser Codeblock wird nur dann einmalig ausgeführt, wenn die Schleifenbedingung erstmalig False ergibt und die Schleife damit ordnungsgemäß, aufgrund der nicht mehr zutreffenden Bedingung, beendet wird. Der else-Zweig kommt nicht zur Ausführung, wenn die Schleifenbedingung von vornherein falsch war/ist.

Wir wollen uns diese Option anhand des letzten Beispiels, zahlenraten.py, ansehen. Das Programm soll dahingehend erweitert werden, dem Anwender eine Abbruchmöglichkeit zu geben, wenn er keine Lust mehr hat, weiter zu raten. Die Eingabe einer 0 soll das Programm beenden. Die Zahl 0 ist geeignet, weil sie nicht im Intervall der zu erratenden Zahlen liegt. Das neue Programm soll zahlenraten2.py heißen.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import random                          # Pseudo-Zufallszahlen

zufallszahl = random.randint(1, 1001)  # Zufallszahl
zaehler = 0                            # noch kein Versuch
geratene_zahl = 0                      # Initialisierung

# weil 'geratene_zahl' = 0  und 'zufallszahl' in [1, 1000]
# ist die Schleifenbedinung zu Beginn garantiert erfüllt
while geratene_zahl != zufallszahl:
    geratene_zahl = int(input("Ihr Tipp (ABBRUCH=0): "))
    zaehler += 1            # Anzahl der Versuche zählen
    if geratene_zahl == 0:  # Abbruchbedingung prüfen
        print("ABBRUCH")
        break               # Schleife vorzeitig beenden
    # Anwender will weiter machen
    if geratene_zahl > zufallszahl:
        print("Die gesuchte Zahl ist kleiner.")
    elif geratene_zahl < zufallszahl:
        print("Die gesuchte Zahl ist größer.")
else:
    # Ausgabe erfolgt nur, wenn Zahl erfolgreich geraten
    print("Anzahl der Versuche: " + str(zaehler))

Die break-Anweisung beendet eine Schleife vorzeitig. Im nächsten Abschnitt werden wir sie detaillierter behandeln. Im Moment reicht es zum Verständnis, wenn Sie wissen, dass break die Schleife abbricht. Weil der Abbruch jedoch nicht auf eine falsche Schleifenbedingung zurückzuführen ist, wird der else-Zweig der while-Schleife nicht ausgeführt. Dieser wird nur dann ausgeführt, wenn der Anwender die gesuchte Zahl erraten und damit die Schleifenbedingung den Wert False angenommen hat.

Beispielsitzungen:

$ zahlenraten2.py
Ihr Tipp (ABBRUCH=0): 500
Die gesuchte Zahl ist größer.
Ihr Tipp (ABBRUCH=0): 725
Die gesuchte Zahl ist größer.
Ihr Tipp (ABBRUCH=0): 0
ABBRUCH
$ zahlenraten2.py
Ihr Tipp (ABBRUCH=0): 500
Die gesuchte Zahl ist kleiner.
Ihr Tipp (ABBRUCH=0): 250
Die gesuchte Zahl ist größer.
Ihr Tipp (ABBRUCH=0): 375
Anzahl der Versuche: 3

for-Schleife

Die for-Schleife ist das zweite Schleifenkonstrukt in Python. Im Vergleich zu gleichnamigen Schleifenkonstrukten in anderen Programmiersprachen, ist diese Schleife in Python ein wenig speziell. Sie iteriert über eine gegebene Anzahl von Objekten. In vielen anderen Programmiersprachen wird daher auch, diese spezielle Form der for-Schleife betreffend, von einer for-each-Schleife gesprochen.

Zur Verdeutlichung betrachten wir ein etwas gekünsteltes Programm argumente.py. Das Programm soll die ihm übergebenen Kommandozeilenargumente zeilenweise ausgeben.

$ argumente.py Karsten Brodmann "Susi Sorglos" 1 2 3
Karsten
Brodmann
Susi Sorglos
1
2
3
$ _

Du weißt bereits, dass die Kommandozeilenargumente in einer Liste namens sys.argv in Form von Zeichenketten abgelegt sind. Listen sind iterierbar. Das bedeutet eine Liste elementweise durchlaufen zu können. Die Konstruktion

for VARIABLE in ITERIERBARES_OBJEKT:
    ANWEISUNG
    ...
    ANWEISUNG

tut genau dies. Neben Listen sind auch verschiedene andere Objekte iterierbar. Später wirst Du selbst Objekte mit dieser Eigenschaft implementieren können.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

for argument in sys.argv:
    print(argument)

In Bezug auf das Beispiel ist es jedenfalls so, dass alle Elemente der Liste, beginnend bei Index 0, bis zu deren Ende durchlaufen und ausgegeben werden. Dabei benötigen wir den Index nicht. Ein sogenannter Iterator, der für Listen implementiert ist, sorgt dafür, die Elemente der Liste von Anfang bis Ende zu liefern. Die Elemente werden im Kopf der for-Schleife nach und nach der Variablen argument zugewiesen. Mit dieser Variablen wird im Schleifenrumpf gearbeitet. Sind alle Elemente des iterierbaren Objekts, hier der Liste sys.argv, durchlaufen, endet die Schleife.

Schleifen vorzeitig beenden oder fortsetzen

Schleifen und die darin enthaltenen Anweisungen müssen nicht zwangsläufig immer vollständig durchlaufen werden. Innerhalb einer Schleife können Bedingungen eintreten, die es unsinnig machen, bestimmte Anweisungen innerhalb der Schleife noch auszuführen. Eventuell kann es sogar möglich sein, eine Schleife komplett zu beenden, auch wenn die Schleifenbedingung noch wahr ist.

Das Programm textkopieren.py liest die Standardeingabe. Dies tut es, solange noch Bytes daraus gelesen werden können. Das Ende der Standardeingabe wird durch eine leere Zeichenkette dargestellt. Sie ist in Python generell das Symbol für ein Dateiende.

Im Beispielprogramm findet keine großartige Verarbeitung statt. Es wird lediglich die Standardeingabe byteweise in die Standardausgabe kopiert, so dass wir mithilfe von Eingabe-/Ausgabeumlenkungen eine Datei kopieren können.

Eine Verarbeitung ist nur möglich, wenn ein Byte zur Verarbeitung vorhanden ist. Bei Erreichen des Dateiendes, ist das nicht mehr der Fall. Ein break beendet dann die Schleife und die Programmausführung wird hinter der Schleife fortgesetzt.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

while True:
    b = sys.stdin.read(1)   # ein Byte aus Standardeingabe lesen
    if b == '':             # Dateiende erreicht?
        break               # wenn ja, abbrechen

    # ok, es wurde erfolgreich ein Zeichen gelesen
    # --> Verarbeitung: hier nur 1:1 ausgeben
    print(b, end='')

Für das Lesen der Standardeingabe gibt es im Modul sys eine Variable stdin. Sie stellt eine Abstraktion der Standardeingabe dar und bietet uns eine Methode namens read(). Dieser übergeben wir als Parameter die Anzahl an Bytes, die bei einem Leseversuch gelesen werden sollen.

Nun mag es unverständlich sein, die Eingabe Byte für Byte zu lesen, wenn einige Zeichen, wie beispielsweise die deutschen Umlaute, aus mehr als einem Byte bestehen. Das ist insoweit unproblematisch, als ein Zeichenstrom letztlich auch nur aus einer Sequenz von Bytes besteht. Und genau einen solchen geben wir aus, wenn wir jedes gelesene Byte unverändert wieder ausgeben.

Die Schleifenkonstruktion ist hinsichtlich ihrer „Bedingung“ interessant. True ist ein konstanter Wahrheitswert. Die Schleifenbedingung ist folglich stets wahr. Die Schleife kann nicht abbrechen, ist eine Endlosschleife. Der Schleifenabbruch erfolgt mittels break, weil diese Anweisung, wenn sie ausgeführt wird, den Schleifenrumpf verlässt.

Beispielsitzung (Programm auf den eigenen Quellcode angewendet):

$ textkopieren.py <textkopieren.py 
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

while True:
    b = sys.stdin.read(1)   # ein Byte aus Standardeingabe lesen
    if b == '':             # Dateiende erreicht?
        break               # wenn ja, abbrechen

    # ok, es wurde erfolgreich ein Zeichen gelesen
    # --> Verarbeitung: hier nur 1:1 ausgeben
    print(b, end='')

Analog zu break gibt es continue. Auch hier werden folgende Anweisungen nicht ausgeführt. Jedoch wird die Schleife nicht abgebrochen. Es beginnt ein neuer Schleifendurchlauf.

break und continue werden in Bezug auf for-Schleifen ganz analog verwendet.

Vielleicht stört Dich im obigen Programm der byteweise Zugriff. Du möchtest wirklich zeichenweise lesen. Das ist in Python so einfach nicht machbar. Der folgende Workaround ist aber fast immer eine gute Lösung. Wir betrachten zuerst das zeilenweise Lesen. sys.stdin kann nämlich zeilenweise iteriert werden. Jede Zeile ist eine Zeichenkette.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

for zeile in sys.stdin:
    print(zeile, end='')

Und weil Zeichenketten ihrerseits zeichenweise iteriert werden können, gönnen wir uns eine zweite for-Schleife zu diesem Zweck.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

for zeile in sys.stdin:
    for zeichen in zeile:
        print(zeichen, end='')

Jede gelesene Textzeile endet mit einem Zeilenumbruch. sys.stdin liest und liefert jede Zeile vollständig. Will man den Zeilenumbruch loswerden, so hilft die Methode strip(), die jedem String-Objekt zur Verfügung steht. Mit ihrer Hilfe kann man Whitespaces am Beginn und am Ende einer Zeichenkette entfernen. strip() liefert ein neues Zeichenkettenobjekt.

Diese Implementierungen setzen Textdateien voraus!

WORK IN PROGRESS …