LU01a - Deklarative vs. Imperative Programmierung
Einstieg: Taxi oder Navi?
Stellen Sie sich vor, Sie sitzen in einem Taxi. Es gibt zwei Arten, ans Ziel zu kommen:
- „Fahren Sie 300 Meter geradeaus, dann links, an der Ampel rechts, nach dem Kreisel die zweite Ausfahrt …„
- „Zum Hauptbahnhof, bitte.“
Beide bringen Sie ans Ziel. Im ersten Fall bestimmen Sie jeden Schritt – Sie müssen den Weg kennen und tragen die Verantwortung für jeden Abzweiger. Im zweiten Fall beschreiben Sie nur das Ziel – wie der Fahrer dorthin kommt, ist seine Sache.
Genau das ist der Unterschied zwischen imperativer und deklarativer Programmierung. Der erste Stil beschreibt das Wie, der zweite das Was.
Diskussionsfrage für den Einstieg
Wann ist welche Variante besser? Sammeln Sie Argumente, bevor Sie weiterlesen.
Mögliche Antworten: Bei einer Baustelle auf der Strecke will man vielleicht selbst steuern (Kontrolle). Kennt man den Weg nicht, ist die Zielangabe klar überlegen (Abstraktion). Der Taxifahrer kennt zudem Abkürzungen, auf die man selbst nie käme (Optimierung durch das System).
Aktivität: Der menschliche Roboter (ohne Computer, ca. 10 Minuten)
Diese Übung macht den Unterschied körperlich erfahrbar.
Aufbau: Eine Person spielt den Roboter, eine Person gibt die Befehle. Ziel: Der Roboter soll einen Stift vom Pult holen und der Kommandogeberin überreichen.
| Runde | Erlaubte Sprache | Beobachtung |
|---|---|---|
| 1 – Imperativ | Nur Einzelbefehle: SCHRITT VOR, DREHE LINKS 90°, ARM HEBEN, GREIFEN, ARM SENKEN, LOSLASSEN. Kein Nennen von Objekten oder Zielen. | Es braucht viele Befehle. Ein Zählfehler („noch ein Schritt„) führt zum Absturz. Die Kommandogeberin muss den Zustand des Roboters ständig im Kopf mitführen. |
| 2 – Deklarativ | Nur ein einziger Satz, der das Ergebnis beschreibt: Gib mir den roten Stift vom Pult. | Ein Befehl genügt. Der Roboter (also das „System“) entscheidet selbst über Weg, Reihenfolge und Griff. Dafür gibt die Kommandogeberin die Kontrolle über das Wie ab. |
Auswertung im Plenum:
- In welcher Runde war der Befehl kürzer? (Runde 2)
- In welcher Runde wussten Sie genau, was passieren wird? (Runde 1)
- Was passiert in Runde 2, wenn zwei rote Stifte auf dem Pult liegen? (Das System muss entscheiden – Mehrdeutigkeit ist der Preis der Abstraktion.)
- Welche Runde war anfälliger für Fehler? (Runde 1 – mehr Einzelschritte, mehr Fehlerquellen.)
Imperative Programmierung
Imperative Programmierung wird oft als die traditionellste Art der Programmierung betrachtet. Sie ist eine Art Anweisungsliste oder Rezept, das dem Computer sagt, was er tun soll.
- Schritt-für-Schritt-Ansatz: Der Programmierer gibt eine genaue Sequenz von Anweisungen, die in einer bestimmten Reihenfolge ausgeführt werden sollen.
- Statusverwaltung: Der Zustand des Programms wird häufig durch Variablen verändert, die Werte speichern und ändern.
- Kontrollstrukturen: Schleifen und bedingte Anweisungen werden verwendet, um die Kontrolle über den Programmablauf zu steuern.
- Beispiel: Ein Sortieralgorithmus wie Bubble Sort, der jeden Schritt im Detail beschreibt.
Deklarative Programmierung
Deklarative Programmierung ist eher eine Art, dem Computer zu sagen, was er erreichen soll, ohne zu beschreiben, wie er es tun soll.
- Was, nicht Wie: Der Schwerpunkt liegt auf dem, was erreicht werden soll, nicht auf dem, wie es erreicht werden soll.
- Ausführung durch das System: Wie das Ergebnis konkret berechnet wird, entscheidet die Sprache bzw. die Laufzeitumgebung – zum Beispiel der Query-Optimizer einer Datenbank.
- Höhere Abstraktion: Oft wird ein höheres Abstraktionsniveau verwendet, das komplexere Operationen in einfacheren Ausdrücken darstellt.
- Beispiel: Eine SQL-Abfrage, die beschreibt, was man von einer Datenbank möchte, ohne die Schritte zur Erlangung dieser Informationen zu beschreiben.
Das durchgehende Beispiel: Die Notenliste
Wir lösen ein und dieselbe Aufgabe in beiden Stilen. Diese Aufgabenstellung begleitet uns durch das ganze Kapitel.
Aufgabe: Aus einer Klassenliste sollen die Namen aller Lernenden mit einer Note von 5.0 oder besser ermittelt werden – in Grossbuchstaben.
noten = [("Alina", 5.5), ("Ben", 3.5), ("Chiara", 4.0), ("Dario", 5.0), ("Elif", 4.5)]
Variante 1: Imperativ
bestanden = [] # 1. Leeres Gefäss vorbereiten for eintrag in noten: # 2. Jedes Element einzeln ansteuern name = eintrag[0] # 3. Bestandteile herauslösen note = eintrag[1] if note >= 5.0: # 4. Bedingung prüfen bestanden.append(name.upper()) # 5. Zustand verändern print(bestanden) # ['ALINA', 'DARIO']
Fünf gedankliche Schritte, und der Zustand von bestanden ändert sich während des Durchlaufs mehrfach.
Live-Demo: Zeichnen Sie die Liste bestanden an die Tafel und lassen Sie die Klasse nach jedem Schleifendurchgang den neuen Inhalt diktieren. Genau das ist ein Trace Table – siehe LU01d.
Variante 2: Deklarativ (List Comprehension)
bestanden = [name.upper() for name, note in noten if note >= 5.0] print(bestanden) # ['ALINA', 'DARIO']
Gelesen wird das als Satz: „Nimm den Namen in Grossbuchstaben, für jeden Namen und jede Note in der Notenliste, sofern die Note mindestens 5.0 ist.„
Variante 3: Deklarativ (map und filter)
bestanden = list(map(lambda p: p[0].upper(), filter(lambda p: p[1] >= 5.0, noten))) print(bestanden) # ['ALINA', 'DARIO']
Diese Bausteine – map, filter und reduce – sind das Herzstück des Moduls und werden später ausführlich behandelt.
Variante 4: Dieselbe Frage als SQL
SELECT UPPER(name) FROM noten WHERE note >= 5.0;
Auffällig: Die SQL-Abfrage und die List Comprehension sind fast identisch aufgebaut – Was auswählen, woher, unter welcher Bedingung. Deklarativ ist also keine Erfindung der funktionalen Programmierung, sondern ein Prinzip, das Sie längst kennen.
Die Schlüsselfrage: Was ist verschwunden?
Stellen Sie der Klasse diese Frage und vergleichen Sie Variante 1 mit Variante 2:
| In Variante 1 vorhanden | In Variante 2 verschwunden, weil … |
|---|---|
Die leere Liste bestanden = [] | … kein Zwischenzustand mehr aufgebaut werden muss. |
Die Schleife for | … die Iteration im Sprachkonstrukt steckt. |
Der Index-Zugriff eintrag[0] | … direkt entpackt wird. |
Das append | … nichts mehr verändert, sondern ein neuer Wert erzeugt wird. |
| Die Reihenfolge der fünf Schritte | … nur noch das Ergebnis beschrieben wird. |
Merksatz für die Wandtafel: Imperativ baut ein Ergebnis Schritt für Schritt auf. Deklarativ beschreibt das Ergebnis.
Warum das relevant ist: der Klassiker-Bug
Dieses Beispiel eignet sich hervorragend, um den Preis der Zustandsverwaltung zu zeigen. Lassen Sie den Code laufen, bevor Sie ihn erklären:
def bestanden_imperativ(liste, ergebnis=[]): for name, note in liste: if note >= 5.0: ergebnis.append(name) return ergebnis print(bestanden_imperativ(noten)) # ['Alina', 'Dario'] print(bestanden_imperativ(noten)) # ['Alina', 'Dario', 'Alina', 'Dario'] ← ?!
Derselbe Aufruf, dasselbe Argument – zwei verschiedene Ergebnisse. Der Grund: Die Liste ergebnis wird in Python nur einmal beim Definieren der Funktion angelegt und bleibt zwischen den Aufrufen bestehen. Sie ist veränderlicher Zustand, der die Funktion überlebt.
Genau solche Fehler will die funktionale Programmierung mit Unveränderlichkeit und puren Funktionen ausschliessen – siehe LU01c und LU02.
Alltagsbeispiele zum Durchspielen
Diese Paare eignen sich gut zum Sammeln an der Wandtafel. Lassen Sie die Klasse jeweils raten, welche Spalte welchem Paradigma entspricht.
| Situation | Imperativ (das Wie) | Deklarativ (das Was) |
|---|---|---|
| Kaffee | „Mahle 18 g Bohnen, verteile sie gleichmässig, presse mit 30 kg an, brühe 25 Sekunden …“ | „Einen Espresso, bitte.„ |
| Möbel | Die IKEA-Anleitung mit 34 nummerierten Schritten | „Stell mir ein Bett ins Zimmer.“ |
| Bildbearbeitung | Über jedes Pixel iterieren und den Helligkeitswert um 20 erhöhen | Schieberegler „Helligkeit +20 %„ |
| Webseite | Mit Canvas Linien, Rechtecke und Text an Koordinaten zeichnen | <h1>Titel</h1> – der Browser entscheidet über Schriftgrösse und Position |
| Wäsche | „Fülle Wasser ein, heize auf 40 °C, drehe die Trommel 12 Minuten …“ | Programm „Buntwäsche 40°„ wählen |
| Daten | Über alle Datensätze schleifen und passende sammeln | SELECT … WHERE … |
Erkennungsmerkmale: Der Spickzettel
Woran erkennt man den Stil beim Blick auf fremden Code?
| Merkmal | Deutet auf |
|---|---|
for / while mit Zählvariable | imperativ |
Eine Variable wird mehrfach überschrieben (summe += x) | imperativ |
append, insert, sort(), del – die Daten selbst werden verändert | imperativ |
List Comprehension, map, filter, sum, sorted() | deklarativ |
| Es wird ein neuer Wert erzeugt statt ein bestehender geändert | deklarativ |
| Der Code liest sich wie ein Satz über das Ergebnis | deklarativ |
Ein guter Test: Kann man den Code laut vorlesen und es klingt wie eine Beschreibung des Ergebnisses – oder wie eine Anleitung?
Die häufigste Fehlvorstellung
Achtung: deklarativ heisst nicht automatisch seiteneffektfrei
Seiteneffektfreiheit ist keine Eigenschaft der deklarativen Programmierung an sich, sondern der puren funktionalen Programmierung – also einer Teilmenge davon.
SQL ist deklarativ, aber ein UPDATE kunden SET status = 'aktiv'; verändert sehr wohl den Zustand der Datenbank. Und HTML ist deklarativ, kennt aber gar keine Funktionen.
Richtig ist: Deklarativ beschreibt das Was statt das Wie. Pur funktional verlangt zusätzlich, dass eine Funktion nichts ausserhalb ihrer selbst verändert. Mehr dazu in LU01c und in LU02 (Pure Functions).
Vergleich
- Kontrolle vs. Abstraktion: Während die imperative Programmierung eine präzise Kontrolle über das „Wie“ der Problemlösung bietet, ermöglicht die deklarative Programmierung eine höhere Abstraktion über das „Was„.
- Lesbarkeit: Deklarative Programme sind oft kürzer und leichter zu verstehen, während imperative Programme detaillierter sind.
- Effizienz: Imperative Programme können oft effizienter sein, da der Programmierer die genaue Kontrolle hat. Deklarative Programme sind möglicherweise weniger effizient, aber schneller zu schreiben und zu warten.
- Fehleranfälligkeit: Der detaillierte Charakter der imperativen Programmierung kann fehleranfälliger sein, da mehr Möglichkeiten für menschliche Fehler bestehen.
Mini-Übungen
Ü1 – Umschreiben. Formulieren Sie diesen imperativen Code deklarativ um:
zahlen = [3, 8, 1, 9, 4, 7] gross = [] for z in zahlen: if z > 5: gross.append(z * 10)
Ü2 – Zuordnen. Imperativ oder deklarativ? sum(zahlen) · liste.sort() · sorted(liste) · while i < 10: · max(noten)
Ü3 – Übersetzen. Beschreiben Sie das Rezept „Spiegelei braten“ einmal imperativ (Schritt für Schritt) und einmal deklarativ (nur das Ergebnis). Was geht bei der deklarativen Fassung verloren, was gewinnt man?
Lösungshinweise
Ü1: gross = [z * 10 for z in zahlen if z > 5]
Ü2: sum deklarativ · liste.sort() imperativ (verändert die Liste, gibt None zurück) · sorted(liste) deklarativ (liefert eine neue Liste, Original bleibt unverändert) · while imperativ · max deklarativ. Das Paar sort() / sorted() ist besonders lehrreich: gleiche Aufgabe, unterschiedliches Paradigma.
Ü3: Verloren geht die Kontrolle über Details (Pfanne, Hitze, Garzeit). Gewonnen werden Kürze und die Freiheit des Ausführenden, den besten Weg zu wählen.
Weiterführende Aufgaben: LU01.A01 und LU01.A03 lösen dasselbe Sortierproblem in beiden Stilen.
Schlussfolgerung
Während die imperative Programmierung den Programmierern die vollständige Kontrolle über den Prozess gibt, bietet die deklarative Programmierung eine elegante und oft intuitivere Methode zur Lösung von Problemen. Die Wahl zwischen diesen Stilen hängt oft von der Art des Problems, den Anforderungen an die Effizienz und die Vorlieben des Programmierers ab. In der modernen Softwareentwicklung werden oft Elemente beider Stile kombiniert, um eine effiziente und wartbare Lösung zu schaffen.
