Notes 7

Sie können diese Seite ausdrucken oder mit einem PDF-Drucker in ein PDF umwandeln, um Ihre eigenen Notizen hinzuzufügen.

Willkommen!

  • In der letzten Vorlesung haben Sie die Grundlagen von Python kennengelernt: Syntax, Typen, Kontrollfluss, Funktionen, objektorientierte Programmierung und Fehlerbehandlung.
  • In dieser Vorlesung vertiefen wir diese Konzepte und lernen elegante Muster für die Datenverarbeitung kennen.

ÜBUNGSAUFGABE 1 - Schleifen (Lückentext)

Ziel: Wiederholung for-Schleifen und String-Methoden aus der letzten Vorlesung

Aufgabe: Füllen Sie die zwei Lücken aus, sodass das Programm jeden Buchstaben eines Strings in Großbuchstaben ausgibt.

text = "hallo"
for ___(1)___ in text:
    print(___(2)___)

Optionen für (1): i, c, text[i], range(text) Optionen für (2): c.upper(), upper(c), c.toupper(), text.upper()

Lösung:

  • (1): c — Schleifenvariable, die jeden Buchstaben durchläuft
  • (2): c.upper() — Methode auf dem String-Objekt aufrufen

Erklärung:

  • In Python iteriert for c in text direkt über jeden Buchstaben
  • upper() ist eine Methode von str, Aufruf mit Punkt-Notation

Häufige Fehler:

  • upper(c) statt c.upper() → In Python sind Methoden an Objekte gebunden
  • text.upper() → Gibt den ganzen String aus, nicht einzelne Buchstaben

ÜBUNGSAUFGABE 2 - Truthiness (✓/✗)

Ziel: Wiederholung bedingte Anweisungen und “falsy”/“truthy” Werte

Aufgabe: In Python können viele Werte in if-Bedingungen verwendet werden. Welche Bedingungen sind True?

if 0:           # ?
if "":          # ?
if "hallo":     # ?
if []:          # ?
if [1, 2, 3]:   # ?
if 42:          # ?

Lösung:

  • if 0: ✗ — Null ist falsy
  • if "": ✗ — Leerer String ist falsy
  • if "hallo": ✓ — Nicht-leerer String ist truthy
  • if []: ✗ — Leere Liste ist falsy
  • if [1, 2, 3]: ✓ — Nicht-leere Liste ist truthy
  • if 42: ✓ — Jede Zahl außer 0 ist truthy

Merke: Leere Dinge (0, “”, []) sind False, gefüllte Dinge sind True!


Bibliotheken von Drittanbietern

  • Einer der Vorteile von Python ist seine riesige Benutzerbasis und die ebenso große Anzahl von Bibliotheken von Drittanbietern.

  • Mit dem Befehl pip install können Sie zusätzliche Bibliotheken installieren:

    pip install qrcode
  • Dann können Sie mit wenigen Zeilen einen QR-Code generieren:

    import qrcode
    
    img = qrcode.make("https://psi.2i.vc/inf-einf-b/")
    img.save("kurs.png")
  • Sie können mehr über verfügbare Pakete auf PyPI erfahren.

Codequalität und Typsicherheit

Defensives Programmieren

  • Prüfen Sie Eingaben frühzeitig und geben Sie aussagekräftige Fehlermeldungen:

    def set_age(age):
        if not isinstance(age, int):
            raise TypeError("Alter muss eine Ganzzahl sein")
        if age < 0 or age > 150:
            raise ValueError(f"Ungültiges Alter: {age}")
        return age
  • isinstance(age, int) prüft, ob age vom Typ int ist – nützlich für Typprüfungen zur Laufzeit.

  • Mit raise lösen wir Exceptions aus, um Fehlerzustände zu signalisieren.

Type Hints

  • Type Hints dokumentieren erwartete Typen (werden aber zur Laufzeit nicht geprüft):

    def calculate_average(numbers: list[float]) -> float:
        if not numbers:
            raise ValueError("Liste darf nicht leer sein")
        return sum(numbers) / len(numbers)
    
    # IDE zeigt Warnung, aber Code läuft:
    result = calculate_average(["1", "2"])  # TypeError bei sum()!
  • Type Hints sind nützlich für Dokumentation und IDE-Unterstützung, ersetzen aber keine Laufzeitprüfungen.

Kommandozeilen-Argumente

  • Wie bei C können Sie auch in Python Kommandozeilen-Argumente verwenden. Betrachten Sie den folgenden Code:

    # Druckt ein Kommandozeilenargument
    
    from sys import argv
    
    if len(argv) == 2:
        print(f"hallo, {argv[1]}")
    else:
        print("hallo, Welt")

    Beachten Sie, dass argv[1] unter Verwendung einer formatierten Zeichenkette gedruckt wird, erkennbar an dem f in der print-Anweisung.

  • Sie können alle Argumente in argv wie folgt ausgeben:

    # Kommandozeilenargumente ausgeben, Indizierung in argv
    
    from sys import argv
    
    for i in range(len(argv)):
        print(argv[i])

    Beachten Sie, dass argv[0] immer der Name des Skripts selbst ist (z.B. greet.py), nicht der Python-Interpreter.

  • Sie können Teile von Listen wegschneiden (engl. to slice). Betrachten Sie den folgenden Code:

    # Kommandozeilenargumente drucken
    
    from sys import argv
    
    for arg in argv[1:]:
        print(arg)

    Beachten Sie, dass die Ausführung dieses Codes dazu führt, dass der Name der Datei, die Sie ausführen, weggeschnitten wird. Es werden alle Argumente ab dem ersten (nullindiziert, also eigtl. ab dem zweiten) ausgegeben.

  • Sie können mehr über die sys-Bibliothek in der Python-Dokumentation erfahren.

Exit-Code

  • Die sys-Bibliothek hat auch eingebaute Methoden. Wir können sys.exit(i) verwenden, um das Programm mit einem bestimmten Exit-Code zu beenden:

    # Beendet mit explizitem Wert, importiert sys
    
    import sys
    
    if len(sys.argv) != 2:
        print("Fehlendes Kommandozeilenargument")
        sys.exit(1)
    
    print(f"hallo, {sys.argv[1]}")
    sys.exit(0)

    Beachten Sie, dass die Punkt-Notation verwendet wird, um die eingebauten Funktionen von “sys” zu nutzen, da wir das ganze sys-Modul importieren.

OOP-Patterns für größere Programme

In der letzten Vorlesung haben wir Klassen, __init__, self, Properties und Dunder Methods kennengelernt. Nun betrachten wir fortgeschrittene Muster.

Vererbung

  • In der letzten Vorlesung haben wir die Klasse Rectangle mit area() und perimeter() erstellt.

  • Problem: Was, wenn wir auch Circle oder andere Formen brauchen? Jede Form hätte area() und perimeter() – das ist Code-Duplikation!

  • Lösung: Wir definieren eine gemeinsame Basisklasse Shape, von der andere Klassen erben.

  • Vererbung ermöglicht es, Klassen zu erstellen, die Eigenschaften einer anderen Klasse übernehmen und erweitern.

  • Die Syntax lautet class Kindklasse(Elternklasse): – die Kindklasse erbt alle Methoden der Elternklasse.

    class Shape:
        """Basisklasse für geometrische Formen."""
        def area(self):
            pass  # Platzhalter – wird von Kindklassen überschrieben
    
        def perimeter(self):
            pass  # Platzhalter
    
    class Circle(Shape):  # Circle erbt von Shape
        def __init__(self, radius):
            self.radius = radius
    
        def area(self):  # Methode überschreiben
            return 3.14159 * self.radius ** 2
    
        def perimeter(self):
            return 2 * 3.14159 * self.radius
    
    class Rectangle(Shape):  # Rectangle erbt auch von Shape
        def __init__(self, width, height):
            self.width = width
            self.height = height
    
        def area(self):
            return self.width * self.height
    
        def perimeter(self):
            return 2 * (self.width + self.height)
  • pass ist eine leere Anweisung, die nichts tut – nützlich als Platzhalter für noch nicht implementierten Code.

  • Vorteil: Wir können alle Formen gleich behandeln, weil sie dieselbe Schnittstelle haben:

    # Alle Subklassen gleich behandeln
    shapes = [Circle(5), Rectangle(3, 4)]
    for shape in shapes:
        print(f"Fläche: {shape.area()}")
  • super() ruft die Methode der Elternklasse auf – nützlich, wenn die Kindklasse die Initialisierung der Elternklasse nutzen will:

    class Square(Rectangle):
        """Ein Quadrat ist ein spezielles Rechteck."""
        def __init__(self, side_length):
            # Ruft Rectangle.__init__(self, width, height) auf
            super().__init__(side_length, side_length)
    
    square = Square(5)
    print(square.area())       # 25 - nutzt Rectangle.area()
    print(square.perimeter())  # 20 - nutzt Rectangle.perimeter()
  • super().__init__(...) sorgt dafür, dass die Elternklasse ihre Attribute (width, height) korrekt setzt.

  • Square muss area() und perimeter() nicht neu definieren – es erbt sie von Rectangle.

  • Wann Vererbung nutzen? Bei “ist-ein”-Beziehungen: Ein Square ist ein (spezielles) Rectangle. Ein Circle ist eine Shape.

Duck Typing

  • In Python zählt nicht der Typ eines Objekts, sondern sein Verhalten:

    “Wenn es wie eine Ente watschelt und wie eine Ente quakt, ist es eine Ente.”

  • Solange ein Objekt die erwarteten Methoden hat, funktioniert es – egal welche Klasse:

    class Dog:
        def make_sound(self): return "Wuff!"
    
    class Cat:
        def make_sound(self): return "Miau!"
    
    # Python interessiert sich nicht für Typen, nur für Verhalten
    def animal_chorus(animals):
        for animal in animals:
            print(animal.make_sound())
    
    animal_chorus([Dog(), Cat()])  # Funktioniert!
  • Mit hasattr() kann man prüfen, ob ein Objekt eine Methode unterstützt:

    if hasattr(obj, 'take_damage'):
        obj.take_damage(10)

Separation of Concerns

  • Gutes Design trennt verschiedene Zuständigkeiten. Jede Klasse sollte eine klare, einzelne Aufgabe haben.
  • Beispiel: Die Spiellogik (Schaden berechnen) sollte von der Ausgabe (Nachrichten anzeigen) getrennt sein.
  • Dadurch wird Code flexibler, leichter testbar und besser wartbar. Details zu diesem Designprinzip behandeln wir in einer späteren Session.

Collections vertieft

In der letzten Vorlesung haben wir Listen und Dictionaries kurz angeteasert. Nun schauen wir uns diese wichtigen Datenstrukturen genauer an.

Listen im Detail

  • list ist eine sehr häufig genutzte Datenstruktur in Python.

  • Für die Verarbeitung von Listen gibt es in Python einige nützliche Funktionen – und Listen haben zusätzlich viele nützliche Methoden.

  • Betrachten Sie zum Beispiel den folgenden Code:

    # Durchschnitt von drei Zahlen in einer Liste
    
    # Punkte
    scores = [72, 73, 33]
    
    # Durchschnitt ausgeben
    average = sum(scores) / len(scores)
    print(f"Durchschnitt: {average}")

    Beachten Sie, dass Sie die eingebaute Funktion sum verwenden können, um den Durchschnitt zu berechnen.

  • Man könnte sich fragen, wieso man sum(scores) schreibt und nicht scores.sum() – gleichermaßen bei len(…). Das liegt unter anderem daran, dass Listen nicht die einzigen Objekte sind, für die man eine Summe berechnen oder die Anzahl ermitteln können will. Diese Operationen könnten auch bei Tupeln oder Mengen nützlich sein. Tatsächlich sind sie bei allen Objekten sinnvoll, die man zählen kann bzw. die mehrere aufsummierbare Werte haben. Anstatt solche Funktionen in allen Klassen, in denen es Sinn macht, neu zu definieren, gibt es sie daher in Python global. Klassen, die sich summieren oder zählen lassen wollen, müssen diese Eigenschaft dann durch vordefinierte Methoden (sog. Dunder-Methods von „Double-Underscore") anbieten.

  • Sie können die folgende Syntax verwenden, um Werte vom Benutzer zu erhalten:

    # Durchschnitt von drei Zahlen mit Hilfe einer Liste und einer Schleife
    
    from cs50 import get_int
    
    # Spielstände abrufen
    scores = []
    for i in range(3):
        score = get_int("Score: ")
        scores.append(score) # Alternative: scores + [score]
    
    # Durchschnitt ausgeben
    average = sum(scores) / len(scores)
    print(f"Durchschnitt: {average}")

    Beachten Sie, dass dieser Code die eingebaute Methode append für Listen verwendet.

  • Sie können mehr über Listen in der Python-Dokumentation erfahren.

  • Sie können auch mehr über len in der Python-Dokumentation erfahren.

Suche in Collections

  • Wir haben uns in C angesehen, wie man in Datenstrukturen nach einem bestimmten Wert sucht.

  • In Python können wir eine lineare Suche sehr elegant mit dem in-Operator durchführen:

    # Implementiert die lineare Suche nach Namen mit "in".
    
    # Eine Liste von Namen
    names = ["Carter", "David", "John"]
    
    # Nach dem Namen fragen
    name = input("Name: ")
    
    # Suche nach Name
    if name in names:
        print("Gefunden")
    else:
        print("Nicht gefunden")

    Beachten Sie, wie “in” zur Umsetzung der linearen Suche verwendet wird.

  • Dieser Code ist zwar elegant, aber nicht effizient.

  • Sie erinnern sich vielleicht noch daran, dass ein Wörterbuch (Pythons Datentyp dict, der mit geschweiften Klammern definiert wird) eine Sammlung von Schlüssel- und _Wert-_Paaren ist und dass Wörterbücher mit Hash-Tabellen sehr effizient implementiert werden können (konstante Laufzeit für die Suche bei Wahl einer geeigneten Hash-Funktion).

  • Man könnte das Telefonbuch auch als Liste von Dictionaries implementieren (ähnlich wie struct in C):

    # Telefonbuch als Liste von Dictionaries (wie structs in C)
    people = [
        {"name": "Fuhrmann", "num": "+1-617-495-1000"},
        {"name": "David", "num": "+1-617-495-1000"},
        {"name": "John", "num": "+1-949-468-2750"},
    ]
    
    # Suche – lineare Suche durch die Liste nötig!
    name = input("Name: ")
    for person in people:
        if person["name"] == name:
            print(f"Nummer: {person['num']}")
            break
  • Das funktioniert, aber wir müssen die ganze Liste durchsuchen (lineare Laufzeit).

  • Besser: Wir nutzen ein echtes Dictionary, bei dem der Name direkt der Schlüssel ist – dann ist die Suche in konstanter Zeit möglich!

  • Für unser einfaches Telefonbuch benötigen wir streng genommen weder name noch num als separate Felder! Wir können diesen Code wie folgt vereinfachen:

    # Implementiert ein Telefonbuch mit dict
    
    from cs50 import get_string
    
    people = {
        "Fuhrmann": "+1-617-495-1000",
        "David": "+1-617-495-1000",
        "John": "+1-949-468-2750",
    }
    
    # Suche nach Name
    name = get_string("Name: ")
    if name in people:
        print(f"Nummer: {people[name]}")
    else:
        print("Nicht gefunden")

    Beachten Sie, dass people nun ein dict ist und mit geschweiften Klammern definiert wird. Die Anweisung if name in people prüft, ob name als Key im Wörterbuch people enthalten ist. Beachten Sie auch, dass wir in der Anweisung print mit dem Wert von name im People-Wörterbuch indexieren können. Sehr nützlich!

  • Python gibt sein Bestes, um bei der Suche eine konstante Zeit zu erreichen, indem es seine eingebauten Suchfunktionen verwendet.

  • Sie können mehr über Wörterbücher in der Python-Dokumentation erfahren.

Variablen als Namensschilder

  • In C sind Variablen wie Behälter, die einen Wert speichern. In Python funktioniert das anders: Variablen sind wie Namensschilder, die an Objekten hängen.

    # Ein Objekt, ein Name
    x = 42
    print(x)  # 42
    
    # Ein Objekt, zwei Namen
    y = x     # y zeigt auf dasselbe 42
    print(y)  # 42
    
    # Namensschild umhängen
    x = 23    # x zeigt jetzt auf 23
    print(y)  # Immer noch 42!
  • Mehrere Namensschilder können auf dasselbe Objekt zeigen:

    noten = [95, 92, 98]
    kopie = noten           # Kein neuer Container!
    
    # Beide Namen zeigen auf dieselbe Liste
    kopie.append(90)        # Wirkt sich auf 'noten' aus
    print(noten)            # [95, 92, 98, 90]
  • Um eine echte Kopie zu erstellen:

    echte_kopie = list(noten)  # Neues Listenobjekt
    # oder:
    echte_kopie = noten.copy()

Mutable vs. Immutable

  • In Python gibt es zwei Arten von Objekten:

    • Immutable (unveränderlich): Zahlen (int, float), Strings, Tupel
    • Mutable (veränderlich): Listen, Dictionaries, Sets
  • Bei immutablen Objekten erzeugt jede “Änderung” ein neues Objekt:

    x = 42
    x += 1    # Erzeugt neues Objekt 43
    
    text = "Python"
    text[0] = "J"  # Fehler! Strings sind immutable
    text = "J" + text[1:]  # Neuen String erstellen: "Jython"
  • Bei mutablen Objekten wird das Objekt selbst verändert:

    lst = [1, 2]
    lst.append(3)  # Gleiches Objekt wird verändert
  • Wichtig: Bei Funktionsparametern verhält sich das unterschiedlich:

    # Immutable: Funktioniert wie "Pass by Value"
    def increment(x):
        x = x + 1          # Neues Namensschild!
        print(f"In Funktion: {x}")  # 43
    
    zahl = 42
    increment(zahl)
    print(f"Nach Aufruf: {zahl}")   # Immer noch 42!
    
    # Mutable: Funktioniert wie "Pass by Reference"
    def add_grade(grades):
        grades.append(100)  # Ändert das Original!
        print(f"In Funktion: {grades}")
    
    noten = [95, 92, 98]
    add_grade(noten)
    print(f"Nach Aufruf: {noten}")  # [95, 92, 98, 100]
  • Achtung: Methoden auf immutablen Objekten geben neue Objekte zurück:

    text = "hallo"
    gross = text.upper()   # Gibt geänderte Kopie zurück
    print(text)            # Immer noch "hallo"
    print(gross)           # "HALLO"

Tupel

  • Tupel sind wie Listen, aber immutable. Sie werden mit runden Klammern erstellt:

    punkt = (3, 4)        # Tupel erstellen
    punkt = 3, 4          # Klammern sind optional
    x = punkt[0]          # Zugriff wie bei Listen: 3
    # punkt[0] = 5        # Fehler! Tupel sind immutable
  • Der Hauptnutzen von Tupeln zeigt sich bei mehreren Rückgabewerten:

    def get_bounds(numbers):
        return min(numbers), max(numbers)  # Tupel!
    
    # Automatisches Entpacken
    minimum, maximum = get_bounds([1, 2, 3, 4, 5])
    print(f"Min: {minimum}, Max: {maximum}")
  • Der berühmte Swap mit Tupeln:

    x = 5
    y = 10
    x, y = y, x   # Tupel-Magic!
  • Teilweises Entpacken mit _ für ignorierte Werte:

    def get_statistik(zahlen):
        return sum(zahlen), sum(zahlen)/len(zahlen), min(zahlen)
    
    summe, mittel, _ = get_statistik([1, 2, 3, 4, 5])  # _ ignoriert den min-Wert
  • Wichtig: Tupel können als Dictionary-Schlüssel verwendet werden (weil sie immutable sind), Listen nicht!

    d = {(1, 2): "Tupel als Schlüssel"}   # OK
    # d = {[1, 2]: "Liste als Schlüssel"} # Fehler!

Sets (Mengen)

  • Sets (set) speichern eindeutige Werte ohne Duplikate und ermöglichen Mengenoperationen wie Vereinigung (|), Schnittmenge (&) und Differenz (-).
  • Syntax: {1, 2, 3} oder set([1, 2, 2, 3]){1, 2, 3}
  • Praktisch zum Entfernen von Duplikaten: eindeutig = set(liste_mit_duplikaten)
  • Die Prüfung auf Enthaltensein ist bei Sets schneller als bei Listen (dank Hash-Tabelle).

Nützliche Dictionary-Methoden

  • Sicherer Zugriff mit .get(): Vermeidet KeyError, wenn Schlüssel nicht existiert:

    noten = {"Alice": 95, "Bob": 87}
    
    # Direkter Zugriff kann fehlschlagen:
    # print(noten["Eve"])  # KeyError!
    
    # Sicherer mit .get() und Standardwert:
    note = noten.get("Eve", 0)   # 0, wenn "Eve" nicht existiert
    print(note)                   # 0
  • Über Dictionaries iterieren:

    students = {
        "Alice": {"Note": 95, "Fach": "Informatik"},
        "Bob": {"Note": 87, "Fach": "Physik"}
    }
    
    # Nur über Schlüssel (Standard)
    for name in students:
        print(name)
    
    # Über Schlüssel-Wert-Paare mit .items()
    for name, info in students.items():
        print(f"{name} studiert {info['Fach']}")
    
    # Nur über Werte mit .values()
    for info in students.values():
        print(info["Note"])
  • Mehrere Einträge hinzufügen mit .update():

    noten = {"Alice": 95, "Bob": 87}
    noten.update({"Charlie": 91, "David": 88})
    # noten ist jetzt: {"Alice": 95, "Bob": 87, "Charlie": 91, "David": 88}

Geschachtelte Collections

  • Collections können andere Collections enthalten. Das ermöglicht komplexe Datenstrukturen:

Mehrdimensionale Listen

  • 2D-Listen funktionieren wie 2D-Arrays in C:
    # Tic-Tac-Toe Brett
    brett = [
        [" ", "X", "O"],
        ["X", "O", " "],
        ["O", " ", "X"]
    ]
    
    print(brett[0])      # Erste Zeile: [" ", "X", "O"]
    print(brett[1][1])   # Mitte: "O"

Strukturierte Daten mit Dictionaries

  • Dictionaries lassen sich elegant verschachteln:

    kurse = {
        "Python": {
            "teilnehmer": [
                {"name": "Alice", "note": 95},
                {"name": "Bob", "note": 87}
            ],
            "raum": "A101",
            "zeiten": ["Mo 14:00", "Mi 16:00"]
        },
        "Java": {
            "teilnehmer": [
                {"name": "Charlie", "note": 92}
            ],
            "raum": "B205"
        }
    }
    
    # Gezielter Zugriff
    print(kurse["Python"]["teilnehmer"][0]["note"])  # 95
    
    # Iteration über verschachtelte Strukturen
    for kurs, info in kurse.items():
        print(f"\nKurs: {kurs}")
        print(f"Raum: {info['raum']}")
        for student in info['teilnehmer']:
            print(f"- {student['name']}: {student['note']}")
  • Tipp: Viele Freiheiten ermöglichen hohe Komplexität. Das begünstigt Fehler – daher ist Zurückhaltung und gute Dokumentation wichtig!

JSON-Daten verarbeiten

  • JSON (JavaScript Object Notation) ist ein weit verbreitetes Format für den Datenaustausch zwischen Systemen.

  • JSON sieht Python-Dictionaries sehr ähnlich – und Python kann JSON direkt in Dictionaries umwandeln.

  • Mit dem json-Modul können Sie JSON-Daten verarbeiten:

    import json
    
    # JSON-String in Dictionary umwandeln
    json_text = '{"name": "Alice", "alter": 25, "kurse": ["Python", "Java"]}'
    daten = json.loads(json_text)  # loads = "load string"
    print(daten["name"])           # Alice
    print(daten["kurse"][0])       # Python
  • JSON-Dateien können Sie mit json.load() einlesen:

    # JSON aus Datei lesen
    with open("config.json") as f:
        config = json.load(f)      # load (ohne 's') für Dateien
  • Das with-Statement ist ein sog. Context Manager: Es sorgt automatisch dafür, dass die Datei nach dem Block geschlossen wird – auch wenn ein Fehler auftritt.

  • Praktische Anwendungen: API-Antworten verarbeiten, Konfigurationsdateien lesen, Daten speichern und laden.

Elegante Datenverarbeitung

Praktische Iterationshelfer

  • enumerate() liefert Index und Element zusammen:

    spieler = ["Anna", "Bob", "Charlie"]
    for i, name in enumerate(spieler):
        print(f"Spieler {i+1}: {name}")
  • zip() iteriert parallel über mehrere Listen:

    namen = ["Anna", "Bob", "Charlie"]
    punkte = [95, 82, 88]
    for name, punkt in zip(namen, punkte):
        print(f"{name}: {punkt}")

Seiteneffekte und reine Funktionen

  • Ein Seiteneffekt ist eine Änderung, die über den Rückgabewert hinausgeht – z.B. eine globale Variable ändern oder eine Liste direkt modifizieren.

  • Reine Funktionen (pure functions) haben keine Seiteneffekte: Gleicher Input ergibt immer gleichen Output, ohne “versteckte” Änderungen.

    # Funktion MIT Seiteneffekt
    highscore = 0
    def update_highscore(punkte):
        global highscore  # Erlaubt Zugriff auf Variable außerhalb der Funktion
        if punkte > highscore:
            highscore = punkte  # Ändert globale Variable!
        return highscore
    
    # Reine Funktion OHNE Seiteneffekt
    def get_new_highscore(alter_highscore, neue_punkte):
        return max(alter_highscore, neue_punkte)  # Nur Input → Output
  • Das Keyword global erlaubt einer Funktion, auf eine Variable außerhalb ihres lokalen Scopes zuzugreifen und sie zu ändern. Ohne global würde Python eine neue lokale Variable anlegen.

  • Warum sind reine Funktionen besser?

    • Leichter zu testen (keine versteckten Abhängigkeiten)
    • Vorhersagbarer (gleicher Input = gleicher Output)
    • Einfacher zu verstehen und zu debuggen
  • Auch In-Place-Modifikation ist ein Seiteneffekt – die nächsten Funktionen zeigen das:

sort() vs. sorted()

  • liste.sort() verändert die Liste direkt (in-place):

    namen = ["Zoe", "Anna", "Ben"]
    namen.sort()
    print(namen)  # ["Anna", "Ben", "Zoe"]
  • sorted(liste) erstellt eine neue Liste (Original bleibt unverändert):

    namen = ["Zoe", "Anna", "Ben"]
    sortiert = sorted(namen)
    print(namen)     # ["Zoe", "Anna", "Ben"] - unverändert!
    print(sortiert)  # ["Anna", "Ben", "Zoe"]
  • Beide unterstützen key für benutzerdefinierte Sortierung:

    sortiert = sorted(namen, key=len)  # Nach Länge sortieren
  • Merke: sort() hat Seiteneffekt (ändert Original), sorted() ist eine reine Funktion (gibt neue Liste zurück).

map() und filter()

  • map() wendet eine Funktion auf jedes Element einer Sequenz an:

    namen = ["anna", "bob", "charlie"]
    
    # Traditionell mit Schleife
    gross = []
    for name in namen:
        gross.append(name.upper())
    
    # Mit map() - kompakter
    gross = map(str.upper, namen)
    print(gross)        # <map object at ...> - ein Iterator!
    print(list(gross))  # ["ANNA", "BOB", "CHARLIE"]
  • filter() behält nur Elemente, für die eine Funktion True zurückgibt:

    zahlen = [1, -4, 7, 0, -3, 12]
    
    def ist_positiv(x):
        return x > 0
    
    positiv = filter(ist_positiv, zahlen)
    print(list(positiv))  # [1, 7, 12]
  • Wichtig: Beide geben einen Iterator zurück, keine Liste!

    • Für eine Liste: list(map(...)) oder list(filter(...))
    • Ein Iterator kann nur einmal durchlaufen werden
    mapped = map(str.upper, namen)
    for name in mapped:
        print(name)  # Funktioniert
    for name in mapped:
        print(name)  # Keine Ausgabe! Iterator ist "aufgebraucht"
  • Hinweis: List Comprehensions (gleich) sind oft pythonischer als map/filter.

Lambda-Funktionen

  • Kurze, anonyme Funktionen für einfache Operationen:

    # Herkömmliche Funktion
    def quadrat(x):
        return x * x
    
    # Gleiche Funktion als Lambda
    quadrat = lambda x: x * x
    
    # Praktisch als key-Funktion
    sortiert = sorted(teams, key=lambda x: len(x))
  • Lambdas nur für einfache Ausdrücke verwenden – bei komplexer Logik besser eine normale Funktion definieren.

List Comprehensions

  • Eleganter Ersatz für Schleifen beim Erstellen von Listen:

    # Traditionell
    quadrate = []
    for n in range(5):
        quadrate.append(n * n)
    
    # Mit List Comprehension
    quadrate = [n * n for n in range(5)]  # [0, 1, 4, 9, 16]
  • Mit Filterbedingung:

    # Nur gerade Zahlen quadrieren
    quadrate = [n * n for n in range(10) if n % 2 == 0]  # [0, 4, 16, 36, 64]
  • Syntax: [ausdruck for element in sequenz if bedingung]

  • Hinweis: Es gibt auch Dict Comprehensions ({k: v for ...}) und Set Comprehensions ({x for ...}) sowie Generator Expressions ((x for ...)) für speichereffiziente Verarbeitung großer Datenmengen.

Zusammenfassend

In dieser Vorlesung haben wir die Python-Grundlagen aus der letzten Woche vertieft und fortgeschrittene Konzepte kennengelernt.

Konkret haben wir besprochen…

  • Bibliotheken von Drittanbietern (pip, PyPI)
  • Codequalität (Defensives Programmieren mit isinstance(), Type Hints)
  • Kommandozeilen-Argumente und Exit-Codes
  • OOP-Patterns: Vererbung mit super(), Duck Typing
  • Collections vertieft: Listen, Tupel, Sets, Dictionaries, geschachtelte Strukturen
  • JSON-Verarbeitung: json.loads() und json.load() für API-Daten und Config-Dateien
  • Seiteneffekte vs. reine Funktionen: Warum sorted() oft besser ist als sort()
  • Funktionale Datenverarbeitung: map(), filter(), Lambda-Funktionen
  • List Comprehensions: Elegante Alternative zu Schleifen

Bis zum nächsten Mal!