| Zeile / Konstrukt | Grundstruktur | Bemerkung |
|---|---|---|
i = 0, fertig = False, total = 0 | Sequenz | Drei Zustandsvariablen, die von Hand gepflegt werden müssen. |
while not fertig | Iteration | Endlosschleife mit Abbruchflagge statt echter Laufbedingung. |
if i >= len(bestellungen) | Selektion | Wird nur gebraucht, weil die Schleife nicht über die Elemente läuft. |
if b["status"] == "storniert" und if b["menge"] <= 0 | Selektion | Sachlich sinnvoll, aber unnötig verschachtelt. |
continue | keine der drei | Imitiert einen Sprung an den Schleifenanfang – genau das, was die strukturierte Programmierung vermeiden will. |
i = i + 1 an drei Stellen | Sequenz | Klassische Fehlerquelle: Eine vergessene Stelle erzeugt eine Endlosschleife. |
fertig = True | Sequenz | Flaggenvariable als Ersatz für eine Schleifenbedingung. |
"""LU01.L11 - Musterloesung: Spaghetticode entwirren.""" from referenz import BESTELLUNGEN def ist_verrechenbar(bestellung): """ Entscheidet, ob eine Bestellung zum Umsatz zaehlt. :param bestellung: dict mit artikel, status, menge, preis :return: True oder False """ return bestellung['status'] != 'storniert' and bestellung['menge'] > 0 def umsatz(bestellungen): """ Summiert Menge mal Preis ueber alle verrechenbaren Bestellungen. :param bestellungen: Liste von Bestellungen :return: Umsatz als Zahl, bei leerer Liste 0 """ total = 0 for bestellung in bestellungen: if ist_verrechenbar(bestellung): total += bestellung['menge'] * bestellung['preis'] return total def main(): """Gibt den Umsatz der Beispielbestellungen aus.""" print(f'Umsatz neu: {umsatz(BESTELLUNGEN):.2f}') if __name__ == '__main__': main()
Übrig bleiben genau die drei Grundstrukturen: Sequenz (Initialisierung und Rückgabe), Iteration (for), Selektion (if).
i. Damit auch jede Möglichkeit, ihn falsch zu initialisieren, an einer Stelle nicht hochzuzählen (Endlosschleife!) oder um eins danebenzuliegen.fertig. Die for-Schleife kennt ihr Ende selbst.continue liest sich der Ablauf von oben nach unten.
Die Gleichwertigkeit prüft test_umsatz_wie_referenz gegen umsatz_alt aus referenz.py – über vier Datensätze, inklusive leerer Liste, komplett stornierter Liste und ungültiger Mengen.
Die Struktur lässt sich mit einem gewöhnlichen Test nicht messen: Beide Fassungen liefern 362.70. Darum lesen zwei Tests die Datei main.py als Syntaxbaum ein:
"""Tests zu LU01.A11 - Spaghetticode entwirren.""" import ast import pathlib import pytest import main import referenz ALLE_STORNIERT = [ {'artikel': 'Maus', 'status': 'storniert', 'menge': 5, 'preis': 10.0}, {'artikel': 'Dock', 'status': 'storniert', 'menge': 1, 'preis': 99.0}, ] MENGE_UNGUELTIG = [ {'artikel': 'Maus', 'status': 'offen', 'menge': 0, 'preis': 10.0}, {'artikel': 'Dock', 'status': 'offen', 'menge': -2, 'preis': 99.0}, ] def syntaxbaum(): """Liest main.py als Syntaxbaum ein.""" pfad = pathlib.Path(__file__).with_name('main.py') return ast.parse(pfad.read_text(encoding='utf-8')) def knoten(*typen): """Sammelt alle Knoten der angegebenen Typen aus main.py.""" return [k for k in ast.walk(syntaxbaum()) if isinstance(k, typen)] def test_umsatz_beispiel(): assert main.umsatz(referenz.BESTELLUNGEN) == pytest.approx(362.70) def test_umsatz_leer(): assert main.umsatz([]) == 0 def test_umsatz_alle_storniert(): assert main.umsatz(referenz.BESTELLUNGEN) == pytest.approx(362.70) assert main.umsatz(ALLE_STORNIERT) == 0 def test_umsatz_menge_ungueltig(): assert main.umsatz(referenz.BESTELLUNGEN) == pytest.approx(362.70) assert main.umsatz(MENGE_UNGUELTIG) == 0 def test_umsatz_wie_referenz(): for daten in (referenz.BESTELLUNGEN, [], ALLE_STORNIERT, MENGE_UNGUELTIG): assert main.umsatz(daten) == pytest.approx(referenz.umsatz_alt(daten)) def test_ist_verrechenbar(): assert main.ist_verrechenbar({'status': 'offen', 'menge': 3, 'preis': 1.0}) is True assert main.ist_verrechenbar({'status': 'geliefert', 'menge': 1, 'preis': 1.0}) is True assert main.ist_verrechenbar({'status': 'storniert', 'menge': 3, 'preis': 1.0}) is False assert main.ist_verrechenbar({'status': 'offen', 'menge': 0, 'preis': 1.0}) is False def test_iteration_ohne_while(): assert main.umsatz(referenz.BESTELLUNGEN) == pytest.approx(362.70) assert knoten(ast.For), 'main.py enthaelt keine for-Schleife' assert not knoten(ast.While), 'main.py enthaelt noch eine while-Schleife' def test_keine_spruenge(): assert main.umsatz(referenz.BESTELLUNGEN) == pytest.approx(362.70) assert not knoten(ast.Continue, ast.Break), 'main.py enthaelt noch continue oder break'
test_iteration_ohne_while verlangt eine for-Schleife und verbietet while, test_keine_spruenge verbietet continue und break. Beide prüfen zusätzlich das korrekte Ergebnis – sonst wären sie mit einer leeren Datei erfüllbar.
Eine Abgabe, die richtig rechnet, aber die alte Struktur behält, kommt damit auf 9 von 11 Testpunkten.
Entfernt man beispielsweise das i = i + 1 im Storno-Zweig, bleibt die Schleife für immer bei derselben stornierten Bestellung stehen – eine Endlosschleife. Das Programm hängt ohne Fehlermeldung. In der neuen Fassung ist dieser Fehler gar nicht mehr möglich, weil die Iteration nicht mehr von Hand gesteuert wird.
Der Ausgangscode brauchte scheinbar Sprünge (continue), um einen Fall zu überspringen. Die neue Fassung zeigt: Dieselbe Wirkung entsteht durch Selektion – statt zu überspringen, wird nur im passenden Fall etwas getan. Das ist genau die Aussage des Theorems: Jeder berechenbare Algorithmus lässt sich allein mit Sequenz, Selektion und Iteration ausdrücken. Sprünge sind bequem, aber nie zwingend nötig.
Der Umsatz ist die Summe aus Menge mal Preis über alle nicht stornierten Bestellungen mit positiver Menge.
In Python direkt so ausdrückbar:
def umsatz_deklarativ(bestellungen): return sum(b['menge'] * b['preis'] for b in bestellungen if ist_verrechenbar(b))
Weder Zwischensumme noch Schleifenkörper sind noch sichtbar – der Code liest sich wie der Satz darüber. Die Bausteine dahinter (map, filter, reduce, Comprehensions) behandeln wir ausführlich in LU04.