Home

Anhang 4: Web APIs, Pydantic, Dash und Python Decorators

appendix fastapi pydantic dash decorators api crud python

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:

  1. Web APIs mit FastAPI — Warum wir Frontend und Backend trennen und wie sie kommunizieren
  2. CRUD-Operationen — Die grundlegenden Datenbankoperationen und ihre REST-Äquivalente
  3. Pydantic Models — Datenvalidierung, die Fehler abfängt, bevor sie Probleme verursachen
  4. Dash als Frontend — Warum Dash besonders ist und wie es sich von traditionellen Web-Frontends unterscheidet
  5. 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:

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:

In Ihrem Projekt:

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:

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:

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:

  1. Benutzer klickt ein Dropdown im Browser
  2. Browser sendet eine Anfrage an den Dash-Server
  3. Ihre Python-Callback-Funktion läuft auf dem Server
  4. Dash sendet das Ergebnis zurück an den Browser
  5. 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
])

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:

  1. Ihre Funktion als Callback zu registrieren
  2. Inputs (was den Callback auslöst) mit Outputs (was aktualisiert wird) zu verbinden
  3. 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:

  1. repeat(3) gibt eine Decorator-Funktion zurück
  2. Dieser Decorator wird auf say_hello angewendet

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:

  1. app.get("/profiles") gibt einen Decorator zurück
  2. Dieser Decorator:
    • Registriert die URL /profiles mit HTTP-Methode GET
    • Verknüpft sie mit der list_profiles-Funktion
    • Gibt die Funktion unverändert (oder leicht modifiziert) zurück

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:

  1. Registriert update_graph als Callback
  2. Verbindet ihn mit den angegebenen Inputs und Outputs
  3. 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

  1. APIs trennen Verantwortlichkeiten — Frontend behandelt Anzeige, Backend behandelt Logik und Daten
  2. CRUD bildet auf HTTP-Methoden ab — POST=Create, GET=Read, PUT=Update, DELETE=Delete
  3. Pydantic validiert automatisch — Regeln einmal definieren, Validierung erfolgt überall
  4. Dash ist durchweg Python — Kein JavaScript nötig für datenfokussierte Web-Apps
  5. Decorators sind nur Funktions-Wrapper — Sie registrieren, modifizieren oder erweitern Funktionen
  6. Die @-Syntax ist syntaktischer Zucker@decorator entspricht func = 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

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk