Anhang 4: Web APIs, Pydantic, Dash und Python Decorators
November 2025 (7154 Words, 40 Minutes)
1. Einführung: Die technischen Grundlagen für Ihr Projekt
Willkommen zu Anhang 4! Dieser Anhang begleitet die Road Profile Viewer-Projektaufgabe, bei der Sie eine Dash-Webanwendung mit Datenbankpersistenz und Datei-Upload-Funktionalität erweitern werden.
Projekt-Repository: road-profile-viewer-class-template (Projekt 1)
Wenn Sie selbstständig lernen, können Sie dem Repository oben folgen und die README.md für die Aufgabenanforderungen lesen.
Dieser Anhang liefert Ihnen das technische Hintergrundwissen – ob als Auffrischung von Konzepten aus anderen Kursen oder als neues Material, das Sie noch nicht kennengelernt haben.
1.1 Was dieser Anhang behandelt
Am Ende dieser Vorlesung werden Sie verstehen:
- Web APIs mit FastAPI — Warum wir Frontend und Backend trennen und wie sie kommunizieren
- CRUD-Operationen — Die grundlegenden Datenbankoperationen und ihre REST-Äquivalente
- Pydantic Models — Datenvalidierung, die Fehler abfängt, bevor sie Probleme verursachen
- Dash als Frontend — Warum Dash besonders ist und wie es sich von traditionellen Web-Frontends unterscheidet
- Python Decorators — Entmystifizierung der
@-Syntax, die Sie überall in FastAPI und Dash sehen
1.2 Ihr Projektkontext
In Ihrer Aufgabe bauen Sie ein System, das so aussieht:
┌─────────────────────────────────────────────────────────────────┐
│ Browser des Benutzers │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Dash Frontend (Python) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Dropdown │ │ Upload │ │ Plotly Visualisierung │ │
│ │ Auswahl │ │ Seite │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ FastAPI Backend (Python) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ GET /profiles│ │POST /profiles│ │ Pydantic Validierung │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SQLite Datenbank │
│ (id, name, x_coordinates, y_coordinates) │
└─────────────────────────────────────────────────────────────────┘
Jede Schicht hat eine spezifische Verantwortung:
- Dash behandelt die Benutzeroberfläche und Visualisierung
- FastAPI behandelt Geschäftslogik und Datenvalidierung
- SQLite speichert die Straßenprofile persistent
Lassen Sie uns jedes Teil verstehen.
2. Web APIs mit FastAPI verstehen
2.1 Was ist eine API?
API steht für Application Programming Interface (Programmierschnittstelle). Im Kontext der Webentwicklung ist eine API eine Möglichkeit für verschiedene Programme, über das Netzwerk miteinander zu kommunizieren.
Stellen Sie es sich wie ein Restaurant vor:
- Sie (der Client) gehen nicht in die Küche
- Sie sagen dem Kellner (der API), was Sie möchten
- Die Küche (der Server) bereitet Ihre Bestellung zu
- Der Kellner bringt das Ergebnis zurück
In Ihrem Projekt:
- Dash ist der Client (die Benutzeroberfläche)
- FastAPI ist der Kellner (die API)
- SQLite ist die Küche (wo Daten gespeichert und abgerufen werden)
2.2 Warum Frontend und Backend trennen?
Sie fragen sich vielleicht: “Warum lassen wir Dash nicht direkt mit der Datenbank kommunizieren?”
Gute Frage! Hier sind die Gründe für die Trennung:
| Aspekt | Direkter Datenbankzugriff | API-Schicht |
|---|---|---|
| Sicherheit | Datenbank-Credentials im Frontend-Code | Credentials bleiben auf dem Server |
| Validierung | Jedes Frontend dupliziert Validierung | Zentrale Validierung an einer Stelle |
| Flexibilität | DB-Änderung erfordert Änderung aller Frontends | DB ändern ohne Frontends anzufassen |
| Testing | Schwer zu testen ohne Datenbank | API kann unabhängig getestet werden |
| Skalierung | Alles skaliert zusammen | Frontend und Backend separat skalieren |
Für Ihre Aufgabe bringt die Trennung auch den zusätzlichen Punkt für den FastAPI-Ansatz!
2.3 REST APIs: Der Standardweg
REST (Representational State Transfer) ist der gebräuchlichste Stil für Web-APIs. Er verwendet Standard-HTTP-Methoden, um Operationen auf Ressourcen (Dinge wie Straßenprofile) durchzuführen.
Die wichtigsten HTTP-Methoden sind:
| HTTP-Methode | Zweck | Beispiel-URL | Was sie tut |
|---|---|---|---|
GET |
Daten lesen | /profiles |
Alle Profile abrufen |
GET |
Ein Element lesen | /profiles/1 |
Profil mit id=1 abrufen |
POST |
Neue Daten erstellen | /profiles |
Neues Profil erstellen |
PUT |
Bestehende Daten aktualisieren | /profiles/1 |
Profil mit id=1 aktualisieren |
DELETE |
Daten entfernen | /profiles/1 |
Profil mit id=1 löschen |
2.4 Warum FastAPI?
FastAPI ist ein modernes Python-Web-Framework. Hier ist, warum es beliebt ist:
1. Type Hints = Automatische Validierung
@app.get("/profiles/{profile_id}")
def get_profile(profile_id: int): # FastAPI validiert, dass profile_id ein int ist
...
Wenn jemand /profiles/abc anfragt, gibt FastAPI automatisch einen Fehler zurück — Sie schreiben keinen Validierungscode.
2. Automatische Dokumentation
FastAPI generiert interaktive API-Dokumentation unter /docs. Sie können Ihre API direkt im Browser testen!
3. Modernes Python
FastAPI verwendet Python 3.6+ Features wie Type Hints und async/await, was den Code sauberer und wartbarer macht.
4. Performance
FastAPI ist eines der schnellsten Python-Frameworks, vergleichbar mit Node.js und Go.
2.5 Ihre erste FastAPI-Anwendung
Hier ist eine minimale FastAPI-App für Ihr Projekt:
# api/main.py
from fastapi import FastAPI
app = FastAPI()
# In-Memory-Speicher für jetzt (Sie ersetzen das später mit Datenbank)
profiles = [
{"id": 1, "name": "default", "x_coordinates": [0, 10, 20], "y_coordinates": [0, 5, 2]}
]
@app.get("/profiles")
def list_profiles():
"""Gibt alle Straßenprofile zurück."""
return profiles
@app.get("/profiles/{profile_id}")
def get_profile(profile_id: int):
"""Gibt ein bestimmtes Straßenprofil nach ID zurück."""
for profile in profiles:
if profile["id"] == profile_id:
return profile
return {"error": "Profil nicht gefunden"}
@app.post("/profiles")
def create_profile(profile: dict):
"""Erstellt ein neues Straßenprofil."""
profile["id"] = len(profiles) + 1
profiles.append(profile)
return profile
API starten:
uvicorn api.main:app --reload
Testen:
# Alle Profile abrufen
curl http://localhost:8000/profiles
# Profil mit id=1 abrufen
curl http://localhost:8000/profiles/1
# Neues Profil erstellen
curl -X POST http://localhost:8000/profiles \
-H "Content-Type: application/json" \
-d '{"name": "mountain_road", "x_coordinates": [0, 10, 20], "y_coordinates": [0, 5, 8]}'
2.6 Wie Dash mit FastAPI kommuniziert
In Ihrer Dash-Anwendung verwenden Sie die requests-Bibliothek, um mit FastAPI zu kommunizieren:
# In Ihrer Dash-App
import requests
# Alle Profile für das Dropdown abrufen
response = requests.get("http://localhost:8000/profiles")
profiles = response.json() # [{"id": 1, "name": "default", ...}, ...]
# Neues Profil aus hochgeladenem JSON erstellen
new_profile = {
"name": "mountain_road",
"x_coordinates": [0, 10, 20],
"y_coordinates": [0, 5, 8]
}
response = requests.post("http://localhost:8000/profiles", json=new_profile)
created = response.json()
Dies ist die Grundlage der Architektur Ihres Projekts!
3. CRUD-Operationen: Eine Auffrischung
3.1 Was ist CRUD?
CRUD ist ein Akronym für die vier grundlegenden Operationen, die Sie mit Daten durchführen können:
- Create — Neue Daten hinzufügen
- Read — Bestehende Daten abrufen
- Update — Bestehende Daten ändern
- Delete — Daten entfernen
Wenn Sie einen Datenbankkurs besucht haben, haben Sie diese Operationen in SQL gesehen:
| CRUD-Operation | SQL-Statement | Beispiel |
|---|---|---|
| Create | INSERT |
INSERT INTO profiles (name, x_coords) VALUES ('road1', '[0,10]') |
| Read | SELECT |
SELECT * FROM profiles WHERE id = 1 |
| Update | UPDATE |
UPDATE profiles SET name = 'new_name' WHERE id = 1 |
| Delete | DELETE |
DELETE FROM profiles WHERE id = 1 |
3.2 CRUD in REST APIs
REST APIs bilden CRUD-Operationen auf HTTP-Methoden ab:
| CRUD | HTTP-Methode | URL-Muster | Request Body |
|---|---|---|---|
| Create | POST |
/profiles |
Neue Profildaten |
| Read (alle) | GET |
/profiles |
Keiner |
| Read (eines) | GET |
/profiles/{id} |
Keiner |
| Update | PUT |
/profiles/{id} |
Aktualisierte Profildaten |
| Delete | DELETE |
/profiles/{id} |
Keiner |
3.3 CRUD für Straßenprofile
Für Ihre Aufgabe sieht jede Operation genau so aus:
Create — Neues Straßenprofil hinzufügen:
POST /profiles
Content-Type: application/json
{
"name": "mountain_road",
"x_coordinates": [0.0, 10.0, 20.0, 30.0],
"y_coordinates": [0.0, 2.0, 5.0, 3.0]
}
Antwort:
{
"id": 2,
"name": "mountain_road",
"x_coordinates": [0.0, 10.0, 20.0, 30.0],
"y_coordinates": [0.0, 2.0, 5.0, 3.0]
}
Read — Alle Profile abrufen (für Dropdown):
GET /profiles
Antwort:
[
{"id": 1, "name": "default", "x_coordinates": [...], "y_coordinates": [...]},
{"id": 2, "name": "mountain_road", "x_coordinates": [...], "y_coordinates": [...]}
]
Read — Ein Profil abrufen (für Visualisierung):
GET /profiles/2
Antwort:
{
"id": 2,
"name": "mountain_road",
"x_coordinates": [0.0, 10.0, 20.0, 30.0],
"y_coordinates": [0.0, 2.0, 5.0, 3.0]
}
Update — Profil umbenennen:
PUT /profiles/2
Content-Type: application/json
{
"name": "alpine_road",
"x_coordinates": [0.0, 10.0, 20.0, 30.0],
"y_coordinates": [0.0, 2.0, 5.0, 3.0]
}
Delete — Profil entfernen:
DELETE /profiles/2
4. Pydantic Models: Datenvalidierung leicht gemacht
4.1 Das Problem: Ungültige Daten
Was passiert, wenn jemand dieses JSON hochlädt?
{
"name": "",
"x_coordinates": [0, 10, 20],
"y_coordinates": [0, 5]
}
Es gibt mehrere Probleme:
- Leerer Name (sollte 1-100 Zeichen sein)
- x- und y-Koordinaten haben unterschiedliche Längen (3 vs 2)
Ohne Validierung gelangen diese schlechten Daten in Ihre Datenbank und verursachen später Fehler.
4.2 Was ist Pydantic?
Pydantic ist eine Python-Bibliothek für Datenvalidierung mit Type Hints. Anstatt Validierungscode manuell zu schreiben, definieren Sie ein Model und Pydantic übernimmt die Prüfung für Sie.
from pydantic import BaseModel
class RoadProfile(BaseModel):
name: str
x_coordinates: list[float]
y_coordinates: list[float]
Wenn Sie jetzt ein RoadProfile erstellen:
# Das funktioniert
profile = RoadProfile(
name="mountain",
x_coordinates=[0.0, 10.0, 20.0],
y_coordinates=[0.0, 5.0, 8.0]
)
# Das schlägt mit klarer Fehlermeldung fehl
profile = RoadProfile(
name=123, # Sollte String sein!
x_coordinates=[0.0, 10.0],
y_coordinates=[0.0, 5.0]
)
# ValidationError: name - Input should be a valid string
4.3 Einschränkungen mit Field hinzufügen
Für Ihre Aufgabe müssen Namen 1-100 Zeichen haben und Koordinaten brauchen mindestens 2 Punkte:
from pydantic import BaseModel, Field
from typing import List
class RoadProfile(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
x_coordinates: List[float] = Field(..., min_length=2)
y_coordinates: List[float] = Field(..., min_length=2)
Die ... bedeutet, dass das Feld erforderlich ist (kein Standardwert).
Validierung testen:
# Leerer Name - schlägt fehl!
RoadProfile(name="", x_coordinates=[0, 10], y_coordinates=[0, 5])
# ValidationError: name - String should have at least 1 character
# Nur ein Punkt - schlägt fehl!
RoadProfile(name="road", x_coordinates=[0], y_coordinates=[0])
# ValidationError: x_coordinates - List should have at least 2 items
4.4 Benutzerdefinierte Validatoren
Ihre Aufgabe erfordert, dass x- und y-Koordinaten die gleiche Länge haben. Pydantic kann dies nicht allein mit Field ausdrücken — Sie brauchen einen benutzerdefinierten Validator:
from pydantic import BaseModel, Field, model_validator
from typing import List
class RoadProfile(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
x_coordinates: List[float] = Field(..., min_length=2)
y_coordinates: List[float] = Field(..., min_length=2)
@model_validator(mode='after')
def check_coordinate_lengths(self):
if len(self.x_coordinates) != len(self.y_coordinates):
raise ValueError(
f"x_coordinates ({len(self.x_coordinates)} Punkte) und "
f"y_coordinates ({len(self.y_coordinates)} Punkte) müssen die gleiche Länge haben"
)
return self
Jetzt schlagen ungleiche Längen fehl:
RoadProfile(
name="bad_road",
x_coordinates=[0, 10, 20],
y_coordinates=[0, 5] # Nur 2 Punkte!
)
# ValidationError: x_coordinates (3 Punkte) und y_coordinates (2 Punkte) müssen die gleiche Länge haben
4.5 Pydantic + FastAPI Integration
Hier ist die Magie: FastAPI verwendet Pydantic automatisch!
from fastapi import FastAPI
from pydantic import BaseModel, Field, model_validator
from typing import List
app = FastAPI()
class RoadProfileCreate(BaseModel):
"""Schema zum Erstellen eines neuen Straßenprofils."""
name: str = Field(..., min_length=1, max_length=100)
x_coordinates: List[float] = Field(..., min_length=2)
y_coordinates: List[float] = Field(..., min_length=2)
@model_validator(mode='after')
def check_coordinate_lengths(self):
if len(self.x_coordinates) != len(self.y_coordinates):
raise ValueError("x- und y-Koordinaten müssen die gleiche Länge haben")
return self
class RoadProfileResponse(BaseModel):
"""Schema für Straßenprofil-Antworten (enthält ID)."""
id: int
name: str
x_coordinates: List[float]
y_coordinates: List[float]
@app.post("/profiles", response_model=RoadProfileResponse)
def create_profile(profile: RoadProfileCreate):
# FastAPI validiert 'profile' automatisch mit Pydantic!
# Bei Validierungsfehler gibt FastAPI einen 422-Fehler mit Details zurück
# Ihre Datenbanklogik hier...
new_id = save_to_database(profile)
return RoadProfileResponse(
id=new_id,
name=profile.name,
x_coordinates=profile.x_coordinates,
y_coordinates=profile.y_coordinates
)
Ungültige Anfrage:
curl -X POST http://localhost:8000/profiles \
-H "Content-Type: application/json" \
-d '{"name": "", "x_coordinates": [0], "y_coordinates": [0, 5]}'
Automatische Fehlerantwort (422 Unprocessable Entity):
{
"detail": [
{
"loc": ["body", "name"],
"msg": "String should have at least 1 character",
"type": "string_too_short"
},
{
"loc": ["body", "x_coordinates"],
"msg": "List should have at least 2 items",
"type": "too_short"
}
]
}
4.6 Über die Übung hinaus: Weitere Pydantic-Features
Pydantic kann viel mehr. Hier sind einige Muster, die Sie nützlich finden könnten:
Optionale Felder mit Standardwerten:
from typing import Optional
class RoadProfile(BaseModel):
name: str
description: Optional[str] = None # Optional, Standardwert None
created_at: str = Field(default_factory=lambda: datetime.now().isoformat())
Verschachtelte Models:
class Coordinates(BaseModel):
x: List[float]
y: List[float]
class RoadProfile(BaseModel):
name: str
coordinates: Coordinates # Verschachteltes Model
Feld-Aliase (für JSON mit anderen Schlüsselnamen):
class RoadProfile(BaseModel):
name: str
x_coords: List[float] = Field(..., alias="x_coordinates")
class Config:
populate_by_name = True # Akzeptiert sowohl 'x_coords' als auch 'x_coordinates'
Serialisierung zu JSON:
profile = RoadProfile(name="test", x_coordinates=[0, 10], y_coordinates=[0, 5])
# In Dictionary konvertieren
profile.model_dump()
# {'name': 'test', 'x_coordinates': [0.0, 10.0], 'y_coordinates': [0.0, 5.0]}
# In JSON-String konvertieren
profile.model_dump_json()
# '{"name":"test","x_coordinates":[0.0,10.0],"y_coordinates":[0.0,5.0]}'
5. Dash als Python-Frontend
5.1 Was macht Dash besonders?
Dash unterscheidet sich von traditionellen Web-Frontends:
| Aspekt | Traditionelles Frontend (React, Vue) | Dash |
|---|---|---|
| Sprache | JavaScript/TypeScript | Python |
| Wo Code läuft | Im Browser | Auf dem Server (größtenteils) |
| Lernkurve | HTML + CSS + JS + Framework | Python + Dash-Komponenten |
| Am besten für | Komplexe interaktive Apps | Daten-Apps & Dashboards |
| Visualisierung | Separate Bibliotheken (D3, Chart.js) | Plotly eingebaut |
Für Data Scientists und Ingenieure, die Python kennen, ermöglicht Dash das Erstellen von Web-Apps ohne JavaScript zu lernen.
5.2 Das versteckte Backend
Hier ist die wichtige Erkenntnis: Dash führt im Hintergrund einen Flask-Server aus.
Wenn ein Benutzer mit Ihrer Dash-App interagiert:
- Benutzer klickt ein Dropdown im Browser
- Browser sendet eine Anfrage an den Dash-Server
- Ihre Python-Callback-Funktion läuft auf dem Server
- Dash sendet das Ergebnis zurück an den Browser
- Browser aktualisiert die Anzeige
┌──────────────────┐ ┌──────────────────┐
│ Browser des │ ──────► │ Dash Server │
│ Benutzers │ │ (Python) │
│ [Dropdown ▼] │ │ │
│ │ ◄────── │ Callback läuft │
│ [Aktualisierter │ │ hier │
│ Graph] │ │ │
└──────────────────┘ └──────────────────┘
Deshalb ist Dash “besonders” — es ist wirklich ein Full-Stack-Framework, das sowohl Frontend als auch Backend behandelt.
5.3 Dash-Komponenten: Bausteine
Dash bietet vorgefertigte Komponenten, die in HTML übersetzt werden:
from dash import Dash, html, dcc
app = Dash(__name__)
app.layout = html.Div([
html.H1("Road Profile Viewer"), # <h1>Road Profile Viewer</h1>
dcc.Dropdown( # <select>...</select>
id="profile-dropdown",
options=[
{"label": "Default", "value": 1},
{"label": "Mountain", "value": 2}
],
value=1
),
dcc.Graph(id="profile-graph") # <div> mit Plotly-Chart
])
html.*— Standard-HTML-Elemente (div, h1, p, etc.)dcc.*— Dash Core Components (interaktive Elemente wie Dropdowns, Graphen, Eingaben)
5.4 Plotly-Integration
Dash wurde von Plotly entwickelt, daher integrieren sich Charts nahtlos:
import plotly.graph_objects as go
from dash import Dash, dcc, html, callback, Output, Input
app = Dash(__name__)
app.layout = html.Div([
dcc.Dropdown(
id="profile-dropdown",
options=[{"label": "Default", "value": 1}],
value=1
),
dcc.Graph(id="profile-graph")
])
@callback(
Output("profile-graph", "figure"),
Input("profile-dropdown", "value")
)
def update_graph(profile_id):
# Profildaten abrufen (von API in Ihrem Projekt)
x = [0, 10, 20, 30]
y = [0, 5, 8, 3]
# Plotly-Figur erstellen
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='Straßenprofil'))
fig.update_layout(title="Straßenprofil", xaxis_title="Entfernung", yaxis_title="Höhe")
return fig
if __name__ == "__main__":
app.run(debug=True)
5.5 Mehrseitige Dash-Apps
Ihre Aufgabe erfordert eine Upload-Seite unter /upload. Dash unterstützt mehrseitige Apps:
pages/
├── home.py # Haupt-Visualisierungsseite
└── upload.py # Upload-Seite
# pages/home.py
from dash import html, dcc, register_page
register_page(__name__, path="/")
layout = html.Div([
html.H1("Road Profile Viewer"),
dcc.Dropdown(id="profile-dropdown"),
dcc.Graph(id="profile-graph"),
dcc.Link("Zum Upload", href="/upload")
])
# pages/upload.py
from dash import html, dcc, register_page
register_page(__name__, path="/upload")
layout = html.Div([
html.H1("Straßenprofil hochladen"),
dcc.Upload(
id="upload-data",
children=html.Div(["Drag and Drop oder ", html.A("Dateien auswählen")]),
),
html.Div(id="preview-container"),
html.Button("Profil speichern", id="save-button"),
dcc.Link("Zurück zum Viewer", href="/")
])
# app.py
from dash import Dash, page_container
app = Dash(__name__, use_pages=True)
app.layout = page_container
if __name__ == "__main__":
app.run(debug=True)
5.6 Warum Dash Decorators braucht
Sie haben wahrscheinlich dieses Muster in Dash bemerkt:
@callback(
Output("profile-graph", "figure"),
Input("profile-dropdown", "value")
)
def update_graph(selected_profile):
...
Was ist @callback? Es ist ein Decorator. Dash verwendet Decorators um:
- Ihre Funktion als Callback zu registrieren
- Inputs (was den Callback auslöst) mit Outputs (was aktualisiert wird) zu verbinden
- Die gesamte Netzwerkkommunikation automatisch zu behandeln
Lassen Sie uns Decorators im nächsten Abschnitt entmystifizieren.
6. Python Decorators entmystifiziert
6.1 Das Mysterium
Wenn Sie Dash oder FastAPI verwendet haben, haben Sie dieses Muster gesehen:
@app.get("/profiles")
def list_profiles():
return profiles
oder
@callback(Output("graph", "figure"), Input("dropdown", "value"))
def update_graph(value):
...
Was macht das @? Warum steht Code über der Funktionsdefinition?
6.2 Funktionen als First-Class-Objekte
In Python sind Funktionen Objekte wie alle anderen. Sie können:
Sie Variablen zuweisen:
def greet(name):
return f"Hallo, {name}!"
say_hello = greet # Keine Klammern = Referenz auf Funktion, nicht Aufruf
print(say_hello("Alice")) # "Hallo, Alice!"
Sie an andere Funktionen übergeben:
def apply_twice(func, value):
return func(func(value))
def add_one(x):
return x + 1
result = apply_twice(add_one, 5) # add_one(add_one(5)) = 7
Sie aus Funktionen zurückgeben:
def create_multiplier(factor):
def multiply(x):
return x * factor
return multiply # Innere Funktion zurückgeben
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
Dieses letzte Muster — eine Funktion, die eine Funktion zurückgibt — ist der Schlüssel zum Verständnis von Decorators.
6.3 Einen Decorator Schritt für Schritt bauen
Angenommen, wir möchten jeden Funktionsaufruf loggen. Ohne Decorators:
def greet(name):
print(f"greet wurde aufgerufen mit: {name}") # Manuelles Logging
return f"Hallo, {name}!"
def farewell(name):
print(f"farewell wurde aufgerufen mit: {name}") # Duplikat!
return f"Auf Wiedersehen, {name}!"
Das ist mühsam und fehleranfällig. Stattdessen erstellen wir einen Decorator:
Schritt 1: Eine Funktion, die eine andere Funktion umhüllt
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"{func.__name__} wurde aufgerufen mit args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} gab zurück: {result}")
return result
return wrapper
Schritt 2: Den Wrapper manuell anwenden
def greet(name):
return f"Hallo, {name}!"
greet = log_calls(greet) # greet durch umhüllte Version ersetzen
greet("Alice")
# Ausgabe:
# greet wurde aufgerufen mit args=('Alice',), kwargs={}
# greet gab zurück: Hallo, Alice!
Schritt 3: @-Syntax verwenden (syntaktischer Zucker)
Die @decorator-Syntax ist nur eine Kurzschreibweise:
@log_calls
def greet(name):
return f"Hallo, {name}!"
# Das ist EXAKT äquivalent zu:
# def greet(name):
# return f"Hallo, {name}!"
# greet = log_calls(greet)
Das ist alles, was ein Decorator ist! Es ist eine Funktion, die eine Funktion nimmt und eine modifizierte Version zurückgibt.
6.4 Decorators mit Argumenten
FastAPI verwendet Decorators wie @app.get("/profiles"). Wie funktioniert das?
Der Trick ist Verschachtelung: eine Funktion, die einen Decorator zurückgibt.
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Hallo!")
say_hello()
# Ausgabe:
# Hallo!
# Hallo!
# Hallo!
Die Kette ist:
repeat(3)gibt eine Decorator-Funktion zurück- Dieser Decorator wird auf
say_helloangewendet
6.5 Wie FastAPI-Decorators funktionieren
Jetzt können Sie FastAPI verstehen:
from fastapi import FastAPI
app = FastAPI()
@app.get("/profiles")
def list_profiles():
return [{"id": 1, "name": "default"}]
Was passiert:
app.get("/profiles")gibt einen Decorator zurück- Dieser Decorator:
- Registriert die URL
/profilesmit HTTP-MethodeGET - Verknüpft sie mit der
list_profiles-Funktion - Gibt die Funktion unverändert (oder leicht modifiziert) zurück
- Registriert die URL
Hier ist eine vereinfachte Version dessen, was FastAPI intern tut:
class SimpleFastAPI:
def __init__(self):
self.routes = {}
def get(self, path):
def decorator(func):
self.routes[("GET", path)] = func
return func
return decorator
def post(self, path):
def decorator(func):
self.routes[("POST", path)] = func
return func
return decorator
app = SimpleFastAPI()
@app.get("/profiles")
def list_profiles():
return [{"id": 1}]
@app.post("/profiles")
def create_profile(data):
return {"id": 2, **data}
# Jetzt enthält app.routes:
# {
# ("GET", "/profiles"): list_profiles,
# ("POST", "/profiles"): create_profile
# }
6.6 Wie Dash-Decorators funktionieren
Dashs @callback funktioniert ähnlich:
@callback(
Output("graph", "figure"),
Input("dropdown", "value")
)
def update_graph(value):
...
Der Decorator:
- Registriert
update_graphals Callback - Verbindet ihn mit den angegebenen Inputs und Outputs
- Dash ruft diese Funktion auf, wenn sich
dropdown.valueändert
Hier ist eine vereinfachte Version:
class SimpleDash:
def __init__(self):
self.callbacks = []
def callback(self, output, *inputs):
def decorator(func):
self.callbacks.append({
"function": func,
"output": output,
"inputs": inputs
})
return func
return decorator
app = SimpleDash()
@app.callback(Output("graph", "figure"), Input("dropdown", "value"))
def update_graph(value):
return create_figure(value)
# Jetzt enthält app.callbacks Informationen darüber, wann update_graph aufgerufen wird
6.7 Häufige eingebaute Decorators
Python hat mehrere nützliche eingebaute Decorators:
@property — Methode in schreibgeschütztes Attribut verwandeln:
class RoadProfile:
def __init__(self, x_coords, y_coords):
self._x = x_coords
self._y = y_coords
@property
def num_points(self):
return len(self._x)
profile = RoadProfile([0, 10, 20], [0, 5, 8])
print(profile.num_points) # 3 (keine Klammern nötig!)
@staticmethod — Eine Methode, die kein self braucht:
class MathUtils:
@staticmethod
def add(a, b):
return a + b
MathUtils.add(2, 3) # 5 (keine Instanz nötig)
@classmethod — Eine Methode, die die Klasse empfängt, nicht die Instanz:
class RoadProfile:
def __init__(self, name, x, y):
self.name = name
self.x = x
self.y = y
@classmethod
def from_json(cls, json_data):
return cls(
json_data["name"],
json_data["x_coordinates"],
json_data["y_coordinates"]
)
data = {"name": "test", "x_coordinates": [0, 10], "y_coordinates": [0, 5]}
profile = RoadProfile.from_json(data)
6.8 Praktisches Beispiel: Logging-Decorator für API-Debugging
Hier ist ein nützlicher Decorator zum Debuggen Ihrer FastAPI-Endpunkte:
import time
from functools import wraps
def log_endpoint(func):
@wraps(func) # Bewahrt Funktionsname und Docstring
def wrapper(*args, **kwargs):
start = time.time()
print(f"→ {func.__name__} aufgerufen")
try:
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"← {func.__name__} zurückgegeben in {elapsed:.3f}s")
return result
except Exception as e:
print(f"✗ {func.__name__} warf {type(e).__name__}: {e}")
raise
return wrapper
# Verwendung in FastAPI
@app.get("/profiles")
@log_endpoint
def list_profiles():
return profiles
# Bei Aufruf:
# → list_profiles aufgerufen
# ← list_profiles zurückgegeben in 0.001s
Hinweis: Die Reihenfolge der Decorators ist wichtig! Sie werden von unten nach oben angewendet:
@app.get("/profiles") # Wird zweites angewendet
@log_endpoint # Wird erstes angewendet
def list_profiles():
...
# Äquivalent zu:
# list_profiles = app.get("/profiles")(log_endpoint(list_profiles))
7. Alles zusammenfügen
7.1 Architekturübersicht
So passen alle Teile in Ihrem Projekt zusammen:
┌─────────────────────────────────────────────────────────────────────────┐
│ Browser des Benutzers │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Dash-generiertes HTML/JS │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Dropdown │ │ Upload Form │ │ Plotly Graph │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
HTTP-Anfragen │ HTTP-Antworten
(JSON-Daten) ▼ (JSON-Daten)
┌─────────────────────────────────────────────────────────────────────────┐
│ Dash Server (Python) │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ @callback Funktionen │ │
│ │ │ │
│ │ @callback(Output("graph"), Input("dropdown")) │ │
│ │ def update_graph(profile_id): │ │
│ │ profile = requests.get(f"{API_URL}/profiles/{profile_id}") │ │
│ │ return create_figure(profile.json()) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
HTTP-Anfragen │ HTTP-Antworten
(JSON-Daten) ▼ (JSON-Daten)
┌─────────────────────────────────────────────────────────────────────────┐
│ FastAPI Server (Python) │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ @app.get, @app.post Routen │ │
│ │ │ │
│ │ @app.post("/profiles") │ │
│ │ def create_profile(profile: RoadProfileCreate): ← Pydantic! │ │
│ │ # Validierung erfolgt automatisch │ │
│ │ return db.create(profile) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
SQL-Abfragen │ Abfrageergebnisse
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ SQLite Datenbank │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ profiles Tabelle: │ │
│ │ ┌────┬───────────────┬────────────────────┬───────────────────┐ │ │
│ │ │ id │ name │ x_coordinates │ y_coordinates │ │ │
│ │ ├────┼───────────────┼────────────────────┼───────────────────┤ │ │
│ │ │ 1 │ default │ [0, 10, 20, 30] │ [0, 5, 8, 3] │ │ │
│ │ │ 2 │ mountain_road │ [0, 10, 20, 30, 40]│ [0, 2, 5, 8, 6] │ │ │
│ │ └────┴───────────────┴────────────────────┴───────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
7.2 Datenfluss: Upload zur Visualisierung
Lassen Sie uns verfolgen, was passiert, wenn ein Benutzer ein neues Straßenprofil hochlädt:
Schritt 1: Benutzer lädt JSON-Datei in Dash hoch
# Dash-Callback wird durch Datei-Upload ausgelöst
@callback(Output("preview-graph", "figure"), Input("upload-data", "contents"))
def preview_upload(contents):
if contents is None:
return {}
# Base64-Inhalt vom Upload dekodieren
content_string = contents.split(",")[1]
decoded = base64.b64decode(content_string)
data = json.loads(decoded)
# Vorschau-Graph anzeigen
return create_figure(data["x_coordinates"], data["y_coordinates"])
Schritt 2: Benutzer klickt “Speichern” → Dash sendet an FastAPI
@callback(Output("save-message", "children"), Input("save-button", "n_clicks"), State("upload-data", "contents"))
def save_profile(n_clicks, contents):
if n_clicks is None:
return ""
# Hochgeladene Daten parsen
data = parse_upload(contents)
# An FastAPI senden
response = requests.post(f"{API_URL}/profiles", json=data)
if response.status_code == 200:
return "Profil erfolgreich gespeichert!"
else:
return f"Fehler: {response.json()['detail']}"
Schritt 3: FastAPI validiert mit Pydantic und speichert
@app.post("/profiles", response_model=RoadProfileResponse)
def create_profile(profile: RoadProfileCreate):
# Pydantic hat bereits validiert:
# - Name ist 1-100 Zeichen
# - x und y haben mindestens 2 Punkte
# - x und y haben gleiche Länge
# In Datenbank speichern
db_profile = db.create_profile(profile)
return db_profile
Schritt 4: Dropdown aktualisiert, Benutzer kann neues Profil ansehen
@callback(Output("profile-dropdown", "options"), Input("save-button", "n_clicks"))
def refresh_dropdown(_):
# Aktualisierte Liste von API abrufen
response = requests.get(f"{API_URL}/profiles")
profiles = response.json()
return [{"label": p["name"], "value": p["id"]} for p in profiles]
7.3 Wichtige Erkenntnisse
- APIs trennen Verantwortlichkeiten — Frontend behandelt Anzeige, Backend behandelt Logik und Daten
- CRUD bildet auf HTTP-Methoden ab — POST=Create, GET=Read, PUT=Update, DELETE=Delete
- Pydantic validiert automatisch — Regeln einmal definieren, Validierung erfolgt überall
- Dash ist durchweg Python — Kein JavaScript nötig für datenfokussierte Web-Apps
- Decorators sind nur Funktions-Wrapper — Sie registrieren, modifizieren oder erweitern Funktionen
- Die @-Syntax ist syntaktischer Zucker —
@decoratorentsprichtfunc = decorator(func)
8. Schnellreferenz
8.1 HTTP-Methoden → CRUD
| CRUD | HTTP | SQL | FastAPI Decorator |
|---|---|---|---|
| Create | POST | INSERT | @app.post("/path") |
| Read | GET | SELECT | @app.get("/path") |
| Update | PUT | UPDATE | @app.put("/path") |
| Delete | DELETE | DELETE | @app.delete("/path") |
8.2 Pydantic Validatoren
from pydantic import BaseModel, Field, field_validator, model_validator
class Example(BaseModel):
# Grundlegende Einschränkungen
name: str = Field(..., min_length=1, max_length=100)
count: int = Field(..., ge=0, le=1000) # >= 0, <= 1000
items: List[str] = Field(..., min_length=1)
# Feld-Level-Validierung
@field_validator('name')
@classmethod
def name_must_not_contain_spaces(cls, v):
if ' ' in v:
raise ValueError('darf keine Leerzeichen enthalten')
return v
# Model-Level-Validierung (nach allen Feldern)
@model_validator(mode='after')
def check_something(self):
# Zugriff auf self.name, self.count, etc.
return self
8.3 Decorator-Muster
# Einfacher Decorator (keine Argumente)
def my_decorator(func):
def wrapper(*args, **kwargs):
# Vorher
result = func(*args, **kwargs)
# Nachher
return result
return wrapper
@my_decorator
def my_function():
pass
# Decorator mit Argumenten
def my_decorator(arg1, arg2):
def decorator(func):
def wrapper(*args, **kwargs):
# arg1, arg2 hier verwenden
return func(*args, **kwargs)
return wrapper
return decorator
@my_decorator("value1", "value2")
def my_function():
pass
9. Weiterführende Literatur
- FastAPI Dokumentation: https://fastapi.tiangolo.com/
- Pydantic Dokumentation: https://docs.pydantic.dev/
- Dash Dokumentation: https://dash.plotly.com/
- Python Decorators Tutorial: https://realpython.com/primer-on-python-decorators/
- REST API Design: https://restfulapi.net/