- 1 Section
- 10 Lessons
- unbegrenzt
- Python Fortgeschritten10
- 1.1Module und Pakete: import, pip, venv
- 1.2Objektorientierung in Python: Klassen, __init__, self
- 1.3Vererbung und Mehrfachvererbung in Python
- 1.4Decorators und Generatoren
- 1.5Fehlerbehandlung vertieft: eigene Exceptions
- 1.6Reguläre Ausdrücke mit re
- 1.7Arbeiten mit APIs: requests, JSON
- 1.8Automatisierung mit os, subprocess, pathlib
- 1.9Unit-Tests in Python: pytest
- 1.10Praxisprojekt: Automatisierungsskript mit Tests
Praxisprojekt: Automatisierungsskript mit Tests
Zum Abschluss des Kurses Python Fortgeschritten kommen zehn Praxis-Aufgaben, die alle behandelten Themen abprüfen – von Modulen, Klassen und Vererbung über Decorators, Generatoren und Exceptions bis zu Regex, APIs, Automatisierung und Tests. Die Aufgaben sind im Stil der IHK-Abschlussprüfung formuliert und nach Schwierigkeit gestaffelt: zwei EINFACH, vier MITTEL und vier SCHWIERIG.
Schreibe ein Skript, das mit dem Modul datetime das aktuelle Datum holt und als YYYY-MM-DD-String ausgibt. Importiere das Modul mit from ... import ..., sodass du nicht datetime.datetime.now() schreiben musst.
Lösung
Mit dem from-Import sparst du dir den Doppelpunkt:
from datetime import datetime heute = datetime.now() print(heute.strftime("%Y-%m-%d")) # 2024-01-15
Alternativ mit date.today(), was direkt nur das Datum (ohne Uhrzeit) liefert: from datetime import date; print(date.today().isoformat()).
Erstelle eine Klasse Buch mit den Attributen titel, autor und seiten. Sie soll einen Konstruktor haben, der alle drei Werte setzt. Außerdem soll print(buch) die Ausgabe "Buch: Titel von Autor (Seiten)" liefern.
Lösung
Konstruktor heißt __init__, die Format-Methode für print heißt __str__:
class Buch: def __init__(self, titel, autor, seiten): self.titel = titel self.autor = autor self.seiten = seiten def __str__(self): return f"Buch: {self.titel} von {self.autor} ({self.seiten})" b = Buch("Der Process", "Kafka", 320) print(b) # Buch: Der Process von Kafka (320)
Eleganter mit @dataclass – generiert __init__ und __repr__ automatisch (allerdings nicht das exakte Format der Aufgabe).
Eine Klasse Mitarbeiter hat einen name und ein gehalt. Schreibe eine Tochterklasse Manager, die zusätzlich einen bonus bekommt. Beide Klassen sollen eine Methode monatslohn() haben – beim Mitarbeiter ist das einfach gehalt, beim Manager gehalt + bonus. Nutze super().__init__().
Lösung
Der Manager-Konstruktor ruft zuerst super().__init__(...) für die geerbten Attribute, fügt dann den Bonus hinzu. monatslohn wird in der Tochter überschrieben:
class Mitarbeiter: def __init__(self, name, gehalt): self.name = name self.gehalt = gehalt def monatslohn(self): return self.gehalt class Manager(Mitarbeiter): def __init__(self, name, gehalt, bonus): super().__init__(name, gehalt) self.bonus = bonus def monatslohn(self): return self.gehalt + self.bonus m = Manager("Anna", 4000, 500) print(m.monatslohn()) # 4500
Hier zeigt sich Polymorphismus: in einer Liste von Mitarbeitern und Managern kannst du gemischt x.monatslohn() aufrufen – Python wählt automatisch die richtige Methode.
Schreibe einen Decorator @timing, der vor und nach dem Aufruf einer Funktion die verstrichene Zeit ausgibt. Format: "funktionsname: 0.123s". Nutze functools.wraps, damit der Name der Funktion erhalten bleibt.
Lösung
Ein Decorator ist eine Funktion, die eine Funktion entgegennimmt und eine andere zurückgibt:
import time from functools import wraps def timing(funktion): @wraps(funktion) def huelle(*args, **kwargs): start = time.perf_counter() ergebnis = funktion(*args, **kwargs) dauer = time.perf_counter() - start print(f"{funktion.__name__}: {dauer:.3f}s") return ergebnis return huelle @timing def langsam(): time.sleep(0.5) langsam() # langsam: 0.501s
functools.wraps kopiert den Namen und Docstring der Original-Funktion auf die Hülle – wichtig für Debugging und Doku.
Schreibe eine Generator-Funktion fibonacci(n), die die ersten n Fibonacci-Zahlen liefert (0, 1, 1, 2, 3, 5, 8, ...). Verwende yield, keine Liste. Teste mit list(fibonacci(10)).
Lösung
Generator hält Zustand zwischen yields – ideal für Sequenzen, die nicht alle gleichzeitig im Speicher liegen müssen:
def fibonacci(n): a, b = 0, 1 for _ in range(n): yield a a, b = b, a + b print(list(fibonacci(10))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Vorteil: fibonacci(1_000_000) würde sofort starten und Werte lazy liefern – die entsprechende Liste hätte über 100 MB im Speicher.
Eine Funktion abheben(konto, betrag) soll eine eigene Exception NichtAusreichendGedeckt werfen, wenn der Kontostand unter den Betrag fällt. Die Exception soll von Exception erben und beim Werfen den fehlenden Betrag mitgeben. Fange sie im Aufrufer und gib die Differenz aus.
Lösung
Eigene Exception als Klasse mit ggf. eigenem Konstruktor:
class NichtAusreichendGedeckt(Exception): def __init__(self, fehlbetrag): super().__init__(f"Fehlbetrag: {fehlbetrag:.2f} €") self.fehlbetrag = fehlbetrag def abheben(saldo, betrag): if betrag > saldo: raise NichtAusreichendGedeckt(betrag - saldo) return saldo - betrag try: abheben(50, 75) except NichtAusreichendGedeckt as e: print(f"Es fehlen {e.fehlbetrag:.2f} €") # Es fehlen 25.00 €
Eigene Exception-Klassen machen den Aufrufer-Code klarer: man fängt gezielt diesen einen Fehler, nicht „irgendeinen ValueError".
Aus einer Log-Zeile wie
192.168.1.5 - - [15/Jan/2024:14:23:01] "GET /index.html" 200 1024
extrahiere mit re:
- die IP-Adresse
- das Datum (nur
DD/Mon/YYYY) - die HTTP-Methode (GET, POST, ...)
- die URL
- den Status-Code
Nutze Named Groups und gib alles als Dict zurück.
Lösung
Mit Named Groups (?P<name>...) und .groupdict():
import re LOG = re.compile(r""" (?P<ip>\d+\.\d+\.\d+\.\d+) # IP \s-\s-\s\[ # Trenner (?P<datum>\d{2}/\w{3}/\d{4}) # Datum [^\]]*\]\s" # Rest bis " (?P<methode>\w+)\s # Methode (?P<url>\S+)"\s # URL (?P<status>\d{3}) # Status """, re.VERBOSE) zeile = '192.168.1.5 - - [15/Jan/2024:14:23:01] "GET /index.html" 200 1024' m = LOG.search(zeile) if m: print(m.groupdict()) # {'ip': '192.168.1.5', 'datum': '15/Jan/2024', # 'methode': 'GET', 'url': '/index.html', 'status': '200'}
Das VERBOSE-Flag erlaubt Kommentare und Whitespace im Pattern – macht komplexe Regex viel lesbarer. groupdict() liefert direkt ein Dict mit den Named Groups.
Schreibe eine Funktion user_holen(user_id), die die JSONPlaceholder-API abfragt: https://jsonplaceholder.typicode.com/users/{user_id}. Sie soll:
- ein Timeout von 5 Sekunden setzen
- bei 4xx/5xx den Fehler werfen
- bei Netzwerk-Fehler eine eigene Exception
APIErrorwerfen, die den Original-Fehler als Kontext mitgibt (raise ... from) - das User-Dict mit Name und Email zurückgeben
Lösung
Wrappe HTTP-Aufruf, transformiere generische Netzwerk-Fehler zu domänenspezifischen:
import requests class APIError(Exception): pass def user_holen(user_id): url = f"https://jsonplaceholder.typicode.com/users/{user_id}" try: r = requests.get(url, timeout=5) r.raise_for_status() daten = r.json() except requests.RequestException as e: raise APIError(f"Konnte User {user_id} nicht laden") from e return {"name": daten["name"], "email": daten["email"]} try: user = user_holen(1) print(user) except APIError as e: print(f"Fehler: {e}") print(f"Ursache: {e.__cause__}")
raise APIError(...) from e verknüpft die neue Exception mit der ursprünglichen – im Traceback steht „The above exception was the direct cause of the following exception". raise_for_status() wirft bei 4xx/5xx automatisch.
Schreibe ein Skript, das in einem Verzeichnis logs/ rekursiv alle .log-Dateien findet und für jede zählt, wie oft das Wort ERROR vorkommt. Gib pro Datei pfad: anzahl aus, sortiert nach Anzahl absteigend. Nutze pathlib und einen Generator-Ausdruck.
Lösung
Mit pathlib und einer Hilfsfunktion, die für jede Datei die ERROR-Zahl bestimmt:
from pathlib import Path def zaehle_errors(pfad): with pfad.open(encoding="utf-8", errors="ignore") as f: return sum(1 for zeile in f if "ERROR" in zeile) logs = Path("logs").rglob("*.log") ergebnisse = [(p, zaehle_errors(p)) for p in logs] ergebnisse.sort(key=lambda t: t[1], reverse=True) for pfad, anzahl in ergebnisse: print(f"{pfad}: {anzahl}")
Der Generator-Ausdruck sum(1 for zeile in f if "ERROR" in zeile) liest die Datei zeilenweise – bei Riesendateien viel speicher-schonender als alles auf einmal in den RAM zu laden.
Gegeben sei diese Klasse:
class Warenkorb: def __init__(self): self.artikel = [] def add(self, name, preis): if preis <= 0: raise ValueError("Preis muss positiv sein") self.artikel.append((name, preis)) def summe(self): return sum(p for _, p in self.artikel)
Schreibe eine Test-Datei test_warenkorb.py mit:
- einer Fixture, die einen frischen Warenkorb liefert
- einem Test, der
addundsummemit zwei Artikeln prüft - einem parametrisierten Test, der mit drei verschiedenen ungültigen Preisen (0, -1, -100) jeweils
ValueErrorerwartet - einem Test, der einen leeren Warenkorb prüft (Summe = 0)
Lösung
Alle drei wichtigen pytest-Werkzeuge kommen zum Einsatz:
# test_warenkorb.py import pytest from warenkorb import Warenkorb @pytest.fixture def leerer_korb(): return Warenkorb() def test_leerer_korb_hat_summe_null(leerer_korb): assert leerer_korb.summe() == 0 def test_add_und_summe(leerer_korb): leerer_korb.add("Brot", 2.50) leerer_korb.add("Milch", 1.20) assert leerer_korb.summe() == pytest.approx(3.70) assert len(leerer_korb.artikel) == 2 @pytest.mark.parametrize("preis", [0, -1, -100]) def test_add_negativer_preis_wirft(leerer_korb, preis): with pytest.raises(ValueError, match="positiv"): leerer_korb.add("Schrott", preis)
Wichtig: pytest.approx(3.70) für Float-Vergleiche – wegen Rundungsfehlern ist 2.50 + 1.20 == 3.70 nicht immer exakt wahr. match="positiv" prüft die Fehlermeldung. Die Fixture stellt sicher, dass jeder Test mit einem frischen Warenkorb startet – keine Wechselwirkungen.
Abschluss
Wenn du alle zehn Aufgaben sicher lösen kannst, hast du das Niveau, das die IHK-Abschlussprüfung im Bereich Python für Fortgeschrittene erwartet. Du beherrschst die Sprache nicht nur als „bessere Skriptsprache", sondern als vollwertiges Werkzeug für Software-Entwicklung mit OOP, Bibliotheks-Code, externer Integration und Qualitätssicherung.
Mit dem Wissen aus diesem Kurs bist du auch gut vorbereitet auf benachbarte Themen wie Objektorientierte Programmierung als allgemeines Konzept, Design Patterns als wiederverwendbare Bausteine, CI/CD & Automatisierung für den Build- und Deploy-Prozess und Git Grundlagen für die Versions-Verwaltung.
