Dies ist eine alte Version des Dokuments!


LU01.L13 - Dasselbe Problem in drei Paradigmen

noten = [("Alina", 5.5), ("Ben", 3.5), ("Chiara", 4.0), ("Dario", 5.0), ("Elif", 3.9)]
 
if __name__ == '__main__':
    summe = 0
    ungenuegend = []
    beste = noten[0][1]
 
    for name, note in noten:
        summe += note
        if note < 4.0:
            ungenuegend.append(name)
        if note > beste:
            beste = note
 
    durchschnitt = summe / len(noten)
 
    print(f"Durchschnitt: {durchschnitt:.2f}")
    print(f"Ungenuegend: {ungenuegend}")
    print(f"Beste Note: {beste}")

Drei Variablen werden parallel fortgeschrieben. Wer eine vierte Kennzahl braucht, ergänzt eine vierte Variable und eine weitere Zeile im Schleifenkörper – die Schleife wird mit jeder Kennzahl unübersichtlicher.

class Klasse:
    def __init__(self, noten=None):
        self._noten = list(noten) if noten else []
 
    def hinzufuegen(self, name, note):
        self._noten.append((name, note))     # veraendert den eigenen Zustand
 
    def durchschnitt(self):
        return sum(note for _, note in self._noten) / len(self._noten)
 
    def ungenuegende(self):
        return [name for name, note in self._noten if note < 4.0]
 
    def beste_note(self):
        return max(note for _, note in self._noten)
 
 
if __name__ == '__main__':
    klasse = Klasse([("Alina", 5.5), ("Ben", 3.5), ("Chiara", 4.0),
                     ("Dario", 5.0), ("Elif", 3.9)])
 
    print(f"Durchschnitt: {klasse.durchschnitt():.2f}")
    print(f"Ungenuegend: {klasse.ungenuegende()}")
    print(f"Beste Note: {klasse.beste_note()}")
def durchschnitt(noten):
    return sum(note for _, note in noten) / len(noten)
 
 
def ungenuegende(noten):
    return [name for name, note in noten if note < 4.0]
 
 
def beste_note(noten):
    return max(note for _, note in noten)
 
 
def note_hinzufuegen(noten, name, note):
    return noten + [(name, note)]           # neue Liste statt Veraenderung
 
 
if __name__ == '__main__':
    noten = [("Alina", 5.5), ("Ben", 3.5), ("Chiara", 4.0),
             ("Dario", 5.0), ("Elif", 3.9)]
 
    print(f"Durchschnitt: {durchschnitt(noten):.2f}")
    print(f"Ungenuegend: {ungenuegende(noten)}")
    print(f"Beste Note: {beste_note(noten)}")
Kriterium Prozedural Objektorientiert Funktional
Wo liegt der Zustand? In lokalen Variablen des Skripts, die während der Schleife wachsen. Im Objekt, in self._noten – er überlebt jeden Methodenaufruf. Nirgends dauerhaft. Die Daten werden als Argument hineingegeben und als Ergebnis herausgereicht.
Zweimal dieselbe Berechnung? Der Schleifencode müsste komplett wiederholt werden, inklusive Zurücksetzen der Variablen. Kann zwei verschiedene Werte liefern, sobald zwischendurch hinzufuegen aufgerufen wurde. Liefert garantiert immer dasselbe – das Ergebnis hängt nur vom Argument ab.
Wie testet man einzeln? Gar nicht sauber: Die Berechnung steckt im Hauptprogramm und ist nicht aufrufbar. Man muss zuerst ein Objekt in einen bestimmten Zustand bringen, dann die Methode aufrufen. assert durchschnitt(testdaten) == erwartet – eine Zeile, kein Aufbau nötig.
Was muss man wissen, um eine Zeile zu verstehen? Den bisherigen Verlauf der Schleife: Welche Werte haben summe, beste gerade? Den aktuellen Zustand des Objekts – der von aussen nicht sichtbar ist. Nur die Signatur der Funktion und ihre Argumente.
Gleichzeitiger Zugriff? Problematisch: geteilte Variablen können sich gegenseitig überschreiben. Problematisch: zwei Programmteile mit derselben Objektreferenz sehen Änderungen des jeweils anderen. Unkritisch: Es gibt keinen geteilten veränderlichen Zustand.
Anzahl Codezeilen am wenigsten (~15) am meisten (~20, plus Klassengerüst) dazwischen (~18), dafür in unabhängigen Einheiten

Zur Zeilenzahl

Die Zeilenzahl ist das schwächste der sechs Kriterien. Die prozedurale Fassung ist am kürzesten und trotzdem am schlechtesten wartbar – Kürze und Qualität sind nicht dasselbe. Bewerten Sie in Ihrer Abgabe vor allem die ersten fünf Zeilen der Tabelle.

Am einfachsten funktional: Man schreibt eine weitere unabhängige Funktion median(noten). Nichts Bestehendes wird angefasst, nichts kann kaputtgehen.

Am schwierigsten prozedural: Die neue Kennzahl braucht eine weitere Variable vor der Schleife und eine weitere Zeile darin. Die Schleife übernimmt damit noch eine Aufgabe mehr – ein Verstoss gegen das Prinzip, dass ein Codeblock für genau eine Sache zuständig sein soll.

Die OO-Fassung liegt dazwischen: Eine neue Methode ist schnell ergänzt, sie hängt aber am Zustand des Objekts.

klasse = Klasse(noten)
print(round(klasse.durchschnitt(), 3))   # 4.38
klasse.hinzufuegen("Fabio", 2.0)
print(round(klasse.durchschnitt(), 3))   # 3.983

durchschnitt() hat keine Argumente. Sein Ergebnis hängt ausschliesslich vom verborgenen Zustand self._noten ab – und der hat sich zwischen den beiden Aufrufen geändert. Der Aufruf ist damit nicht referenziell transparent: Man kann ihn nicht durch seinen Wert ersetzen, ohne die Vorgeschichte zu kennen.

In der funktionalen Fassung ist das ausgeschlossen, weil die Daten sichtbar im Argument stehen:

print(durchschnitt(noten))                          # 4.38
erweitert = note_hinzufuegen(noten, "Fabio", 2.0)
print(durchschnitt(noten))                          # 4.38 - unveraendert
print(durchschnitt(erweitert))                      # 3.983 - andere Daten, anderer Name

Unterschiedliche Ergebnisse haben hier unterschiedliche Namen. Genau das ist der Gewinn.

Erkennbar an drei Punkten:

  • hinzufuegen gibt nichts zurück (None) und tut stattdessen etwas – es ist eine Prozedur im Sinn von LU01c.
  • self._noten.append(…) verändert eine bestehende Datenstruktur, statt eine neue zu erzeugen.
  • Die Reihenfolge der Aufrufe entscheidet über das Ergebnis – das Kennzeichen zustandsbehafteter, imperativer Programmierung.

Objektorientierung ist also kein Gegensatz zur imperativen Programmierung, sondern eine Art, imperativen Code zu organisieren: Sie bündelt Zustand und die darauf arbeitenden Prozeduren zu Einheiten. Der Gegensatz verläuft zwischen Zustand verändern und Werte erzeugen, nicht zwischen OO und funktional.

Mögliche Antworten – die Beispiele der Lernenden dürfen abweichen, müssen aber begründet sein:

Paradigma Passende Situation Begründung
Prozedural Ein kurzes Wartungsskript, das einmal pro Monat Logdateien aufräumt. Der Aufwand für Struktur lohnt sich nicht; das Skript hat einen Ablauf und ein Ende.
Objektorientiert Eine GUI-Anwendung oder ein Spiel, in dem Objekte über die Zeit ihren Zustand ändern (Fenster, Spielfigur, Warenkorb). Zustand ist hier der Zweck der Sache, nicht der Feind. Ein Objekt bündelt ihn an einer klar zuständigen Stelle.
Funktional Datenaufbereitung und Auswertung: Importe filtern, transformieren, aggregieren – und alles, was parallel oder verteilt läuft. Ohne geteilten veränderlichen Zustand sind die Schritte einzeln testbar, beliebig kombinierbar und parallelisierbar.

Fazit: In der Praxis werden die drei Stile gemischt. Eine typische Anwendung hat objektorientierte Struktur an der Oberfläche, einen funktionalen Kern für die Berechnungen und prozedurale Ränder dort, wo mit Dateien, Netzwerk und Benutzern interagiert wird.


© Kevin Maurizi

  • modul/m323/learningunits/lu01/loesungen/paradigmenvergleich.1787040778.txt.gz
  • Zuletzt geändert: 2026/08/18 10:12
  • von admin