add synced notes on IT know how
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
Hier eine Einführung in „asynchron“ in der Python-Programmierung, für jemanden ohne Vorkenntnisse in diesem Bereich.
|
||||
|
||||
---
|
||||
|
||||
## 1. Intuitive Vorstellung: Was heißt „asynchron“?
|
||||
|
||||
Stell dir vor, du kochst:
|
||||
|
||||
- Du stellst Wasser auf den Herd (es braucht Zeit, bis es kocht).
|
||||
- Während du wartest, schneidest du Gemüse, bereitest Sauce vor usw.
|
||||
- Du **blockierst nicht** deine Zeit, indem du nur auf den Topf starrst.
|
||||
|
||||
**Asynchron** bedeutet in der Programmierung:
|
||||
Dein Programm kann etwas starten, das länger dauert (z.B. eine Netzwerkabfrage), und während es darauf wartet, **andere Dinge erledigen**, statt „untätig“ zu blockieren.
|
||||
|
||||
---
|
||||
|
||||
## 2. Grundbegriffe: synchron vs. asynchron
|
||||
|
||||
### 2.1 Synchron (blockierend)
|
||||
|
||||
Synchroner, „klassischer“ Code:
|
||||
|
||||
- Befehl A wird ausgeführt.
|
||||
- Erst wenn A fertig ist, wird B ausgeführt.
|
||||
- Wenn A lange wartet (z.B. auf eine Antwort aus dem Internet), **steht das ganze Programm an dieser Stelle still**.
|
||||
|
||||
Beispiel (synchron, blockierend):
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
print("Starte")
|
||||
time.sleep(5) # wartet 5 Sekunden – Programm ist blockiert
|
||||
print("Fertig")
|
||||
```
|
||||
|
||||
Während `time.sleep(5)` läuft, kann das Programm nichts anderes tun.
|
||||
|
||||
### 2.2 Asynchron (nicht-blockierend innerhalb eines Ablaufs)
|
||||
|
||||
Asynchroner Code versucht:
|
||||
|
||||
- Langsame Operationen (z.B. Netzwerk, Festplatte, Datenbank) so zu starten,
|
||||
- und während sie „laufen“, andere Aufgaben zu bearbeiten.
|
||||
|
||||
In Python machst du das typischerweise mit `async` und `await`.
|
||||
|
||||
Sehr vereinfachtes Beispiel:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def aufgabe(name, dauer):
|
||||
print(f"{name} gestartet")
|
||||
await asyncio.sleep(dauer) # nicht-blockierend warten
|
||||
print(f"{name} fertig")
|
||||
|
||||
async def main():
|
||||
# Zwei Aufgaben (Tasks) gleichzeitig laufen lassen
|
||||
task1 = asyncio.create_task(aufgabe("A", 2))
|
||||
task2 = asyncio.create_task(aufgabe("B", 2))
|
||||
|
||||
await task1
|
||||
await task2
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Typischer Ablauf:
|
||||
- „A gestartet“
|
||||
- „B gestartet“
|
||||
- (2 Sekunden vergehen)
|
||||
- „A fertig“
|
||||
- „B fertig“
|
||||
|
||||
Beide Aufgaben „warten gleichzeitig“, und die Zeit überlappt sich.
|
||||
|
||||
---
|
||||
|
||||
## 3. Wichtige Abgrenzungen: Begriffe, die oft durcheinandergehen
|
||||
|
||||
### 3.1 Nebenläufigkeit (Concurrency) vs. Parallelität
|
||||
|
||||
- **Nebenläufigkeit (Concurrency)**: Mehrere Aufgaben werden so organisiert, dass sie *scheinbar gleichzeitig* laufen, indem man schnell zwischen ihnen hin- und herschaltet.
|
||||
- **Parallelität (Parallelism)**: Mehrere Aufgaben laufen *wirklich gleichzeitig* auf mehreren CPU-Kernen.
|
||||
|
||||
Asynchrones Programmieren in Python (`asyncio`) ist in erster Linie ein Werkzeug für **Nebenläufigkeit**, nicht zwingend für echte Parallelität.
|
||||
|
||||
### 3.2 Threads vs. Async
|
||||
|
||||
- **Threads**:
|
||||
- Betriebssystem-Fäden (OS-Threads).
|
||||
- Können an verschiedenen CPU-Kernen parallel laufen.
|
||||
- Schwerer zu testen, zu debuggen (Race Conditions, Deadlocks).
|
||||
- **Async (z.B. asyncio in Python)**:
|
||||
- Läuft typischerweise in **einem** Thread.
|
||||
- Nutzt einen **Event Loop**, um zwischen Aufgaben zu wechseln, wenn sie gerade warten.
|
||||
- Sehr gut geeignet, wenn viele Aufgaben hauptsächlich **I/O-lastig** sind (Netzwerk, Dateien).
|
||||
|
||||
**Kurz:**
|
||||
- Viele Netzwerk-Anfragen gleichzeitig? → Async kann ideal sein.
|
||||
- Viel CPU-Berechnung (z.B. Bildverarbeitung)? → Threads oder Prozesse (Multiprocessing) sind oft sinnvoller.
|
||||
|
||||
### 3.3 Blocking vs. Non-blocking I/O
|
||||
|
||||
- **Blockierend**: „Lies aus dem Netzwerk“ – der Code bleibt stehen, bis Daten da sind.
|
||||
- **Non-blockierend**: „Lies aus dem Netzwerk, aber wenn gerade nichts da ist, mach solange andere Aufgaben.“
|
||||
|
||||
Asynchrones Python nutzt non-blocking I/O und einen Event Loop, um viele solcher Operationen gleichzeitig zu verwalten.
|
||||
|
||||
---
|
||||
|
||||
## 4. Asynchron in Python konkret: `asyncio`, `async`, `await`
|
||||
|
||||
### 4.1 Historischer Kontext
|
||||
|
||||
- Vor Python 3.4 gab es `asyncio` nur als externes Paket.
|
||||
- Ab Python 3.5 wurden die Schlüsselwörter `async` und `await` eingeführt und haben das Arbeiten mit Async deutlich angenehmer gemacht.
|
||||
|
||||
### 4.2 Zentrale Begriffe
|
||||
|
||||
- **Coroutine**: eine Funktion, die „angehalten“ und später fortgesetzt werden kann. In Python: definiert mit `async def`.
|
||||
- **Event Loop**: eine Schleife, die:
|
||||
- Aufgaben plant,
|
||||
- sie laufen lässt, bis sie warten müssen (z.B. auf I/O),
|
||||
- dann anderen Aufgaben CPU-Zeit gibt.
|
||||
- **Task**: eine geplante Coroutine, die vom Event Loop verwaltet wird.
|
||||
- **Future**: ein Platzhalter für ein Ergebnis, das noch nicht fertig ist.
|
||||
|
||||
### 4.3 Einfaches Beispiel: Event Loop und Coroutines
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def hallo():
|
||||
print("Hallo...")
|
||||
await asyncio.sleep(1) # simuliert I/O-Wartezeit
|
||||
print("...Welt!")
|
||||
|
||||
async def main():
|
||||
await hallo()
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
- `hallo()` ist eine Coroutine.
|
||||
- `await asyncio.sleep(1)` bedeutet: „warte 1 Sekunde, aber blockiere nicht den Event Loop“.
|
||||
|
||||
---
|
||||
|
||||
## 5. Welche Probleme werden durch asynchrones Programmieren gelöst?
|
||||
|
||||
### 5.1 Viele gleichzeitige I/O-Aufgaben
|
||||
|
||||
Typische Beispiele:
|
||||
|
||||
- Webserver, die viele gleichzeitige HTTP-Anfragen beantworten.
|
||||
- Web-Scraper oder Clients, die viele HTTP-Anfragen an andere Server stellen.
|
||||
- Chat-Server, WebSockets, Streaming.
|
||||
- Programme, die gleichzeitig:
|
||||
- Dateien lesen/schreiben,
|
||||
- mit einer Datenbank kommunizieren,
|
||||
- HTTP-Anfragen senden.
|
||||
|
||||
**Synchroner Ansatz**:
|
||||
Jede Anfrage blockiert einen Thread/Prozess, solange sie auf Antwort wartet → sehr viele Threads/Prozesse nötig.
|
||||
|
||||
**Asynchroner Ansatz**:
|
||||
Ein Event Loop verwaltet viele tausend Verbindungen in einem oder wenigen Threads, indem er immer dort weiterarbeitet, wo gerade Daten verfügbar sind.
|
||||
|
||||
### 5.2 Bessere Ressourcennutzung bei I/O-lastigen Programmen
|
||||
|
||||
Wenn dein Programm hauptsächlich:
|
||||
|
||||
- Daten lädt (HTTP, DB),
|
||||
- auf Antworten wartet,
|
||||
- nicht viel rechnet,
|
||||
|
||||
dann ist asynchroner Code oft **effizienter** (weniger Overhead, weniger Threads, bessere Skalierung).
|
||||
|
||||
### 5.3 Responsivere Anwendungen (z.B. GUIs)
|
||||
|
||||
In grafischen Anwendungen (oder auch CLI-Tools) willst du:
|
||||
|
||||
- Nicht, dass die Oberfläche „einfriert“, während eine Anfrage ans Internet läuft.
|
||||
- Stattdessen nutzt du asynchrone oder nebenläufige Mechanismen, damit der Haupt-Thread weiterhin Eingaben entgegennimmt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Praxisnahe Beispiele
|
||||
|
||||
### 6.1 Vergleich: synchron vs. asynchron HTTP-Anfragen
|
||||
|
||||
#### Synchron: nacheinander mit `requests`
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
urls = [
|
||||
"https://example.com",
|
||||
"https://httpbin.org/delay/2",
|
||||
"https://httpbin.org/delay/3",
|
||||
]
|
||||
|
||||
def fetch(url):
|
||||
print(f"Rufe {url} ab...")
|
||||
response = requests.get(url)
|
||||
print(f"{url}: Status {response.status_code}")
|
||||
|
||||
def main():
|
||||
for url in urls:
|
||||
fetch(url)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
- Jede Anfrage wartet, bis sie fertig ist.
|
||||
- Gesamtzeit ≈ Summe aller Wartezeiten.
|
||||
|
||||
#### Asynchron: gleichzeitig mit `aiohttp` und `asyncio`
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
urls = [
|
||||
"https://example.com",
|
||||
"https://httpbin.org/delay/2",
|
||||
"https://httpbin.org/delay/3",
|
||||
]
|
||||
|
||||
async def fetch(session, url):
|
||||
print(f"Rufe {url} ab...")
|
||||
async with session.get(url) as response:
|
||||
print(f"{url}: Status {response.status}")
|
||||
|
||||
async def main():
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = [fetch(session, url) for url in urls]
|
||||
await asyncio.gather(*tasks) # starte alle gleichzeitig
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
- Alle Anfragen werden „gleichzeitig“ gestartet.
|
||||
- Gesamtzeit ≈ maximale Einzeldauer, nicht Summe.
|
||||
|
||||
### 6.2 Viele „Schlaf-Aufgaben“ parallel (Simulation von I/O)
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
async def simulierte_io_aufgabe(n):
|
||||
dauer = random.uniform(0.5, 2.0)
|
||||
print(f"Aufgabe {n} startet, Dauer ~{dauer:.2f}s")
|
||||
await asyncio.sleep(dauer)
|
||||
print(f"Aufgabe {n} fertig")
|
||||
|
||||
async def main():
|
||||
tasks = [simulierte_io_aufgabe(i) for i in range(5)]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Output (ähnlich):
|
||||
|
||||
- Mehrere Aufgaben starten schnell hintereinander.
|
||||
- Sie enden in anderer Reihenfolge, je nach Dauer.
|
||||
- Die Gesamtzeit liegt in etwa bei der **längsten** Wartezeit, nicht bei der Summe aller.
|
||||
|
||||
---
|
||||
|
||||
## 7. Typische Herausforderungen und Stolperfallen
|
||||
|
||||
### 7.1 Denken in „async“ ist ungewohnt
|
||||
|
||||
Für Einsteiger:
|
||||
- Man kann `await` **nur** in `async def`-Funktionen verwenden.
|
||||
- Asynchrone Funktionen verhalten sich anders als normale:
|
||||
|
||||
```python
|
||||
async def foo():
|
||||
return 42
|
||||
|
||||
# Aufruf:
|
||||
result = foo() # das ist KEINE 42, sondern eine Coroutine!
|
||||
```
|
||||
|
||||
Du musst sie ausführen:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def foo():
|
||||
return 42
|
||||
|
||||
async def main():
|
||||
result = await foo()
|
||||
print(result)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### 7.2 Blockierender Code in asynchronem Kontext
|
||||
|
||||
Problem:
|
||||
- Du hast eine `async`-Funktion, benutzt darin aber eine **blockierende** Bibliothek (z.B. `requests`, `time.sleep`).
|
||||
- Dann blockierst du trotzdem den Event Loop, obwohl du „async“ verwendest.
|
||||
|
||||
Beispiel (so besser nicht):
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
async def schlecht():
|
||||
print("Blockiere Event Loop...")
|
||||
time.sleep(5) # blockiert den Event Loop komplett!
|
||||
print("Weiter geht's")
|
||||
|
||||
asyncio.run(schlecht())
|
||||
```
|
||||
|
||||
Lösung:
|
||||
- Entweder eine **asynchrone Alternative** verwenden (z.B. `aiohttp` statt `requests`).
|
||||
- Oder blockierende Funktion in einem Thread/Prozess auslagern (z.B. `asyncio.to_thread`).
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
def blockierende_funktion():
|
||||
time.sleep(5)
|
||||
return "fertig"
|
||||
|
||||
async def gut():
|
||||
print("Starte blockierende Funktion in Thread...")
|
||||
result = await asyncio.to_thread(blockierende_funktion)
|
||||
print("Ergebnis:", result)
|
||||
|
||||
asyncio.run(gut())
|
||||
```
|
||||
|
||||
### 7.3 Debugging und Fehlersuche
|
||||
|
||||
- Fehler in asynchronen Programmen können schwerer nachzuvollziehen sein.
|
||||
- Stack-Traces sehen anders aus, weil Coroutines, Tasks und der Event Loop beteiligt sind.
|
||||
- Es kann passieren, dass Tasks „stillschweigend“ fehlschlagen, wenn man sie nicht korrekt awaited oder Fehler nicht abfängt.
|
||||
|
||||
### 7.4 Testen von asynchronem Code
|
||||
|
||||
- Unit-Tests brauchen meist auch einen Event Loop.
|
||||
- Viele Testframeworks bieten dafür Mechanismen (`pytest` mit `pytest-asyncio`).
|
||||
|
||||
Beispiel mit `pytest-asyncio`:
|
||||
|
||||
```python
|
||||
# test_example.py
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
async def verdoppeln(x):
|
||||
await asyncio.sleep(0.1)
|
||||
return x * 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verdoppeln():
|
||||
assert await verdoppeln(21) == 42
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Wann lohnt sich asynchrones Programmieren (und wann nicht)?
|
||||
|
||||
**Sinnvoll bei:**
|
||||
|
||||
- Web-APIs, Microservices, Webserver.
|
||||
- Chat-Server, WebSocket-Anwendungen.
|
||||
- Web-Scraping vieler Seiten gleichzeitig.
|
||||
- I/O-lastigen Programmen mit vielen Netzwerk- oder Datenbankzugriffen.
|
||||
|
||||
**Weniger sinnvoll bei:**
|
||||
|
||||
- Reinen CPU-lastigen Aufgaben (z.B. numerische Berechnungen, Bildverarbeitung).
|
||||
- Hier helfen eher: mehrere Prozesse (`multiprocessing`) oder spezialisierte Bibliotheken (NumPy, Numba, etc.).
|
||||
- Kleinen Scripts, die nur wenige, einfache Schritte nacheinander machen – da ist synchroner Code oft einfacher und ausreichend.
|
||||
|
||||
---
|
||||
|
||||
## 9. Zusammenfassung in einfachen Worten
|
||||
|
||||
- **Asynchron** in Python bedeutet:
|
||||
Du kannst zeitaufwändige, I/O-lastige Aufgaben starten und in der Zwischenzeit andere Aufgaben erledigen, statt auf jede einzelne zu warten.
|
||||
|
||||
- Die Mechanismen dafür sind:
|
||||
- `async def` (Coroutines),
|
||||
- `await` (warten, ohne zu blockieren),
|
||||
- ein **Event Loop** (z.B. in `asyncio`).
|
||||
|
||||
- Es löst besonders gut Probleme mit **vielen gleichzeitigen I/O-Operationen** (Netzwerk, Datenbanken), wie bei Webservern und Web-Scrapern.
|
||||
|
||||
- Herausforderungen:
|
||||
- Umdenken gegenüber normalem, synchronem Code.
|
||||
- Aufpassen, keine blockierenden Funktionen im Event Loop zu verwenden.
|
||||
- Debugging und Testen sind etwas komplexer.
|
||||
|
||||
+499
@@ -0,0 +1,499 @@
|
||||
Im Folgenden bekommst du eine umfassende, aber einsteigerfreundliche Einführung in FastAPI.
|
||||
|
||||
---
|
||||
|
||||
## 1. Grundidee: Was ist FastAPI?
|
||||
|
||||
**FastAPI** ist ein modernes, schnelles Web-Framework für Python, mit dem du **Web-APIs** (Schnittstellen) bauen kannst.
|
||||
Eine API ist eine „Schnittstelle“, über die andere Programme mit deinem Programm sprechen können – z. B. eine Web-App, ein Mobile-App-Backend oder interne Services in einem Unternehmen.
|
||||
|
||||
Kernpunkte von FastAPI:
|
||||
|
||||
- **Schwerpunkt:** Aufbau von **HTTP-APIs** (REST-APIs, JSON-basierte APIs).
|
||||
- **Geschwindigkeit:** Sehr performant durch Nutzung von **asynchronem Python** (`async`/`await`), basierend auf **ASGI**.
|
||||
- **Typisierung:** Starke Nutzung von **Python-Typannotationen** (z. B. `str`, `int`, eigene Klassen).
|
||||
→ Daraus entstehen automatisch:
|
||||
- Validierung von Daten,
|
||||
- automatische Dokumentation (Swagger / OpenAPI),
|
||||
- bessere IDE-Unterstützung (Autovervollständigung, Fehlererkennung).
|
||||
- **Auto-Dokumentation:** FastAPI generiert automatisch eine **interaktive API-Dokumentation** im Browser.
|
||||
|
||||
Ein typisches „Hello World“ mit FastAPI sieht so aus:
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"message": "Hello World"}
|
||||
```
|
||||
|
||||
Starten kannst du das z. B. mit:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
Dann ist die API z. B. unter `http://127.0.0.1:8000` erreichbar.
|
||||
|
||||
---
|
||||
|
||||
## 2. Abgrenzung: FastAPI vs. verwandte Begriffe und Frameworks
|
||||
|
||||
### 2.1 FastAPI vs. „API“ / REST / HTTP allgemein
|
||||
|
||||
- **HTTP**: Das zugrunde liegende Protokoll, über das Browser oder andere Dienste kommunizieren.
|
||||
- **REST-API**: Eine Art, HTTP-APIs zu strukturieren (z. B. `GET /users`, `POST /orders`).
|
||||
- **FastAPI**: Ein **Framework**, das dir hilft, solche HTTP/REST-APIs in Python zu bauen.
|
||||
|
||||
FastAPI „spricht“ also HTTP, baut REST-APIs, ist aber selbst das **Werkzeug**, kein Protokoll.
|
||||
|
||||
---
|
||||
|
||||
### 2.2 FastAPI vs. Flask
|
||||
|
||||
**Flask** ist ein sehr bekanntes, minimalistisches Python-Webframework.
|
||||
|
||||
**Ähnlichkeiten:**
|
||||
|
||||
- Beide erlauben es, mit wenig Code HTTP-Endpunkte zu definieren.
|
||||
- Beide sind relativ leichtgewichtig und flexibel.
|
||||
|
||||
**Unterschiede:**
|
||||
|
||||
- **Asynchronität**:
|
||||
- Flask: traditionell synchron (WSGI), Async ist erst neuerdings und eingeschränkt verfügbar.
|
||||
- FastAPI: von Anfang an für **async** gebaut (ASGI).
|
||||
- **Typen & Validierung**:
|
||||
- Flask: Kein eingebautes System für automatische Validierung – du machst das selbst oder mit Erweiterungen.
|
||||
- FastAPI: Nutzt **[[Pydantic]]**-Modelle und Typannotationen → automatische Validierung.
|
||||
- **Dokumentation**:
|
||||
- Flask: Kein automatisches API-Dokumentations-UI.
|
||||
- FastAPI: Automatisch generierte OpenAPI/Swagger-UI unter `/docs` und `/redoc`.
|
||||
|
||||
Praxisbeispiel Vergleich:
|
||||
|
||||
**Flask:**
|
||||
|
||||
```python
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/items", methods=["POST"])
|
||||
def create_item():
|
||||
data = request.get_json()
|
||||
name = data.get("name")
|
||||
price = data.get("price")
|
||||
if not isinstance(name, str) or not isinstance(price, (int, float)):
|
||||
return jsonify({"error": "Invalid data"}), 400
|
||||
return jsonify({"name": name, "price": price})
|
||||
```
|
||||
|
||||
**FastAPI:**
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
@app.post("/items")
|
||||
def create_item(item: Item):
|
||||
# item ist schon validiert
|
||||
return item
|
||||
```
|
||||
|
||||
FastAPI übernimmt hier die Validierung automatisch.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 FastAPI vs. Django (und Django REST Framework)
|
||||
|
||||
**Django** ist ein „Full-Stack“-Framework:
|
||||
|
||||
- liefert Templates, ORM (Datenbankzugriff), Admin-Interface, Auth-System, Formulare etc.
|
||||
- ideal für klassische Webanwendungen mit HTML-Seiten.
|
||||
|
||||
Für APIs nutzt man meist **Django REST Framework (DRF)** als Erweiterung.
|
||||
|
||||
**FastAPI** dagegen ist:
|
||||
|
||||
- stärker auf **APIs** fokussiert,
|
||||
- nicht „alles aus einer Hand“, sondern:
|
||||
- Web-Layer: `Starlette`,
|
||||
- Datenvalidierung: `Pydantic`,
|
||||
- Datenbank: du wählst selbst z. B. SQLAlchemy, Tortoise ORM etc.
|
||||
|
||||
Faustregel:
|
||||
|
||||
- Wenn du eine klassische Website mit HTML-Rendering brauchst → Django.
|
||||
- Wenn du primär eine performant API bauen willst (z. B. für SPA, Microservices) → FastAPI ist sehr attraktiv.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 FastAPI vs. Node.js / Express
|
||||
|
||||
**Node.js + Express** ist eine sehr verbreitete Lösung für APIs in JavaScript/TypeScript.
|
||||
|
||||
- **Sprache:** Node → JavaScript/TypeScript, FastAPI → Python.
|
||||
- **Typen:** TypeScript kann Typen bieten, FastAPI nutzt Python-Typen + [[Pydantic]].
|
||||
- **Ökosystem:** Node sehr stark im Web-/Frontend-nahen Bereich, Python stark bei Data Science, Machine Learning und Backend-Services.
|
||||
|
||||
FastAPI ist besonders interessant, wenn du sowieso Python nutzt (z. B. wegen ML/AI) und dafür eine passende Web-API brauchst.
|
||||
|
||||
---
|
||||
|
||||
## 3. Welche Probleme löst FastAPI?
|
||||
|
||||
### 3.1 Saubere, valide Eingabedaten
|
||||
|
||||
Problem ohne Framework-Unterstützung:
|
||||
|
||||
- Du bekommst z. B. einen JSON-Body und musst:
|
||||
- alle Felder prüfen (Typ, Pflichtfelder, Wertebereiche),
|
||||
- Fehler verständlich zurückgeben,
|
||||
- alles manuell machen.
|
||||
|
||||
FastAPI + [[Pydantic]] lösen das:
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class User(BaseModel):
|
||||
name: str = Field(..., min_length=3)
|
||||
age: int = Field(..., ge=0, le=120) # 0 <= age <= 120
|
||||
|
||||
@app.post("/users")
|
||||
def create_user(user: User):
|
||||
# Wenn name zu kurz oder age negativ ist, liefert FastAPI automatisch 422 mit Fehlerdetails
|
||||
return {"message": "User created", "user": user}
|
||||
```
|
||||
|
||||
Vorteil:
|
||||
|
||||
- Weniger Fehleranfälligkeit.
|
||||
- Konsistente Fehlerantworten.
|
||||
- Gute Developer-Erfahrung.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Automatische Dokumentation und Testbarkeit
|
||||
|
||||
FastAPI erzeugt automatisch eine OpenAPI-Spezifikation und UI:
|
||||
|
||||
- `http://localhost:8000/docs` → Swagger UI (interaktive Oberfläche, du kannst Requests direkt aus dem Browser abschicken).
|
||||
- `http://localhost:8000/redoc` → ReDoc, alternative Dokumentationsansicht.
|
||||
|
||||
Das hilft:
|
||||
|
||||
- Dir selbst beim Testen.
|
||||
- Frontend-Entwicklern oder anderen Teams, die deine API nutzen.
|
||||
- Beim automatisierten Generieren von Client-SDKs (z. B. TypeScript-Client).
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Performance und asynchrones I/O
|
||||
|
||||
Problem:
|
||||
|
||||
- In „klassischen“ synchronen Webframeworks blockiert jeder Request, der z. B. auf eine externe API oder langsame DB wartet.
|
||||
- Bei vielen gleichzeitigen Anfragen leiden Durchsatz und Antwortzeit.
|
||||
|
||||
FastAPI setzt auf **ASGI** (Asynchronous Server Gateway Interface) und `async def`:
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
import httpx # asynchroner HTTP-Client
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/external")
|
||||
async def call_external_api():
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get("https://httpbin.org/get")
|
||||
return response.json()
|
||||
```
|
||||
|
||||
Vorteil:
|
||||
|
||||
- Viele I/O-lastige Requests können parallel abgewickelt werden.
|
||||
- Besonders sinnvoll bei Microservices, die viel mit anderen Services kommunizieren.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Abhängigkeiten und Wiederverwendbarkeit (Dependency Injection)
|
||||
|
||||
FastAPI bietet ein eingebautes **Dependency Injection**-System.
|
||||
Beim Entwickeln von APIs brauchst du häufig:
|
||||
|
||||
- Datenbankverbindungen,
|
||||
- Authentifizierungslogik,
|
||||
- Konfigurationsobjekte.
|
||||
|
||||
Ohne System würdest du das überall wiederholen oder global speichern.
|
||||
Mit FastAPI:
|
||||
|
||||
```python
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
def get_settings():
|
||||
# z. B. Konfiguration laden
|
||||
return {"app_name": "Meine App"}
|
||||
|
||||
@app.get("/info")
|
||||
def read_info(settings = Depends(get_settings)):
|
||||
return {"app_name": settings["app_name"]}
|
||||
```
|
||||
|
||||
Das verbessert:
|
||||
|
||||
- Testbarkeit (du kannst Dependencies im Test austauschen),
|
||||
- Struktur deines Codes (klarere Trennung von Zuständigkeiten).
|
||||
|
||||
---
|
||||
|
||||
## 4. Typische Einsatzszenarien (praxisnah)
|
||||
|
||||
### Beispiel 1: Einfaches CRUD für ein „Item“
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
price: float
|
||||
|
||||
# „Fake-Datenbank“ im Speicher
|
||||
items_db: List[Item] = []
|
||||
|
||||
@app.post("/items", response_model=Item)
|
||||
def create_item(item: Item):
|
||||
# einfache Prüfung: ID darf nicht doppelt sein
|
||||
if any(existing.id == item.id for existing in items_db):
|
||||
raise HTTPException(status_code=400, detail="Item ID already exists")
|
||||
items_db.append(item)
|
||||
return item
|
||||
|
||||
@app.get("/items", response_model=List[Item])
|
||||
def list_items():
|
||||
return items_db
|
||||
|
||||
@app.get("/items/{item_id}", response_model=Item)
|
||||
def get_item(item_id: int):
|
||||
for item in items_db:
|
||||
if item.id == item_id:
|
||||
return item
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
```
|
||||
|
||||
Du bekommst:
|
||||
|
||||
- JSON-APIs für CRUD,
|
||||
- automatische Dokumentation,
|
||||
- automatische Validierung für `Item`.
|
||||
|
||||
---
|
||||
|
||||
### Beispiel 2: Path-Parameter, Query-Parameter, Body
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class SearchFilters(BaseModel):
|
||||
min_price: Optional[float] = None
|
||||
max_price: Optional[float] = None
|
||||
|
||||
@app.get("/products/{category}")
|
||||
def search_products(
|
||||
category: str,
|
||||
q: Optional[str] = None, # Query-Parameter ?q=Text
|
||||
filters: SearchFilters = None # Body JSON
|
||||
):
|
||||
return {
|
||||
"category": category,
|
||||
"query": q,
|
||||
"filters": filters
|
||||
}
|
||||
```
|
||||
|
||||
Beispiel-Request:
|
||||
|
||||
- `GET /products/books?q=python` mit JSON-Body:
|
||||
```json
|
||||
{
|
||||
"min_price": 10,
|
||||
"max_price": 50
|
||||
}
|
||||
```
|
||||
|
||||
FastAPI erkennt:
|
||||
|
||||
- `category` als Pfadparameter,
|
||||
- `q` als Query-Parameter,
|
||||
- `filters` als JSON-Body und validiert ihn.
|
||||
|
||||
---
|
||||
|
||||
### Beispiel 3: Einfache Authentifizierung per Token
|
||||
|
||||
```python
|
||||
from fastapi import Depends, FastAPI, HTTPException, Header
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
def get_current_user(x_token: str = Header(...)):
|
||||
if x_token != "secrettoken123":
|
||||
raise HTTPException(status_code=401, detail="Invalid or missing token")
|
||||
return {"username": "alice"}
|
||||
|
||||
@app.get("/profile")
|
||||
def read_profile(current_user = Depends(get_current_user)):
|
||||
return {"message": f"Hello, {current_user['username']}"}
|
||||
```
|
||||
|
||||
Hier:
|
||||
|
||||
- Der Endpunkt `/profile` verlangt einen HTTP-Header `X-Token`.
|
||||
- FastAPI übernimmt das Zusammenspiel von Header → Dependency → Endpoint-Logik.
|
||||
|
||||
---
|
||||
|
||||
## 5. Herausforderungen und typische Stolpersteine
|
||||
|
||||
FastAPI nimmt dir viel ab, aber es gibt einige Themen, die für Einsteiger Hürden sein können:
|
||||
|
||||
### 5.1 Asynchrones Programmieren (`async` / `await`)
|
||||
|
||||
- Wenn du noch nie mit Async gearbeitet hast, ist es ungewohnt:
|
||||
- Wann nutze ich `async def`?
|
||||
- Wo brauche ich `await`?
|
||||
- Was ist „Blocking I/O“?
|
||||
- Du musst darauf achten, dass du **asynchrone Bibliotheken** verwendest, wenn du im Handler `async` einsetzt
|
||||
(z. B. `httpx` statt `requests`, `asyncpg` statt `psycopg2`).
|
||||
|
||||
Wenn du erst mal einsteigst, kannst du auch erst **synchron** (ohne `async`) starten und später umstellen.
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Typannotationen und Pydantic verstehen
|
||||
|
||||
FastAPI baut stark auf Typen auf:
|
||||
|
||||
- Für jemanden ohne Erfahrung mit Typannotationen in Python ist das anfangs ungewohnt.
|
||||
- Du musst verstehen:
|
||||
- Wie du eigene Modelle mit `BaseModel` definierst.
|
||||
- Wie optionale Felder mit `Optional[...]` und Standardwerten funktionieren.
|
||||
- Wie Validierung und Fehlernachrichten von [[Pydantic]] aussehen.
|
||||
|
||||
Aber:
|
||||
Der Lerneffekt lohnt sich, weil du insgesamt saubereren, stabileren Code bekommst.
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Datenbankintegration
|
||||
|
||||
FastAPI selbst bringt keinen [[ORM]] mit. Du musst wählen:
|
||||
|
||||
- z. B. **[[SQLAlchemy]]**, Tortoise-ORM, Prisma, Gino etc.
|
||||
|
||||
Dabei stellen sich Fragen wie:
|
||||
|
||||
- Wie verwalte ich Datenbank-Sessions pro Request?
|
||||
- Nutze ich die sync- oder async-Variante meiner ORM?
|
||||
- Wie realisiere ich Migrations (alembic, etc.)?
|
||||
|
||||
Es gibt viele Beispielprojekte, aber es ist ein zusätzlicher Schritt im Vergleich zu Django, wo ein [[ORM]] „eingebaut“ ist.
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Deployment / Betrieb
|
||||
|
||||
Für Einsteiger ist der Weg von „läuft lokal“ zu „läuft im Internet“ oft herausfordernd:
|
||||
|
||||
- FastAPI-Anwendung läuft typischerweise mit:
|
||||
- [[uvicorn]] oder [[hypercorn]] (ASGI-Server),
|
||||
- oft hinter einem Reverse Proxy wie [[Nginx]].
|
||||
- Themen:
|
||||
- Logging konfigurieren,
|
||||
- Umgebungsvariablen (Konfiguration),
|
||||
- HTTPS/SSL (z. B. via [[Nginx]]/Let’s Encrypt),
|
||||
- Skalierung (mehrere Worker, z. B. `gunicorn` + `uvicorn.workers.UvicornWorker`).
|
||||
|
||||
Für den Anfang kannst du auch auf Plattformen wie Render, Railway, fly.io, oder Docker + Cloud setzen.
|
||||
|
||||
---
|
||||
|
||||
### 5.5 Versionierung und Wartung großer Projekte
|
||||
|
||||
Bei größeren APIs:
|
||||
|
||||
- Wie strukturiere ich meinen Code?
|
||||
- z. B. mit **Routern** (`APIRouter`) und Modulen.
|
||||
- Wie versioniere ich die API? (`/v1/user`, `/v2/user`, …)
|
||||
- Wie halte ich die Dokumentation aktuell?
|
||||
- FastAPI hilft zwar, aber bei vielen Endpunkten braucht man Konventionen und ggf. zusätzliche Dokumentation.
|
||||
|
||||
Beispiel mit Router:
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, APIRouter
|
||||
|
||||
app = FastAPI()
|
||||
items_router = APIRouter(prefix="/items", tags=["items"])
|
||||
|
||||
@items_router.get("/")
|
||||
def list_items():
|
||||
return [{"id": 1, "name": "Item 1"}]
|
||||
|
||||
app.include_router(items_router)
|
||||
```
|
||||
|
||||
So kannst du größere Projekte modular strukturieren.
|
||||
|
||||
---
|
||||
|
||||
## 6. Zusammenfassung
|
||||
|
||||
- **FastAPI** ist ein modernes Framework für **Web-APIs in Python**, fokussiert auf:
|
||||
- hohe **Performance** (async),
|
||||
- **Typen** + automatische Validierung ([[Pydantic]]),
|
||||
- automatische **OpenAPI-/Swagger-Dokumentation**,
|
||||
- gute Developer Experience.
|
||||
|
||||
- Es unterscheidet sich von:
|
||||
- **Flask**: moderner, stärker typisiert, async-first, integrierte Validierung & Doku.
|
||||
- **Django**: kein Full-Stack-Framework, sondern eher API-fokussiert; du kombinierst es mit eigenen Tools für DB, Templates etc.
|
||||
- Node/Express: andere Sprache, andere Ökosysteme; FastAPI besonders stark, wenn du ohnehin Python nutzt.
|
||||
|
||||
- Es löst typische Probleme beim API-Bau:
|
||||
- Validierung von Eingaben,
|
||||
- Dokumentation & Testbarkeit,
|
||||
- Performance bei vielen gleichzeitigen Anfragen,
|
||||
- saubere Struktur durch Dependency Injection.
|
||||
|
||||
- Herausforderungen:
|
||||
- Einstieg in asynchrones Programmieren,
|
||||
- Verständnis von Typannotationen & [[Pydantic]],
|
||||
- separate Auswahl & Integration einer Datenbanklösung,
|
||||
- Deployment & Betrieb.
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
Pydantic ist ein zentrales Werkzeug im heutigen Python-Ökosystem, vor allem im Umfeld von APIs (z.B. [[FastAPI]]), Konfiguration und Datenvalidierung.
|
||||
Im Folgenden bekommst du eine systematische Einführung mit praxisnahen Beispielen.
|
||||
|
||||
**Hinweis:** Die Beispiele orientieren sich an **Pydantic v2** (aktuelle Hauptversion). In v1 ist die Syntax ähnlich, aber es gibt einige Unterschiede (z.B. `@validator` vs. `@field_validator`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Grundsätzliche Definition: Was ist Pydantic?
|
||||
|
||||
**Kurz:**
|
||||
Pydantic ist eine Bibliothek für **Datenmodelle mit Validierung und Parsing** auf Basis von **Python-Typannotationen**.
|
||||
|
||||
Du beschreibst deine Datenstruktur wie bei einer Klasse mit Typen:
|
||||
- Pydantic prüft zur Laufzeit, ob eingehende Daten diese Struktur erfüllen.
|
||||
- Es konvertiert (parst) Werte soweit wie möglich in die gewünschten Typen.
|
||||
- Es gibt strukturierte Fehlermeldungen aus, wenn etwas nicht passt.
|
||||
- Es kann aus deinen Modellen u.a. **JSON-Schemas** generieren.
|
||||
|
||||
Beispiel – ein einfaches Datenmodell:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from typing import List
|
||||
|
||||
class User(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
tags: List[str] = []
|
||||
|
||||
# Daten aus einer externen Quelle (z.B. JSON)
|
||||
payload = {
|
||||
"id": "123", # wird zu int konvertiert
|
||||
"name": "Alice",
|
||||
"tags": ["admin", "beta"]
|
||||
}
|
||||
|
||||
user = User(**payload)
|
||||
print(user)
|
||||
print(user.id, type(user.id))
|
||||
|
||||
# Fehlvalidierung
|
||||
try:
|
||||
User(id="abc", name=123)
|
||||
except ValidationError as e:
|
||||
print(e.errors())
|
||||
```
|
||||
|
||||
Wichtige Punkte:
|
||||
- `id` ist als `int` deklariert, ein String `"123"` wird automatisch konvertiert.
|
||||
- Wenn Konvertierung scheitert (z.B. `"abc"` → `int`), erzeugt Pydantic eine **ValidationError** mit detailierten Fehlerinfos.
|
||||
|
||||
---
|
||||
|
||||
## 2. Grundkonzepte von Pydantic (v2)
|
||||
|
||||
### 2.1 BaseModel und Felder
|
||||
|
||||
Alle Modelle erben typically von `BaseModel`:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
class Product(BaseModel):
|
||||
id: int
|
||||
name: str = Field(..., min_length=3, description="Produktname")
|
||||
price: float = Field(ge=0)
|
||||
description: Optional[str] = None
|
||||
```
|
||||
|
||||
- `Field(...)` bedeutet „Pflichtfeld“ mit zusätzlichen Metadaten/Constraints.
|
||||
- `ge=0` = „greater or equal 0“.
|
||||
- `Optional[str] = None` = optionales Feld, default `None`.
|
||||
|
||||
### 2.2 Validierung & Parsing
|
||||
|
||||
Pydantic führt **Validierung und Parsing beim Erstellen** des Modells durch.
|
||||
Man kann explizit „parsen“:
|
||||
|
||||
```python
|
||||
from pydantic import TypeAdapter
|
||||
from typing import List
|
||||
|
||||
# Einzelnes Modell: meistens direkt Model(**data)
|
||||
product = Product(id="1", name="TV", price="999.90")
|
||||
|
||||
# Sammlung von Modellen validieren:
|
||||
ta = TypeAdapter(List[Product])
|
||||
|
||||
data = [
|
||||
{"id": 1, "name": "TV", "price": 999.90},
|
||||
{"id": "2", "name": "Laptop", "price": "1299.50"},
|
||||
]
|
||||
|
||||
products = ta.validate_python(data)
|
||||
print(products)
|
||||
```
|
||||
|
||||
`TypeAdapter` in v2 ersetzt viele frühere `parse_obj_as`-Usecases.
|
||||
|
||||
### 2.3 Serialisierung
|
||||
|
||||
Pydantic-Modelle lassen sich leicht in z.B. JSON-kompatible Strukturen umwandeln:
|
||||
|
||||
```python
|
||||
product = Product(id=1, name="TV", price=999.90)
|
||||
print(product.model_dump()) # dict
|
||||
print(product.model_dump_json()) # JSON-String
|
||||
```
|
||||
|
||||
Man kann steuern:
|
||||
- welche Felder inkludiert/exkludiert werden,
|
||||
- wie verschachtelte Modelle serialisiert werden,
|
||||
- ob Alias-Namen verwendet werden sollen etc.
|
||||
|
||||
---
|
||||
|
||||
## 3. Abgrenzung zu verwandten Konzepten / Bibliotheken
|
||||
|
||||
### 3.1 Pydantic vs. `dataclasses`
|
||||
|
||||
Python `dataclasses`:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class UserDC:
|
||||
id: int
|
||||
name: str
|
||||
```
|
||||
|
||||
- `dataclasses` stellen nur **strukturelle Container** bereit.
|
||||
- Keine automatische Validierung oder Typkonvertierung.
|
||||
- Typannotationen sind rein informativ (für IDE, mypy), nicht enforced.
|
||||
|
||||
Pydantic:
|
||||
|
||||
```python
|
||||
class UserModel(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
```
|
||||
|
||||
- Führt **Validierung & Parsing** beim Erstellen durch.
|
||||
- Gibt strukturierte Fehler aus.
|
||||
- Generiert optional JSON-Schemas.
|
||||
- Basiert auch auf Typannotationen, aber **wertet sie zur Laufzeit aus**.
|
||||
|
||||
Kurz:
|
||||
- `dataclasses`: leichtgewichtige Container.
|
||||
- Pydantic: Container + Validierung + Parsing + Schema.
|
||||
|
||||
### 3.2 Pydantic vs. Marshmallow / Cerberus u.ä.
|
||||
|
||||
- **Marshmallow** ist ebenfalls eine Validierungs-/Serialisierungsbibliothek.
|
||||
- Du definierst Schemas explizit über Felder (z.B. `fields.Int()`) statt über Typannotationen.
|
||||
- Skill: starke Serialisierung/Deserialisierung, aber andere API.
|
||||
|
||||
- **Pydantic**:
|
||||
- Nutzt standardmäßige Python-Typannotationen (nativer für moderne Python-Code).
|
||||
- Sehr eng mit Typing-Ökosystem (mypy, IDEs).
|
||||
- Performance-fokussiert, in v2 mit `pydantic-core` in Rust.
|
||||
|
||||
### 3.3 Pydantic vs. Typing-Features (`TypedDict`, `Protocol`, …)
|
||||
|
||||
- `TypedDict` definiert nur statische Typinformationen für Dictionaries.
|
||||
- Pydantic-Modelle sind **richtige Klassen** mit Methoden, Validierung und Verhalten.
|
||||
|
||||
### 3.4 Pydantic vs. ORMs (z.B. Django Models, SQLAlchemy Models)
|
||||
|
||||
- [[ORM]]-Modelle repräsentieren **Datenbanktabellen** und kümmern sich um **Persistenz** (CRUD, Queries).
|
||||
- Pydantic-Modelle sind **reine Daten- und Validierungsmodelle**, ohne DB-Anbindung.
|
||||
|
||||
In der Praxis:
|
||||
- Du kannst Pydantic-Modelle nutzen, um **Requests/Responses** zu validieren und zu dokumentieren.
|
||||
- ORMs nutzen, um die Daten in der Datenbank zu speichern.
|
||||
|
||||
[[FastAPI]] macht genau das:
|
||||
- Pydantic-Modelle für Request/Response,
|
||||
- SQLAlchemy/SQLModel/etc. für DB.
|
||||
|
||||
---
|
||||
|
||||
## 4. Welche Probleme löst Pydantic?
|
||||
|
||||
### 4.1 Validierung externer Daten (APIs, Formulare, Message Queues)
|
||||
|
||||
Externe Daten sind oft:
|
||||
- unvollständig,
|
||||
- im falschen Typ,
|
||||
- fehlerhaft strukturiert.
|
||||
|
||||
Pydantic sorgt für:
|
||||
- Zentral definierte Datenstruktur.
|
||||
- Automatische Validierung bei jedem Eingang.
|
||||
- Konvertierung (z.B. `"123"` → `int`, `"2024-01-01"` → `datetime`).
|
||||
|
||||
Beispiel: Request-Daten einer (pseudo) API:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
from typing import List
|
||||
|
||||
class Article(BaseModel):
|
||||
title: str
|
||||
url: HttpUrl
|
||||
tags: List[str] = []
|
||||
|
||||
payload = {
|
||||
"title": "Pydantic Einführung",
|
||||
"url": "https://example.com/pydantic",
|
||||
"tags": ["python", "validation"]
|
||||
}
|
||||
|
||||
article = Article(**payload)
|
||||
print(article)
|
||||
```
|
||||
|
||||
Wenn `url` kein gültiger URL-String ist, kommt eine strukturierte Fehlermeldung.
|
||||
|
||||
### 4.2 Konfiguration und Umgebungsvariablen
|
||||
|
||||
Pydantic kann Konfiguration aus:
|
||||
- Umgebungsvariablen,
|
||||
- `.env`-Dateien,
|
||||
- kwargs,
|
||||
- etc.
|
||||
laden und validieren.
|
||||
|
||||
In v2 nutzt man `pydantic-settings`:
|
||||
|
||||
```python
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class AppSettings(BaseSettings):
|
||||
debug: bool = False
|
||||
database_url: str
|
||||
port: int = 8000
|
||||
|
||||
model_config = {
|
||||
"env_file": ".env",
|
||||
"env_prefix": "APP_",
|
||||
}
|
||||
|
||||
settings = AppSettings()
|
||||
print(settings.database_url, settings.debug)
|
||||
```
|
||||
|
||||
- `APP_DATABASE_URL` in der Umgebung oder `.env` wird gelesen.
|
||||
- Falsche Typen werden validiert/konvertiert (z.B. `"true"` → `bool`).
|
||||
- Fehlende Pflichtwerte (z.B. `database_url`) führen zu Fehlern.
|
||||
|
||||
### 4.3 Saubere Domain-Modelle und Business-Logik
|
||||
|
||||
Du kannst Pydantic-Modelle verwenden, um deine Domain-Objekte zu modellieren, inklusive:
|
||||
- Validierung von Invarianten (z.B. Preis > 0, Datum in der Zukunft/ Vergangenheit),
|
||||
- Standardwerte,
|
||||
- abgeleitete Felder.
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, field_validator
|
||||
from datetime import datetime
|
||||
|
||||
class Event(BaseModel):
|
||||
name: str
|
||||
start: datetime
|
||||
end: datetime
|
||||
|
||||
@field_validator("end")
|
||||
def end_must_be_after_start(cls, v, info):
|
||||
start = info.data.get("start")
|
||||
if start and v <= start:
|
||||
raise ValueError("end must be after start")
|
||||
return v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Herausforderungen & Stolpersteine
|
||||
|
||||
### 5.1 Performance und Overhead
|
||||
|
||||
- Pydantic führt bei **jedem Instanziieren** eines Modells Validierung/Parsing durch.
|
||||
- Bei sehr großen Datenmengen oder sehr häufigen Instanziierungen kann das Performance kosten.
|
||||
- *Lösung*: gezielt einsetzen, ggf. `model_validate` mit `from_attributes=True` o.Ä., Caching, oder an bestimmten Stellen auf „raw“ Datenstrukturen ausweichen.
|
||||
|
||||
### 5.2 Lax vs. Strict Typen
|
||||
|
||||
Standardmäßig ist Pydantic recht **„freundlich“**:
|
||||
- `"123"` → `int(123)`
|
||||
- `"true"` → `bool(True)` (bei Settings)
|
||||
- `"1.23"` → `float(1.23)`
|
||||
|
||||
Das ist praktisch, kann aber auch unerwartete Effekte haben.
|
||||
|
||||
Du kannst **strict**-Typen verwenden oder striktere Konfiguration:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, StrictInt
|
||||
|
||||
class Model(BaseModel):
|
||||
value: StrictInt
|
||||
|
||||
# Model(value="1") -> ValidationError (keine Autokonvertierung)
|
||||
```
|
||||
|
||||
Oder über `model_config`:
|
||||
|
||||
```python
|
||||
class Model(BaseModel):
|
||||
value: int
|
||||
|
||||
model_config = {
|
||||
"strict": True,
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Umgang mit Optional, Defaults, Required
|
||||
|
||||
Typische Stolperfallen:
|
||||
|
||||
```python
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class Example(BaseModel):
|
||||
a: int # Pflichtfeld
|
||||
b: Optional[int] # „darf None sein“, aber kein Default → ebenfalls Pflichtfeld
|
||||
c: int = 0 # optional, default = 0
|
||||
d: Optional[int] = None # optional, default = None
|
||||
e: int = Field(..., description="explizit required") # Pflichtfeld
|
||||
```
|
||||
|
||||
- `Optional[int]` heißt nur „`int` oder `None`“, nicht automatisch optional im Sinne von „nicht im Input vorhanden“.
|
||||
- „Required“ bedeutet: Feld muss im Input vorhanden sein, außer es gibt einen Default.
|
||||
|
||||
### 5.4 Migration v1 → v2
|
||||
|
||||
Wenn du Codebeispiele im Netz findest, sind viele noch Pydantic v1:
|
||||
- `@validator` wurde größtenteils zu `@field_validator`.
|
||||
- `parse_obj_as` → `TypeAdapter`.
|
||||
- `Config`-Inner-Class → `model_config` oder `ConfigDict`.
|
||||
|
||||
Beim Einstieg: gleich v2-Doku lesen und wählen.
|
||||
|
||||
### 5.5 Komplexe verschachtelte Strukturen
|
||||
|
||||
Pydantic kann sehr komplexe Strukturen validieren (verschachtelte Modelle, Union-Typen, generische Modelle).
|
||||
Herausforderung ist eher das **Verständnis** der Typen und Validierungsreihenfolge.
|
||||
|
||||
---
|
||||
|
||||
## 6. Praxisnahe Beispiele
|
||||
|
||||
### 6.1 Verschachtelte Modelle
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
|
||||
class Address(BaseModel):
|
||||
street: str
|
||||
city: str
|
||||
zip_code: str
|
||||
|
||||
class Customer(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
addresses: List[Address]
|
||||
|
||||
data = {
|
||||
"id": "1",
|
||||
"name": "Bob",
|
||||
"addresses": [
|
||||
{"street": "Main St 1", "city": "Berlin", "zip_code": "10115"},
|
||||
{"street": "Side St 2", "city": "Hamburg", "zip_code": "20095"},
|
||||
]
|
||||
}
|
||||
|
||||
customer = Customer(**data)
|
||||
print(customer)
|
||||
```
|
||||
|
||||
Fehler in einer Adresse werden detailliert auf der jeweiligen „Pfad“-Ebene ausgegeben.
|
||||
|
||||
### 6.2 Feld-Constraints & Metadaten
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Literal
|
||||
|
||||
class Order(BaseModel):
|
||||
id: int
|
||||
status: Literal["open", "paid", "shipped"]
|
||||
quantity: int = Field(gt=0, description="Muss > 0 sein")
|
||||
customer_email: str = Field(pattern=r"[^@]+@[^@]+\.[^@]+")
|
||||
|
||||
order = Order(
|
||||
id=1,
|
||||
status="open",
|
||||
quantity=5,
|
||||
customer_email="test@example.com"
|
||||
)
|
||||
```
|
||||
|
||||
- `Literal` beschränkt mögliche Werte (Enum-artig).
|
||||
- `pattern` (Regex) validiert z.B. einfache E-Mail-Formate.
|
||||
|
||||
### 6.3 Custom Validierung mit `field_validator` und `model_validator`
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, field_validator, model_validator
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
password_repeat: str
|
||||
|
||||
@field_validator("username")
|
||||
def username_not_empty(cls, v):
|
||||
if not v.strip():
|
||||
raise ValueError("username must not be empty")
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def passwords_match(self):
|
||||
if self.password != self.password_repeat:
|
||||
raise ValueError("passwords do not match")
|
||||
return self
|
||||
```
|
||||
|
||||
- `field_validator` prüft einzelne Felder.
|
||||
- `model_validator` (v2) hat Zugriff auf das ganze Modell (z.B. um zwei Felder zu vergleichen).
|
||||
|
||||
### 6.4 JSON-Schema / OpenAPI-Integration
|
||||
|
||||
Pydantic kann JSON-Schemas erzeugen, die u.a. von [[FastAPI]] genutzt werden, um automatisch Doku (OpenAPI/Swagger) zu generieren:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
print(Item.model_json_schema())
|
||||
```
|
||||
|
||||
Das ausgegebene Schema beschreibt die Struktur, Typen und Constraints – ideal für API-Dokumentation.
|
||||
|
||||
---
|
||||
|
||||
## 7. Zusammenfassung
|
||||
|
||||
- **Definition:** Pydantic ist eine Python-Bibliothek für **Datenmodelle mit Validierung, Parsing und Serialisierung** auf Basis von Typannotationen.
|
||||
- **Abgrenzung:**
|
||||
- Mehr als `dataclasses` (mit Validierung & Parsing).
|
||||
- Nutzt Python-Typing natürlicher als Marshmallow & Co.
|
||||
- Kein ORM, sondern ergänzt diese (oft für API-Schicht).
|
||||
- **Probleme, die gelöst werden:**
|
||||
- Validierung externer Daten (APIs, Config, User-Input).
|
||||
- Typ-sichere Domain-Modelle.
|
||||
- Konfiguration aus Umgebungsvariablen/Dateien inkl. Typenprüfung.
|
||||
- Automatische Generierung von JSON-Schemas (z.B. für APIs).
|
||||
- **Herausforderungen:**
|
||||
- Performance bei massiver Nutzung.
|
||||
- Verständnis von strict vs. lax Typen.
|
||||
- Stolperfallen bei Optional/Defaults.
|
||||
- Versionsunterschiede (v1 vs. v2).
|
||||
+1170
File diff suppressed because it is too large
Load Diff
+368
@@ -0,0 +1,368 @@
|
||||
## 1. Grundidee von `argparse`
|
||||
|
||||
`argparse` ist das Standardmodul in Python, um Kommandozeilen-Argumente zu definieren, zu parsen und automatisch Hilfe-/Usage-Texte zu erzeugen.
|
||||
|
||||
Minimalbeispiel:
|
||||
|
||||
```python
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Ein kleines Beispiel-CLI")
|
||||
parser.add_argument("datei", help="Pfad zur Eingabedatei")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(args.datei)
|
||||
```
|
||||
|
||||
Aufruf:
|
||||
```bash
|
||||
python script.py meine_datei.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Argumente definieren: `ArgumentParser.add_argument`
|
||||
|
||||
### 2.1 Positionsargumente
|
||||
|
||||
- Werden ohne führende `-` oder `--` angegeben.
|
||||
- Reihenfolge ist relevant.
|
||||
|
||||
```python
|
||||
parser.add_argument("quelle", help="Quellpfad")
|
||||
parser.add_argument("ziel", help="Zielpfad")
|
||||
```
|
||||
|
||||
Aufruf:
|
||||
```bash
|
||||
python script.py input.txt output.txt
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Pflichtwerte, die immer gebraucht werden (z.B. Eingabe- und Ausgabedatei).
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Optionale Argumente (Flags / Optionen)
|
||||
|
||||
- Beginnen mit `-` bzw. `--`.
|
||||
- Reihenfolge ist egal.
|
||||
- Können Standardwerte haben.
|
||||
|
||||
```python
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="store_true",
|
||||
help="Ausführliche Ausgabe aktivieren"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-n", "--anzahl",
|
||||
type=int,
|
||||
default=10,
|
||||
help="Anzahl der Elemente (Standard: 10)"
|
||||
)
|
||||
```
|
||||
|
||||
Aufruf:
|
||||
```bash
|
||||
python script.py input.txt --verbose --anzahl 5
|
||||
# oder kurz:
|
||||
python script.py input.txt -v -n 5
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Konfiguration, optionales Verhalten, Debug/Verbose-Flags, Parameter mit Default.
|
||||
|
||||
---
|
||||
|
||||
## 3. Wichtige Parameter von `add_argument`
|
||||
|
||||
### 3.1 `name` / `flags`
|
||||
|
||||
- Beispiel:
|
||||
- Positional: `"datei"`
|
||||
- Optional: `"-v", "--verbose"`
|
||||
|
||||
```python
|
||||
parser.add_argument("datei")
|
||||
parser.add_argument("-v", "--verbose")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 `type`
|
||||
|
||||
- Convertiert Eingabe in Typ.
|
||||
- Validiert automatisch (bei falschem Typ Fehler + Hilfe).
|
||||
|
||||
```python
|
||||
parser.add_argument("--port", type=int, default=8080)
|
||||
parser.add_argument("--faktor", type=float)
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Numerische Werte, Pfade, eigene Typen (z.B. `Path` aus `pathlib`).
|
||||
|
||||
---
|
||||
|
||||
### 3.3 `default`
|
||||
|
||||
- Standardwert, wenn Argument nicht übergeben wird.
|
||||
|
||||
```python
|
||||
parser.add_argument("--log-level", default="INFO")
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Sinnvolle Defaults, um CLI kompakt zu halten.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 `required`
|
||||
|
||||
- Macht optionale Argumente zwingend erforderlich.
|
||||
|
||||
```python
|
||||
parser.add_argument("--config", required=True)
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Flags/Optionen, die zwingend gesetzt werden müssen (z.B. API-Key, Konfigdatei).
|
||||
|
||||
---
|
||||
|
||||
### 3.5 `help`
|
||||
|
||||
- Beschreibung für die `--help`-Ausgabe.
|
||||
|
||||
```python
|
||||
parser.add_argument("--mode", help="Betriebsmodus: fast oder safe")
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Dokumentation der Optionen (sehr wichtig für Benutzerfreundlichkeit).
|
||||
|
||||
---
|
||||
|
||||
### 3.6 `choices`
|
||||
|
||||
- Schränkt erlaubte Werte ein.
|
||||
|
||||
```python
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["fast", "safe"],
|
||||
default="safe",
|
||||
help="fast = schneller, safe = sicherer"
|
||||
)
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Enum-ähnliche Optionen (z.B. `debug/info/warn/error`, `json/text`).
|
||||
|
||||
---
|
||||
|
||||
### 3.7 `action`
|
||||
|
||||
Steuert, was passiert, wenn das Argument gesetzt wird.
|
||||
|
||||
Häufige Actions:
|
||||
|
||||
1. `store` (Standard)
|
||||
Speichert den Wert (z.B. `--port 8000` → `args.port = 8000`).
|
||||
|
||||
2. `store_true` / `store_false`
|
||||
Boolean-Flag, das `True`/`False` setzt.
|
||||
|
||||
```python
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
```
|
||||
|
||||
3. `append`
|
||||
Fügt mehrere Werte in eine Liste ein.
|
||||
|
||||
```python
|
||||
parser.add_argument(
|
||||
"-t", "--tag",
|
||||
action="append",
|
||||
help="Kann mehrfach verwendet werden"
|
||||
)
|
||||
# Aufruf: --tag a --tag b -> args.tag = ["a", "b"]
|
||||
```
|
||||
|
||||
4. `count`
|
||||
Zählt, wie oft ein Flag verwendet wurde.
|
||||
|
||||
```python
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="count",
|
||||
default=0,
|
||||
help="Mehrfach verwenden für mehr Details"
|
||||
)
|
||||
# -v -> 1, -vv -> 2 ...
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Flags (bool), Mehrfachangaben (Listen), Verbosity-Level etc.
|
||||
|
||||
---
|
||||
|
||||
### 3.8 `nargs`
|
||||
|
||||
Gibt an, wie viele Werte zu einem Argument gehören.
|
||||
|
||||
Typische Varianten:
|
||||
|
||||
- `nargs=1` → eine Liste mit einem Element
|
||||
- `nargs=2` → genau 2 Werte
|
||||
- `nargs="+"` → mindestens ein Wert
|
||||
- `nargs="*"` → beliebig viele (auch 0)
|
||||
|
||||
```python
|
||||
parser.add_argument("dateien", nargs="+", help="Eine oder mehrere Dateien")
|
||||
parser.add_argument("--koordinaten", nargs=2, type=float, help="x y")
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Mehrere Dateien, Koordinaten, Listen von Werten.
|
||||
|
||||
---
|
||||
|
||||
### 3.9 `metavar`
|
||||
|
||||
- Steuert, wie das Argument im Help-Text angezeigt wird.
|
||||
|
||||
```python
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
metavar="DATEI",
|
||||
help="Ausgabedatei"
|
||||
)
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Schöner formatierte Hilfe (statt generischer Namen).
|
||||
|
||||
---
|
||||
|
||||
### 3.10 `dest`
|
||||
|
||||
- Name des Attributes in `args`.
|
||||
|
||||
```python
|
||||
parser.add_argument("-o", "--output", dest="ausgabedatei")
|
||||
# args.ausgabedatei
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Lesbare/konfliktfreie Python-Bezeichner, wenn CLI-Namen nicht ideal sind.
|
||||
|
||||
---
|
||||
|
||||
## 4. Subkommandos: `subparsers`
|
||||
|
||||
Für CLI-Tools mit mehreren Befehlen (ähnlich `git commit`, `git status`).
|
||||
|
||||
```python
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(prog="tool")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# Subkommando: "run"
|
||||
run_parser = subparsers.add_parser("run", help="Job ausführen")
|
||||
run_parser.add_argument("job_id", type=int)
|
||||
|
||||
# Subkommando: "list"
|
||||
list_parser = subparsers.add_parser("list", help="Jobs auflisten")
|
||||
list_parser.add_argument("--status", choices=["open", "done"])
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "run":
|
||||
print(f"Starte Job {args.job_id}")
|
||||
elif args.command == "list":
|
||||
print(f"Liste Jobs mit Status {args.status}")
|
||||
```
|
||||
|
||||
Aufrufe:
|
||||
```bash
|
||||
tool run 42
|
||||
tool list --status open
|
||||
```
|
||||
|
||||
Use-Case:
|
||||
- Umfangreiche Tools mit verschiedenen Befehlen (z.B. Admin-Tools, Deployment-CLI).
|
||||
|
||||
---
|
||||
|
||||
## 5. Automatische Hilfe und Usage
|
||||
|
||||
`argparse` erzeugt automatisch `-h` / `--help`:
|
||||
|
||||
```bash
|
||||
python script.py --help
|
||||
```
|
||||
|
||||
Du bekommst:
|
||||
|
||||
- Beschreibung (`description`)
|
||||
- Liste aller Argumente
|
||||
- Default-Werte (wenn konfiguriert)
|
||||
- Subkommandos (falls vorhanden)
|
||||
|
||||
Beispiel:
|
||||
|
||||
```python
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Konvertiert Dateien in andere Formate."
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Minimaler „Best-Practice“-Skeleton
|
||||
|
||||
```python
|
||||
import argparse
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Beispiel-Tool für argparse"
|
||||
)
|
||||
|
||||
# Positionsargumente
|
||||
parser.add_argument("eingabe", help="Eingabedatei")
|
||||
|
||||
# Optionale Argumente
|
||||
parser.add_argument(
|
||||
"-o", "--output",
|
||||
help="Ausgabedatei (Standard: stdout)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="store_true",
|
||||
help="Ausführliche Ausgabe"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["fast", "safe"],
|
||||
default="safe",
|
||||
help="Verarbeitungsmodus (Standard: safe)"
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
if args.verbose:
|
||||
print(f"Starte in Modus {args.mode} mit Eingabe {args.eingabe}")
|
||||
# weitere Logik…
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
---
|
||||
+525
@@ -0,0 +1,525 @@
|
||||
Hier eine umfassende, aber für Einsteiger verständliche Einführung in **black**, den „opinionated“ Python-Code-Formatter.
|
||||
|
||||
---
|
||||
|
||||
## 1. Grundidee von *black*
|
||||
|
||||
**Was ist black?**
|
||||
|
||||
- *black* ist ein **automatisches Formatierungs-Tool** für Python.
|
||||
- Es ändert **nur die Formatierung**, nicht die Logik deines Codes.
|
||||
- Es ist **„opinionated“**: Es gibt nur sehr wenige Einstellungen – black entscheidet den Stil für dich.
|
||||
|
||||
**Zentrale Idee:**
|
||||
|
||||
> „*You are not your code style.*“
|
||||
> Statt darüber zu diskutieren, ob ein Leerzeichen hier oder dort besser ist, überlässt du das black.
|
||||
>
|
||||
> Ziel:
|
||||
> - einheitlicher Stil
|
||||
> - weniger Diskussionen in Code Reviews
|
||||
> - Fokus auf inhaltliche Fehler, nicht auf Formatierung
|
||||
|
||||
---
|
||||
|
||||
## 2. Was macht black konkret?
|
||||
|
||||
Black nimmt deinen Python-Code, parst ihn und schreibt ihn nach festen Regeln neu. Beispiele:
|
||||
|
||||
### 2.1 Installation
|
||||
|
||||
```bash
|
||||
pip install black
|
||||
```
|
||||
|
||||
### 2.2 Einfache Nutzung
|
||||
|
||||
Eine einzelne Datei formatieren:
|
||||
|
||||
```bash
|
||||
black main.py
|
||||
```
|
||||
|
||||
Ein ganzes Projekt:
|
||||
|
||||
```bash
|
||||
black .
|
||||
```
|
||||
|
||||
Nur anzeigen, was geändert würde (ohne zu schreiben):
|
||||
|
||||
```bash
|
||||
black --diff --check .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Praxisnahe Formatierungsbeispiele
|
||||
|
||||
### 3.1 Zeilenumbrüche und Einrückung
|
||||
|
||||
**Vorher:**
|
||||
|
||||
```python
|
||||
def very_long_function_name(arg1,arg2,arg3,arg4,arg5,arg6,arg7=False,arg8=None):
|
||||
return (arg1+arg2+arg3+arg4+arg5+arg6)
|
||||
```
|
||||
|
||||
**Nachher (black):**
|
||||
|
||||
```python
|
||||
def very_long_function_name(
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
arg4,
|
||||
arg5,
|
||||
arg6,
|
||||
arg7=False,
|
||||
arg8=None,
|
||||
):
|
||||
return arg1 + arg2 + arg3 + arg4 + arg5 + arg6
|
||||
```
|
||||
|
||||
Was passiert?
|
||||
|
||||
- Argumente werden sauber untereinander geschrieben.
|
||||
- Operatoren (+) werden mit Leerzeichen versehen.
|
||||
- Abschluss-Komma nach dem letzten Argument (hilft bei späteren Änderungen).
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Strings und Anführungszeichen
|
||||
|
||||
Black bevorzugt fast immer **doppelte Anführungszeichen**.
|
||||
|
||||
**Vorher:**
|
||||
|
||||
```python
|
||||
name = 'Alice'
|
||||
message = 'Hello, ' + name + '!'
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
|
||||
```python
|
||||
name = "Alice"
|
||||
message = "Hello, " + name + "!"
|
||||
```
|
||||
|
||||
Ausnahmen:
|
||||
- Wenn ein String doppelte Anführungszeichen enthält, kann black einzelne beibehalten, um weniger zu escapen.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Leerzeichen und Klammern
|
||||
|
||||
**Vorher:**
|
||||
|
||||
```python
|
||||
result=(1+2)*3
|
||||
if x==42:
|
||||
print( 'Answer',x )
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
|
||||
```python
|
||||
result = (1 + 2) * 3
|
||||
if x == 42:
|
||||
print("Answer", x)
|
||||
```
|
||||
|
||||
Black:
|
||||
- fügt Leerzeichen um Operatoren hinzu (`1 + 2`, `x == 42`),
|
||||
- entfernt unnötige Leerzeichen (`print( 'Answer',x )` → `print("Answer", x)`).
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Collections (Listen, Dicts, Sets)
|
||||
|
||||
**Vorher:**
|
||||
|
||||
```python
|
||||
config = {"host":"localhost","port":5432,"debug":True}
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
|
||||
```python
|
||||
config = {
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
"debug": True,
|
||||
}
|
||||
```
|
||||
|
||||
Vorteile:
|
||||
- Bessere Lesbarkeit
|
||||
- Leicht, neue Einträge hinzuzufügen (wegen Abschluss-Komma).
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Lange Ausdrücke
|
||||
|
||||
**Vorher:**
|
||||
|
||||
```python
|
||||
query = session.query(User).filter(User.is_active==True, User.signup_date>=start_date, User.signup_date<=end_date).order_by(User.signup_date.desc())
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
|
||||
```python
|
||||
query = (
|
||||
session.query(User)
|
||||
.filter(
|
||||
User.is_active == True,
|
||||
User.signup_date >= start_date,
|
||||
User.signup_date <= end_date,
|
||||
)
|
||||
.order_by(User.signup_date.desc())
|
||||
)
|
||||
```
|
||||
|
||||
Black bricht lange Zeilen so um, dass:
|
||||
- sie unter der vorgegebenen Maximalbreite bleiben (standard: 88 Zeichen),
|
||||
- die Struktur des Codes klarer sichtbar wird.
|
||||
|
||||
---
|
||||
|
||||
## 4. Abgrenzung zu verwandten Tools
|
||||
|
||||
### 4.1 Formatter vs. Linter vs. Typprüfer
|
||||
|
||||
- **Formatter** (Formatierer):
|
||||
Passen das **Aussehen** deines Codes an – z. B. black, autopep8, yapf.
|
||||
- **Linter**:
|
||||
Finden mögliche **Fehler, Stilprobleme oder unschöne Konstrukte** – z. B. flake8, pylint, ruff.
|
||||
- **Typprüfer**:
|
||||
Prüfen, ob Typen konsistent sind (z. B. mit `typing`) – z. B. mypy, pyright.
|
||||
|
||||
Black ist **nur** ein Formatter.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Black vs. autopep8
|
||||
|
||||
**autopep8**:
|
||||
|
||||
- Ziel: Code so anpassen, dass er PEP8-konform ist.
|
||||
- Orientierung direkt an den PEP8-Regeln.
|
||||
- Viele Optionen (z. B. bestimmte Checks an- oder abschalten).
|
||||
|
||||
**black**:
|
||||
|
||||
- Ziel: **konsequenter, einheitlicher Stil** – nicht nur PEP8, sondern zusätzliche strenge Regeln.
|
||||
- Sehr wenige Konfigurationsmöglichkeiten (bewusst!).
|
||||
- Output ist oft deutlich „strenger” und einheitlicher als autopep8.
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Black vs. yapf
|
||||
|
||||
**yapf**:
|
||||
|
||||
- Google-Tool zur Formatierung von Python.
|
||||
- Sehr konfigurierbar: du kannst deinen Style stark beeinflussen (ähnlich wie bei C++/clang-format).
|
||||
- Mehr Freiheit, aber dadurch auch mehr Diskussionen möglich.
|
||||
|
||||
**black**:
|
||||
|
||||
- „Meine Regeln oder gar nicht“.
|
||||
- Ziel: Diskussionen vermeiden, daher kaum Konfigurationsoptionen.
|
||||
- Sehr stabiler, vorhersehbarer Output.
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Black vs. isort
|
||||
|
||||
**isort** ist ein Tool, um **Imports** zu sortieren und zu gruppieren.
|
||||
|
||||
- Sortiert `import`-Zeilen alphabetisch und nach Gruppen:
|
||||
- Standardbibliothek
|
||||
- Third-Party
|
||||
- Projektinterne Module
|
||||
|
||||
Black:
|
||||
|
||||
- Formatiert auch Imports (Zeilenumbrüche, Leerzeichen),
|
||||
- sortiert sie aber **nicht** nach Paketnamen.
|
||||
|
||||
Typischerweise nutzt man:
|
||||
|
||||
```bash
|
||||
isort .
|
||||
black .
|
||||
```
|
||||
|
||||
Oder beides zusammen über Tools wie `ruff` oder `pre-commit`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Welche Probleme löst black?
|
||||
|
||||
### 5.1 Konsistenter Stil in Teams
|
||||
|
||||
Ohne Tool:
|
||||
|
||||
- Jeder schreibt „sein“ Python.
|
||||
- Unterschiedliche Leerzeichen, Umbrüche, String-Stile, etc.
|
||||
- Code wirkt „bunt“ und uneinheitlich.
|
||||
|
||||
Mit black:
|
||||
|
||||
- Jedes Commit, jede Datei, jede Funktion hat denselben Stil.
|
||||
- Neue Teammitglieder lernen schneller, was „üblich“ ist – es ist einfach: das, was black macht.
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Weniger Diskussionen in Code Reviews
|
||||
|
||||
Vor black:
|
||||
|
||||
- Kommentare wie: „Bitte hier ein Leerzeichen einfügen.“
|
||||
- „Kannst du die Argumente untereinander schreiben?“
|
||||
- „Wir verwenden eigentlich doppelte Anführungszeichen.“
|
||||
|
||||
Mit black:
|
||||
|
||||
- Reviewer sagen: „Bitte einmal black drüber laufen lassen.“
|
||||
- Fokus liegt auf:
|
||||
- Ist der Algorithmus korrekt?
|
||||
- Sind die Funktionen gut benannt?
|
||||
- Sind Tests vorhanden?
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Bessere Lesbarkeit & Wartbarkeit
|
||||
|
||||
- Lange Zeilen werden sinnvoll umgebrochen.
|
||||
- Verschachtelte Ausdrücke werden strukturiert.
|
||||
- Datensammlungen (Listen, Dicts) werden mehrzeilig und übersichtlich dargestellt.
|
||||
|
||||
Beispiel: Eine unübersichtliche Dict-Liste wird automatisch gut lesbar formatiert.
|
||||
|
||||
**Vorher:**
|
||||
|
||||
```python
|
||||
users=[{"id":1,"name":"Alice","active":True},{"id":2,"name":"Bob","active":False}]
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
|
||||
```python
|
||||
users = [
|
||||
{"id": 1, "name": "Alice", "active": True},
|
||||
{"id": 2, "name": "Bob", "active": False},
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Weniger „Rauschen“ in Git-Diffs
|
||||
|
||||
Manuell Änderungen + Formatierung:
|
||||
|
||||
- Du änderst eine Zeile, formatierst etwas,
|
||||
- der Diff zeigt viele Änderungen, obwohl nur wenig Logik geändert wurde.
|
||||
|
||||
Mit black:
|
||||
|
||||
- Wenn alle Dateien bereits formatiert sind, entstehen bei späteren Änderungen klarere Diffs:
|
||||
- Format ist überall gleich,
|
||||
- nur die wirklich geänderte Logik fällt auf.
|
||||
|
||||
---
|
||||
|
||||
## 6. Herausforderungen und typische Stolpersteine
|
||||
|
||||
### 6.1 Einstieg in ein bestehendes Projekt
|
||||
|
||||
Problem:
|
||||
|
||||
- Du führst black in einem **alten, großen Projekt** ein.
|
||||
- Beim ersten Durchlauf ändert black Hunderte/tausende Dateien.
|
||||
- Git-Diff ist riesig.
|
||||
|
||||
Lösungen / Best Practices:
|
||||
|
||||
- Einmaliger „Formatting-Commit“ (nur Style):
|
||||
- In einem eigenen Commit alle Dateien mit black formatieren.
|
||||
- Danach neue Commits nur mit funktionalen Anpassungen.
|
||||
- Oder schrittweise:
|
||||
- Nur neue/aktuell bearbeitete Module mit black formatieren.
|
||||
- z. B. mit `pre-commit`-Hook nur geänderte Dateien behandeln.
|
||||
|
||||
---
|
||||
|
||||
### 6.2 „Mir gefällt der Stil nicht!“
|
||||
|
||||
Black ist sehr strikt:
|
||||
- Du kannst nicht „mal eben“ sagen:
|
||||
- „Ich möchte lieber 120 statt 88 Zeichen pro Zeile“ (ok, das **geht** als Option)
|
||||
- Aber: Du kannst nicht festlegen, wie exakt bestimmte Konstrukte formatiert werden.
|
||||
|
||||
Beispiel: Viele stören sich zunächst an:
|
||||
- doppelten Anführungszeichen,
|
||||
- „ungewöhnlichen“ Zeilenumbrüchen.
|
||||
|
||||
Wichtig:
|
||||
- black ist ein **Team-Tool**.
|
||||
- Man einigt sich darauf, den Stil zu akzeptieren, um Diskussionen zu vermeiden.
|
||||
|
||||
---
|
||||
|
||||
### 6.3 Integration mit anderen Tools (z. B. isort, flake8, ruff)
|
||||
|
||||
Typische Stolperfallen:
|
||||
|
||||
- `isort` und `black` können sich widersprechen, wenn sie unterschiedliche Maximalzeilenlängen nutzen.
|
||||
- Linter können sich über Formatierung beschweren, wenn sie anders konfiguriert sind als black.
|
||||
|
||||
Best Practice:
|
||||
|
||||
- Einheitliche Konfiguration, z. B. in `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 88
|
||||
```
|
||||
|
||||
So passen Formatierung und Importsortierung zusammen.
|
||||
|
||||
---
|
||||
|
||||
### 6.4 Änderungen zwischen Black-Versionen
|
||||
|
||||
Black entwickelt sich weiter.
|
||||
|
||||
- In seltenen Fällen ändert eine neue Version den Stil leicht.
|
||||
- Dann kann ein erneuter Durchlauf große Diffs erzeugen.
|
||||
|
||||
Abhilfe:
|
||||
|
||||
- Black-Version in `pyproject.toml` oder `requirements.txt` fest pinnen:
|
||||
|
||||
```text
|
||||
black==24.4.2
|
||||
```
|
||||
|
||||
- Gelegentlich bewusst aktualisieren und einmaliger Reformatting-Commit.
|
||||
|
||||
---
|
||||
|
||||
### 6.5 Performance bei sehr großen Projekten
|
||||
|
||||
Für normale Projekte ist black schnell genug.
|
||||
Bei **sehr großen Repositories** kann einmaliges Formatieren aber dauern.
|
||||
|
||||
Lösungen:
|
||||
|
||||
- Nur geänderte Dateien formatieren (z. B. über `pre-commit`).
|
||||
- In CI nur `black --check .` laufen lassen (prüft, ob alles formatiert ist, ohne neu zu schreiben).
|
||||
|
||||
---
|
||||
|
||||
## 7. Black in der Praxis: Workflows
|
||||
|
||||
### 7.1 Integration in den Editor/IDE
|
||||
|
||||
Die meisten Editoren können black beim Speichern ausführen:
|
||||
|
||||
- **VS Code**:
|
||||
- Erweiterung „Python“ installieren.
|
||||
- In `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"python.formatting.provider": "black",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
```
|
||||
|
||||
- **PyCharm**:
|
||||
- Externes Tool konfigurieren oder über Plugins.
|
||||
|
||||
Vorteil:
|
||||
- Du denkst nicht mehr über Formatierung nach: Speichern = fertig formatiert.
|
||||
|
||||
---
|
||||
|
||||
### 7.2 Verwendung mit `pre-commit`
|
||||
|
||||
`pre-commit` ist ein Tool, das Skripte ausführt, bevor ein Git-Commit akzeptiert wird.
|
||||
|
||||
Installieren:
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
```
|
||||
|
||||
In `.pre-commit-config.yaml`:
|
||||
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.4.2 # Version anpassen
|
||||
hooks:
|
||||
- id: black
|
||||
```
|
||||
|
||||
Aktivieren:
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
Jetzt wird bei jedem `git commit` black auf die geänderten Dateien angewandt.
|
||||
Das stellt sicher, dass alles im Repo immer formatiert ist.
|
||||
|
||||
---
|
||||
|
||||
### 7.3 Black in der CI-Pipeline
|
||||
|
||||
In CI-Systemen (GitHub Actions, GitLab CI, etc.) kannst du prüfen, ob der Code schon formatiert ist.
|
||||
|
||||
Beispiel (CLI):
|
||||
|
||||
```bash
|
||||
black --check .
|
||||
```
|
||||
|
||||
- Exit-Code 0: alles ok.
|
||||
- Exit-Code ≠ 0: irgendetwas müsste formatiert werden.
|
||||
|
||||
Das verhindert, dass unformatierter Code in `main` oder `master` landet.
|
||||
|
||||
---
|
||||
|
||||
## 8. Zusammenfassung
|
||||
|
||||
- **black** ist ein **sehr strenger, kaum konfigurierbarer Formatter** für Python-Code.
|
||||
- Ziel:
|
||||
- **einheitlicher Stil**,
|
||||
- **keine Diskussionen** über Formatierung,
|
||||
- Fokus auf Logik und Qualität statt auf Leerzeichen.
|
||||
- Abgrenzung:
|
||||
- black formatiert nur – es ist kein Linter und kein Typprüfer.
|
||||
- Im Vergleich zu autopep8/yapf: weniger konfigurierbar, dafür klarer, einheitlicher Stil.
|
||||
- Es löst:
|
||||
- Stil-Inkonsistenz, Formatierungs-Diskussionen, „Rauschen“ in Diffs.
|
||||
- Herausforderungen:
|
||||
- Einstieg in ältere Projekte,
|
||||
- wenig Einfluss auf Stil,
|
||||
- Abstimmung mit anderen Tools, Versionsänderungen.
|
||||
- In der Praxis:
|
||||
- Nutze black im Editor (Format-on-save),
|
||||
- im `pre-commit`-Hook,
|
||||
- und als `--check` in der CI.
|
||||
|
||||
Wenn du möchtest, kann ich dir im nächsten Schritt ein kleines Beispielprojekt konstruieren (mit `pyproject.toml`, `pre-commit`, isort/black-Konfiguration), damit du siehst, wie man black von Anfang an sauber einrichtet.
|
||||
+640
@@ -0,0 +1,640 @@
|
||||
Mypy ist ein statischer Typprüfer („Type Checker“) für Python. Er hilft dir, Fehler schon beim Schreiben bzw. vor dem Ausführen des Codes zu finden – ähnlich wie ein Compiler in streng typisierten Sprachen – ohne dass du Python als dynamische Sprache „aufgibst“.
|
||||
|
||||
Ich gehe Schritt für Schritt durch:
|
||||
|
||||
1. Grundidee (für Einsteiger verständlich)
|
||||
2. Kurzer praktischer Einstieg (Installation, erste Checks, einfache Beispiele)
|
||||
3. Welche Probleme mypy löst
|
||||
4. Abgrenzung zu ähnlichen / verwandten Tools
|
||||
5. Herausforderungen und typische Stolpersteine
|
||||
6. Praxisnahe Beispiele und Patterns
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 1. Grundidee: Was macht mypy?
|
||||
|
||||
Python ist dynamisch typisiert: Variablen haben zur Laufzeit Typen, aber der Interpreter prüft sie nicht im Voraus. Viele Fehler sieht man erst, wenn der entsprechende Code ausgeführt wird.
|
||||
|
||||
Mypy ändert daran nichts zur Laufzeit – aber es analysiert deinen Code **statisch** (also ohne ihn auszuführen) und prüft, ob die verwendeten **Typannotationen** konsistent sind.
|
||||
|
||||
### Typannotationen – ein Beispiel
|
||||
|
||||
Ohne Typen:
|
||||
|
||||
```python
|
||||
def add(a, b):
|
||||
return a + b
|
||||
```
|
||||
|
||||
Das ist legal, aber du kannst aus dem Code nicht erkennen, ob `a` und `b` Zahlen, Strings oder etwas anderes sein sollen. Python lässt vieles zu:
|
||||
|
||||
```python
|
||||
print(add(1, 2)) # 3
|
||||
print(add("a", "b")) # "ab"
|
||||
print(add(1, "b")) # TypeError zur Laufzeit
|
||||
```
|
||||
|
||||
Mit Typannotationen:
|
||||
|
||||
```python
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
```
|
||||
|
||||
Damit sagst du: `add` nimmt zwei `int` und gibt einen `int` zurück.
|
||||
|
||||
Mypy überprüft jetzt:
|
||||
|
||||
```bash
|
||||
mypy mein_code.py
|
||||
```
|
||||
|
||||
und meldet z.B.:
|
||||
|
||||
```text
|
||||
mein_code.py:10: error: Argument 2 to "add" has incompatible type "str"; expected "int"
|
||||
```
|
||||
|
||||
– wenn du irgendwo `add(1, "b")` aufrufst.
|
||||
|
||||
**Grundidee**:
|
||||
Du schreibst „Verträge“ (Typen) in deinen Code, und mypy überprüft, ob du dich überall daran hältst. Das verbessert Lesbarkeit, Robustheit und macht refactoring sicherer.
|
||||
|
||||
---
|
||||
|
||||
## 2. Kurzer praktischer Einstieg
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
pip install mypy
|
||||
```
|
||||
|
||||
(Je nach Setup evtl. in einer virtuellen Umgebung.)
|
||||
|
||||
### Minimalbeispiel
|
||||
|
||||
`calculator.py`:
|
||||
|
||||
```python
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
def main() -> None:
|
||||
x = add(1, 2)
|
||||
y = add("a", "b") # Fehler
|
||||
|
||||
print(x, y)
|
||||
```
|
||||
|
||||
Mypy ausführen:
|
||||
|
||||
```bash
|
||||
mypy calculator.py
|
||||
```
|
||||
|
||||
Ausgabe:
|
||||
|
||||
```text
|
||||
calculator.py:6: error: Argument 1 to "add" has incompatible type "str"; expected "int"
|
||||
Found 1 error in 1 file (checked 1 source file)
|
||||
```
|
||||
|
||||
Obwohl Python den Code ausführen würde (und bei `add("a", "b")` sogar ein „korrektes“ Ergebnis liefern würde: `"ab"`), sagt mypy: Du hast gegen deinen eigenen Typvertrag verstoßen.
|
||||
|
||||
### Gradual Typing
|
||||
|
||||
Du musst nicht alles von Anfang an typisieren. Du kannst Schritt für Schritt anfangen:
|
||||
|
||||
```python
|
||||
def add(a, b): # keine Typen hier
|
||||
return a + b
|
||||
|
||||
def use_add() -> int:
|
||||
result = add(1, 2) # mypy lässt das oft durchgehen (je nach Konfiguration)
|
||||
return result
|
||||
```
|
||||
|
||||
Mypy arbeitet „gradual“:
|
||||
|
||||
- Ungetypte Bereiche werden als `Any` betrachtet (unsicher, aber flexibel).
|
||||
- Getypte Bereiche werden überprüft.
|
||||
- Du kannst nach und nach mehr Typen hinzufügen und die Strenge erhöhen.
|
||||
|
||||
---
|
||||
|
||||
## 3. Welche Probleme werden durch mypy gelöst?
|
||||
|
||||
### 3.1. Typbezogene Fehler früh erkennen
|
||||
|
||||
Typische Klassen von Bugs:
|
||||
|
||||
1. **Falsche Argumenttypen**:
|
||||
|
||||
```python
|
||||
def send_email(to: str, subject: str, body: str) -> None:
|
||||
...
|
||||
|
||||
send_email(["user@example.com"], "Hi", "Text") # Bug: Liste statt String
|
||||
```
|
||||
|
||||
Mypy:
|
||||
|
||||
```text
|
||||
error: Argument 1 to "send_email" has incompatible type "List[str]"; expected "str"
|
||||
```
|
||||
|
||||
2. **Verfügbare Attribute/Methoden**:
|
||||
|
||||
```python
|
||||
def greet(name: str) -> None:
|
||||
print(name.upper())
|
||||
|
||||
user_name: int = 42
|
||||
greet(user_name)
|
||||
```
|
||||
|
||||
Mypy:
|
||||
|
||||
```text
|
||||
error: Argument 1 to "greet" has incompatible type "int"; expected "str"
|
||||
```
|
||||
|
||||
3. **Optionale Werte vergessen zu prüfen** (`None`):
|
||||
|
||||
```python
|
||||
from typing import Optional
|
||||
|
||||
def get_user_name(user_id: int) -> Optional[str]:
|
||||
...
|
||||
|
||||
def print_name(user_id: int) -> None:
|
||||
name = get_user_name(user_id)
|
||||
print(name.upper()) # Bug: name kann None sein!
|
||||
```
|
||||
|
||||
Mypy:
|
||||
|
||||
```text
|
||||
error: Item "None" of "Optional[str]" has no attribute "upper"
|
||||
```
|
||||
|
||||
Du wirst gezwungen, zuerst auf `None` zu prüfen:
|
||||
|
||||
```python
|
||||
def print_name(user_id: int) -> None:
|
||||
name = get_user_name(user_id)
|
||||
if name is None:
|
||||
print("User not found")
|
||||
return
|
||||
print(name.upper()) # jetzt ok
|
||||
```
|
||||
|
||||
### 3.2. Sicherere Refactorings
|
||||
|
||||
Wenn du Funktionensignaturen änderst, Parameter umbenennst oder Rückgabetypen anpasst, kann mypy dir helfen, alle Stellen zu finden, die du anpassen musst.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```python
|
||||
# vorher
|
||||
def get_price(product_id: int) -> float:
|
||||
...
|
||||
|
||||
# nachher
|
||||
def get_price(product_id: int) -> int: # Rückgabetyp geändert!
|
||||
...
|
||||
```
|
||||
|
||||
Wenn irgendwo angenommen wird, dass `float` zurückkommt:
|
||||
|
||||
```python
|
||||
price_cents: float = get_price(123) # jetzt inkonsistent
|
||||
```
|
||||
|
||||
meldet mypy das. Das verringert das Risiko von subtilen Bugs nach Refactorings.
|
||||
|
||||
### 3.3. Bessere Dokumentation & IDE-Unterstützung
|
||||
|
||||
Typannotationen sind lebende Dokumentation:
|
||||
|
||||
```python
|
||||
def load_config(path: str) -> dict[str, str]:
|
||||
...
|
||||
```
|
||||
|
||||
Du siehst sofort, was die Funktion erwartet und liefert – ohne lange Kommentare. IDEs nutzen die Typen für:
|
||||
|
||||
- Autovervollständigung
|
||||
- Inlay Hints
|
||||
- Navigation („go to definition“)
|
||||
- Inline-Fehlermeldungen
|
||||
|
||||
---
|
||||
|
||||
## 4. Abgrenzung zu ähnlichen / verwandten Tools
|
||||
|
||||
### 4.1. Mypy vs. Linter (z.B. flake8, pylint)
|
||||
|
||||
**Linter** prüfen v.a.:
|
||||
|
||||
- Stil (PEP 8)
|
||||
- potenziell problematische Patterns (unbenutzte Variablen, Schatten von Builtins, zu komplexe Funktionen)
|
||||
- gewisse Logikfehler (z.B. nie erreichte Codezweige)
|
||||
|
||||
**Mypy** fokussiert auf **Typkonsistenz**:
|
||||
|
||||
- Stimmen die deklarierten Typen mit den tatsächlichen Verwendungen überein?
|
||||
- Können bestimmte Codezweige überhaupt erreicht werden, wenn Typen berücksichtigt werden?
|
||||
- Sind Operationen auf bestimmten Typen erlaubt?
|
||||
|
||||
Beispiel:
|
||||
|
||||
```python
|
||||
x = []
|
||||
x.append(1)
|
||||
x.append("a")
|
||||
```
|
||||
|
||||
Linter: meistens kein Problem.
|
||||
Mypy (je nach Typinferenz) könnte sagen:
|
||||
|
||||
```text
|
||||
List item 1 has incompatible type "str"; expected "int"
|
||||
```
|
||||
|
||||
Fazit:
|
||||
Linter und mypy ergänzen sich – sie ersetzen sich nicht.
|
||||
|
||||
### 4.2. Mypy vs. Testframeworks (pytest, unittest)
|
||||
|
||||
**Tests**:
|
||||
|
||||
- prüfen Laufzeitverhalten für konkrete Eingaben.
|
||||
- stellen sicher, dass Funktionen das tun, was fachlich / funktional gewünscht ist.
|
||||
|
||||
**Mypy**:
|
||||
|
||||
- prüft nur Typkonsistenz – keine fachliche Korrektheit.
|
||||
- findet z.B. nicht, ob du die falsche mathematische Formel verwendest, solange die Typen passen.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```python
|
||||
def calculate_discount(price: float) -> float:
|
||||
return price * 2 # fachlich falsch, aber typgerecht
|
||||
```
|
||||
|
||||
Mypy ist zufrieden. Ein Unit-Test würde diesen Fehler finden.
|
||||
|
||||
Fazit:
|
||||
Mypy ergänzt Tests, ersetzt sie aber nicht.
|
||||
|
||||
### 4.3. Mypy vs. andere Typechecker (Pyright, Pyre, pytype)
|
||||
|
||||
Es gibt mehrere Typchecker für Python:
|
||||
|
||||
- **mypy** – der „Klassiker“, in Python geschrieben, von vielen Projekten verwendet.
|
||||
- **pyright** – sehr schneller Typechecker (Microsoft), in TypeScript geschrieben.
|
||||
- **pyre** – von Meta (Facebook), mit Fokus auf große Codebasen.
|
||||
- **pytype** – von Google.
|
||||
|
||||
Sie verfolgen alle eine ähnliche Idee: statische Typprüfung für Python. Unterschiede gibt es bei:
|
||||
|
||||
- Performance
|
||||
- Genauigkeit / Strenge in bestimmten Bereichen
|
||||
- Tooling-Integration (z.B. VS Code nutzt intern Pyright)
|
||||
|
||||
Für den Einstieg ist mypy völlig ausreichend und weit verbreitet.
|
||||
|
||||
### 4.4. Mypy vs. Laufzeit-Typprüfung (pydantic, marshmallow)
|
||||
|
||||
**pydantic** & Co.:
|
||||
|
||||
- validieren und konvertieren Daten zur **Laufzeit** (z.B. JSON-Input in API).
|
||||
- werfen Exceptions, wenn Daten nicht passen.
|
||||
- nutzen Typannotationen als Basis, sind aber nicht auf Compile-/Check-Zeit beschränkt.
|
||||
|
||||
**Mypy**:
|
||||
|
||||
- prüft nur zur Analysezeit, ändert das Laufzeitverhalten nicht.
|
||||
- „merkt nicht“, ob zur Laufzeit echte Validierung stattfindet.
|
||||
|
||||
Beispiel mit pydantic:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class User(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
user = User(id="123", name="Alice") # zur Laufzeit wird "123" zu int geparst
|
||||
```
|
||||
|
||||
Mypy würde melden:
|
||||
|
||||
```text
|
||||
Argument "id" to "User" has incompatible type "str"; expected "int"
|
||||
```
|
||||
|
||||
– obwohl pydantic das zur Laufzeit akzeptiert und konvertiert.
|
||||
Hier musst du entscheiden, ob du dich eher am statischen Vertrag (Typ) oder am dynamischen Verhalten orientieren willst.
|
||||
|
||||
---
|
||||
|
||||
## 5. Herausforderungen und typische Stolpersteine
|
||||
|
||||
### 5.1. Legacy Code ohne Typen
|
||||
|
||||
In bestehendem Code fehlen oft Typannotationen, und vieles ist dynamisch.
|
||||
|
||||
Strategien:
|
||||
|
||||
- Zuerst nur neue Modules/Funktionen typisieren.
|
||||
- Mypy mit „lockereren“ Einstellungen starten.
|
||||
- Langsam „strictness“ erhöhen.
|
||||
|
||||
Beispiel-Konfiguration (`mypy.ini`):
|
||||
|
||||
```ini
|
||||
[mypy]
|
||||
python_version = 3.11
|
||||
ignore_missing_imports = True
|
||||
disallow_untyped_defs = False
|
||||
disallow_incomplete_defs = False
|
||||
```
|
||||
|
||||
Später kannst du verschärfen:
|
||||
|
||||
```ini
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
warn_unused_ignores = True
|
||||
strict_optional = True
|
||||
```
|
||||
|
||||
### 5.2. Dynamische Features von Python
|
||||
|
||||
Dinge wie:
|
||||
|
||||
- dynamisches Hinzufügen von Attributen
|
||||
- `setattr`, `getattr`
|
||||
- Metaklassen-Magie
|
||||
- Monkey-Patching
|
||||
|
||||
sind schwer für statische Analyser.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```python
|
||||
class Dynamic:
|
||||
pass
|
||||
|
||||
obj = Dynamic()
|
||||
obj.name = "Alice" # dynamisches Attribut
|
||||
print(obj.name)
|
||||
```
|
||||
|
||||
Mypy weiß nicht, dass `name` existiert, und meldet:
|
||||
|
||||
```text
|
||||
error: "Dynamic" has no attribute "name"
|
||||
```
|
||||
|
||||
Workarounds:
|
||||
|
||||
- Attribut im Klassendefinitionskörper deklarieren:
|
||||
|
||||
```python
|
||||
class Dynamic:
|
||||
name: str
|
||||
```
|
||||
- oder `# type: ignore` an problematischen Stellen nutzen.
|
||||
|
||||
### 5.3. Komplexe Typen und Verbosität
|
||||
|
||||
Generics, `Union`, `Optional`, `TypedDict`, `Protocol` etc. können komplex werden. Das kostet Einarbeitung.
|
||||
|
||||
Beispiel für generische Funktion:
|
||||
|
||||
```python
|
||||
from typing import TypeVar, Iterable, List
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def first(items: Iterable[T]) -> T:
|
||||
for item in items:
|
||||
return item
|
||||
raise ValueError("Empty iterable")
|
||||
```
|
||||
|
||||
Mypy hilft hier, allgemeingültige, typsichere Utilities zu schreiben, aber das Typ-System wird relativ mächtig (und gelegentlich inelegant).
|
||||
|
||||
### 5.4. False Positives und `# type: ignore`
|
||||
|
||||
Manchmal **weißt du mehr** als mypy. Dann musst du mit mypy kommunizieren:
|
||||
|
||||
```python
|
||||
from typing import cast, Any
|
||||
|
||||
def get_from_json(json_obj: dict[str, Any]) -> int:
|
||||
return cast(int, json_obj["value"])
|
||||
```
|
||||
|
||||
oder:
|
||||
|
||||
```python
|
||||
some_weird_library_call() # type: ignore[arg-type]
|
||||
```
|
||||
|
||||
Zu viele `# type: ignore` können aber wieder die Sicherheit untergraben. Es lohnt sich, sie sparsam und begründet einzusetzen.
|
||||
|
||||
### 5.5. Performance bei großen Codebasen
|
||||
|
||||
Für wirklich große Projekte kann mypy langsamer werden, vor allem bei vielen Imports und tiefen Typstrukturen.
|
||||
Es gibt Optionen wie `--incremental` und `dmypy` (Daemon-Modus), um das zu beschleunigen.
|
||||
|
||||
---
|
||||
|
||||
## 6. Praxisnahe Beispiele & Patterns
|
||||
|
||||
### 6.1. Basic Typannotationen
|
||||
|
||||
```python
|
||||
def greet(name: str, times: int = 1) -> None:
|
||||
for _ in range(times):
|
||||
print(f"Hello, {name}!")
|
||||
```
|
||||
|
||||
Sammlungstypen:
|
||||
|
||||
```python
|
||||
from typing import List, Dict
|
||||
|
||||
def total_length(names: List[str]) -> int:
|
||||
length = 0
|
||||
for n in names:
|
||||
length += len(n)
|
||||
return length
|
||||
|
||||
def invert_mapping(mapping: Dict[int, str]) -> Dict[str, int]:
|
||||
return {v: k for k, v in mapping.items()}
|
||||
```
|
||||
|
||||
Ab Python 3.9 kannst du oft die Kurzform nutzen:
|
||||
|
||||
```python
|
||||
def total_length(names: list[str]) -> int:
|
||||
...
|
||||
def invert_mapping(mapping: dict[int, str]) -> dict[str, int]:
|
||||
...
|
||||
```
|
||||
|
||||
### 6.2. Optional und Union
|
||||
|
||||
```python
|
||||
from typing import Optional, Union
|
||||
|
||||
def parse_int(value: str) -> Optional[int]:
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def stringify(value: Union[int, float]) -> str:
|
||||
return f"{value:.2f}"
|
||||
```
|
||||
|
||||
Aufruf:
|
||||
|
||||
```python
|
||||
result = parse_int("123")
|
||||
if result is not None:
|
||||
print(result + 1)
|
||||
```
|
||||
|
||||
Mypy zwingt dich, mit dem `None`-Fall umzugehen.
|
||||
|
||||
### 6.3. Typen für Klassen
|
||||
|
||||
```python
|
||||
class User:
|
||||
def __init__(self, user_id: int, name: str) -> None:
|
||||
self.user_id = user_id
|
||||
self.name = name
|
||||
|
||||
def greet(self) -> str:
|
||||
return f"Hello, {self.name}!"
|
||||
```
|
||||
|
||||
### 6.4. Dataclasses mit Typen
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Product:
|
||||
id: int
|
||||
name: str
|
||||
price_cents: int
|
||||
|
||||
def apply_discount(product: Product, percent: float) -> Product:
|
||||
discount = int(product.price_cents * percent / 100)
|
||||
return Product(
|
||||
id=product.id,
|
||||
name=product.name,
|
||||
price_cents=product.price_cents - discount
|
||||
)
|
||||
```
|
||||
|
||||
Mypy prüft, ob du `Product` überall korrekt verwendest.
|
||||
|
||||
### 6.5. Typen für „Dictionary-Objekte“: TypedDict
|
||||
|
||||
Wenn du viele Dictionaries mit immer der gleichen Struktur verwendest (z.B. JSON):
|
||||
|
||||
```python
|
||||
from typing import TypedDict
|
||||
|
||||
class UserData(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
email: str
|
||||
|
||||
def send_welcome_mail(user: UserData) -> None:
|
||||
print(f"Sending mail to {user['email']}")
|
||||
```
|
||||
|
||||
Mypy prüft:
|
||||
|
||||
- fehlen Keys?
|
||||
- stimmen die Typen der Werte?
|
||||
|
||||
### 6.6. Duck Typing mit Protocol
|
||||
|
||||
Du willst „etwas, das wie eine Datei ist“, nicht unbedingt eine konkrete Klasse:
|
||||
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
class Writer(Protocol):
|
||||
def write(self, data: str) -> int:
|
||||
...
|
||||
|
||||
def log_to(writer: Writer, message: str) -> None:
|
||||
writer.write(message + "\n")
|
||||
```
|
||||
|
||||
Jede Klasse, die `write(self, str) -> int` implementiert, passt – ganz im Sinne von „Duck Typing“, aber statisch überprüfbar.
|
||||
|
||||
### 6.7. Mypy-Konfiguration in der Praxis
|
||||
|
||||
`mypy.ini` im Projektverzeichnis:
|
||||
|
||||
```ini
|
||||
[mypy]
|
||||
python_version = 3.11
|
||||
strict = True # aktiviert viele strenge Checks
|
||||
|
||||
# Beispiele für häufige Optionen:
|
||||
# disallow_untyped_defs = True
|
||||
# disallow_incomplete_defs = True
|
||||
# no_implicit_optional = True
|
||||
# warn_unused_ignores = True
|
||||
# warn_return_any = True
|
||||
```
|
||||
|
||||
Du kannst auch per Modulbereich Regeln setzen:
|
||||
|
||||
```ini
|
||||
[mypy-myproject.legacy.*]
|
||||
ignore_errors = True # Legacy-Code vorerst ignorieren
|
||||
|
||||
[mypy-myproject.core.*]
|
||||
strict = True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
- **Grundidee**: mypy ist ein statischer Typprüfer für Python. Du annotierst deinen Code mit Typen, und mypy überprüft, ob der Code diese Verträge einhält.
|
||||
- **Probleme, die gelöst werden**:
|
||||
- Frühes Finden von typbezogenen Fehlern
|
||||
- Sicherere Refactorings
|
||||
- Bessere Dokumentation und Tooling/IDE-Unterstützung
|
||||
- **Abgrenzung**:
|
||||
- Linter → Stil & allgemeine Codequalität; mypy → Typkonsistenz
|
||||
- Tests → fachliche Korrektheit zur Laufzeit; mypy → statische Typkonsistenz
|
||||
- Andere Typchecker (Pyright etc.) verfolgen die gleiche Grundidee
|
||||
- Runtime-Validatoren (pydantic) prüfen zur Laufzeit, mypy nur zur Analysezeit
|
||||
- **Herausforderungen**:
|
||||
- Einstieg in bestehende, dynamische Codebasen
|
||||
- Dynamische Python-Features sind schwer statisch zu erfassen
|
||||
- Komplexe Typen können verbose sein
|
||||
- Manchmal sind `cast` und `# type: ignore` nötig
|
||||
|
||||
Wenn du magst, kann ich dir als nächsten Schritt ein kleines Beispielprojekt skizzieren (z.B. eine Mini-API oder ein CLI-Tool) und zeigen, wie man mypy dort konkret einführt und schrittweise verschärft.
|
||||
+1212
File diff suppressed because it is too large
Load Diff
+540
@@ -0,0 +1,540 @@
|
||||
Ruff ist ein relativ neues, sehr schnelles Tool zur Verbesserung der Code-Qualität in Python. Es kombiniert Funktionen, für die man früher mehrere Werkzeuge brauchte (Linter, Formatter, Import-Sortierer, teilweise Sicherheits-Checks), in einem Programm.
|
||||
|
||||
Ich gehe Schritt für Schritt durch:
|
||||
|
||||
1. Grundidee und Zielgruppe
|
||||
2. Was macht ruff konkret?
|
||||
3. Abgrenzung zu ähnlichen Tools
|
||||
4. Welche Probleme löst ruff?
|
||||
5. Typische Herausforderungen im Einsatz
|
||||
6. Praxisnahe Beispiele (inkl. Konfiguration)
|
||||
|
||||
---
|
||||
|
||||
## 1. Grundidee von ruff – in einfachen Worten
|
||||
|
||||
Stell dir vor, mehrere Entwickler:innen arbeiten an einem Python-Projekt. Ohne Regeln sieht der Code schnell sehr unterschiedlich aus:
|
||||
|
||||
- Mal sind Leerzeichen anders gesetzt,
|
||||
- mal werden nicht genutzte Variablen liegen gelassen,
|
||||
- vielleicht wird an manchen Stellen unsicherer Code geschrieben,
|
||||
- im schlimmsten Fall fallen bestimmte Fehler erst spät auf.
|
||||
|
||||
Bisher nutzte man typischerweise mehrere Tools:
|
||||
|
||||
- ein **Linter** (z. B. *flake8* oder *pylint*),
|
||||
- einen **Formatter** (z. B. *black*),
|
||||
- einen **Import-Sortierer** (z. B. *isort*),
|
||||
- evtl. ein **Security-Tool** (z. B. *bandit*),
|
||||
- plus diverse Plugins.
|
||||
|
||||
**Ruff will das bündeln:**
|
||||
|
||||
- Ein Tool für viele Code-Qualitätsaspekte
|
||||
- Sehr schnell (geschrieben in Rust, nicht in Python)
|
||||
- Einfache Konfiguration (alles typischerweise in `pyproject.toml`)
|
||||
- Viele Checks, die sich an bekannten Tools orientieren (flake8-Plugins, isort-Regeln, bandit-Regeln usw.)
|
||||
|
||||
---
|
||||
|
||||
## 2. Was macht ruff konkret?
|
||||
|
||||
### 2.1. Linting (Fehler und Stilprobleme finden)
|
||||
|
||||
Ruff untersucht deinen Python-Code und meldet z. B.:
|
||||
|
||||
- Syntax-Fehler
|
||||
- Ungenutzte Variablen oder Importe
|
||||
- Logische Stolperfallen (z. B. `== None` statt `is None`)
|
||||
- Stil-Regeln (z. B. maximale Zeilenlänge)
|
||||
- Potenziell unsichere Konstruktionen
|
||||
|
||||
Beispiel:
|
||||
|
||||
```python
|
||||
def calculate(a, b):
|
||||
result = a + b
|
||||
unused = 42
|
||||
if a == None:
|
||||
print("a is None")
|
||||
return result
|
||||
```
|
||||
|
||||
Ruff würde in etwa melden (vereinfacht):
|
||||
|
||||
- `unused` wird nie verwendet
|
||||
- `a == None` – besser `a is None`
|
||||
|
||||
Viele dieser Probleme kann ruff auch **automatisch beheben** (`--fix`).
|
||||
|
||||
---
|
||||
|
||||
### 2.2. Formatierung (Code automatisch „schön“ machen)
|
||||
|
||||
Ruff hat inzwischen einen **eigenen Formatter**, der ähnlich wie *black* funktioniert:
|
||||
|
||||
- Einheitliche Einrückungen und Zeilenumbrüche
|
||||
- Klammer-Formatierung
|
||||
- Konsistente Verwendung von Anführungszeichen (je nach Einstellung)
|
||||
- Entfernung überflüssiger Leerzeilen usw.
|
||||
|
||||
Beispiel (unformatierter Code):
|
||||
|
||||
```python
|
||||
def foo( x:int,y:int )->int:
|
||||
return x+y
|
||||
```
|
||||
|
||||
Nach `ruff format` könnte das so aussehen:
|
||||
|
||||
```python
|
||||
def foo(x: int, y: int) -> int:
|
||||
return x + y
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3. Imports sortieren und aufräumen
|
||||
|
||||
Ruff kann ähnlich wie *isort*:
|
||||
|
||||
- Importe sortieren (alphabetisch und nach Gruppen: Standardbibliothek, Drittanbieter, Projektcode)
|
||||
- Unbenutzte Importe entfernen
|
||||
|
||||
Beispiel:
|
||||
|
||||
```python
|
||||
import myproject.utils
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
|
||||
from math import sqrt
|
||||
from math import ceil
|
||||
```
|
||||
|
||||
Nach ruff (vereinfacht):
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
from math import ceil, sqrt
|
||||
|
||||
import requests
|
||||
|
||||
import myproject.utils
|
||||
```
|
||||
|
||||
Und wenn `sys` gar nicht verwendet wird, kann ruff es auch entfernen.
|
||||
|
||||
---
|
||||
|
||||
### 2.4. Ein Tool – mehrere Regel-Sammlungen
|
||||
|
||||
Ruff bringt rule sets mit, die vielen bekannten Tools entsprechen, z. B.:
|
||||
|
||||
- **E/F/W**: Pycodestyle/Pyflakes-ähnlich (via flake8)
|
||||
- **I**: isort-Regeln
|
||||
- **N**: pep8-naming (Namenskonventionen)
|
||||
- **S**: bandit (Sicherheitsregeln)
|
||||
- **UP**: pyupgrade (veraltete Syntax, modernisieren)
|
||||
- u. v. m.
|
||||
|
||||
Du kannst über die Konfiguration steuern, welche Regel-Gruppen du aktivierst oder deaktivierst.
|
||||
|
||||
---
|
||||
|
||||
## 3. Abgrenzung zu ähnlichen Tools
|
||||
|
||||
### 3.1. Ruff vs. flake8 (+ Plugins)
|
||||
|
||||
**flake8** ist ein Linter; für viele Extras braucht man Plugins:
|
||||
|
||||
- z. B. `flake8-bugbear`, `flake8-import-order`, `pep8-naming` usw.
|
||||
|
||||
**Ruff:**
|
||||
|
||||
- Bietet die Funktionalität vieler flake8-Plugins „eingebaut“.
|
||||
- Ist deutlich **schneller** (insbesondere bei großen Projekten).
|
||||
- Wird oft als Drop-in-Ersatz für flake8 verwendet.
|
||||
|
||||
Aber:
|
||||
|
||||
- flake8 ist schon lange etabliert; manche Teams haben stark angepasste flake8-Setups, die man nicht 1:1 nach ruff übertragen kann.
|
||||
- Einige Spezial-Plugins existieren ggf. nur für flake8.
|
||||
|
||||
---
|
||||
|
||||
### 3.2. Ruff vs. pylint
|
||||
|
||||
**pylint**:
|
||||
|
||||
- Sehr umfangreicher Linter (viele komplexe Regeln, u. a. über Projektstruktur, OOP-Patterns usw.)
|
||||
- Langsam im Vergleich zu ruff
|
||||
- Detaillierte Reports und Scores
|
||||
|
||||
**Ruff**:
|
||||
|
||||
- Fokus auf **Geschwindigkeit** und auf Regeln, die sich gut automatisieren/auto-fixen lassen.
|
||||
- Viele „klassische“ Lint-Regeln, Naming, Imports, Security-Basics, aber nicht alle tiefgehenden Analysen von pylint.
|
||||
- Für komplexe Architektur-Regeln wird weiterhin oft pylint oder andere Tools genutzt.
|
||||
|
||||
---
|
||||
|
||||
### 3.3. Ruff vs. black
|
||||
|
||||
**black**:
|
||||
|
||||
- Reiner **Formatter** – macht nur Formatierung, keine Lint-Fehler (bis auf ganz wenige Ausnahmen).
|
||||
- Sehr stabile, strikte Formatierung (Meinung: „The uncompromising code formatter“).
|
||||
|
||||
**Ruff**:
|
||||
|
||||
- Eigenständiger Formatter, der in vielen Projekten Black ersetzen kann.
|
||||
- Zusätzlich: Linting, Import-Sortierung, Security-Regeln usw.
|
||||
- Du kannst:
|
||||
- nur ruff als Formatter nutzen, oder
|
||||
- ruff als Linter + black als Formatter (dann `ruff format` nicht verwenden), wenn dein Team bereits stark auf black setzt.
|
||||
|
||||
---
|
||||
|
||||
### 3.4. Ruff vs. isort
|
||||
|
||||
**isort**:
|
||||
|
||||
- Spezialisiert auf das Sortieren von Imports.
|
||||
|
||||
**Ruff**:
|
||||
|
||||
- Hat eine integrierte Import-Sortierung (Regelgruppe `I`).
|
||||
- Für die meisten Fälle reicht ruff völlig aus.
|
||||
- Wenn ihr sehr spezielle Import-Sortierregeln braucht, ist isort manchmal noch flexibler, aber das wird immer weniger relevant.
|
||||
|
||||
---
|
||||
|
||||
### 3.5. Ruff vs. mypy/pyright (Typprüfung)
|
||||
|
||||
**mypy/pyright**:
|
||||
|
||||
- Statische Typprüfer: sie prüfen, ob die Typannotationen sinnvoll zusammenpassen.
|
||||
- Finden z. B. Fehler wie: „Funktion gibt laut Typ `str` zurück, tatsächlich aber `int`“.
|
||||
|
||||
**Ruff**:
|
||||
|
||||
- Enthält Regeln, die mit Typannotationen arbeiten (z. B. Style, Safety), aber **kein vollwertiger Typprüfer**.
|
||||
- Typfehler (im Sinne von mypy) sollten weiterhin mit mypy oder pyright geprüft werden.
|
||||
|
||||
---
|
||||
|
||||
### 3.6. Ruff vs. bandit (Security)
|
||||
|
||||
**bandit**:
|
||||
|
||||
- Spezialisiertes Security-Tool für Python.
|
||||
|
||||
**Ruff**:
|
||||
|
||||
- Hat viele bandit-Regeln integriert (Regelgruppe `S`).
|
||||
- Deckt gängige Sicherheitsfallen ab (z. B. `eval` auf untrusted Input, hartkodierte Passwörter etc.).
|
||||
- Für tiefgehende Security-Audits kann ein spezialisiertes Tool trotzdem sinnvoll sein.
|
||||
|
||||
---
|
||||
|
||||
## 4. Welche Probleme löst ruff?
|
||||
|
||||
### 4.1. Performance-Probleme in großen Projekten
|
||||
|
||||
Früher:
|
||||
|
||||
- flake8 + black + isort + bandit + mypy
|
||||
→ viele Tools, mehrfaches Einlesen des Codes, CI dauert lange.
|
||||
|
||||
Mit ruff:
|
||||
|
||||
- Ein Tool übernimmt Linting, Formatierung, Importe, einen großen Teil der Security-Regeln.
|
||||
- Deutlich weniger Laufzeit, besonders in CI-Pipelines oder bei großen Repositories.
|
||||
|
||||
---
|
||||
|
||||
### 4.2. Zu viele Tools, komplizierte Konfiguration
|
||||
|
||||
Problem:
|
||||
|
||||
- Unterschiedliche Konfigurationsdateien (`.flake8`, `pyproject.toml`, `setup.cfg`, `.isort.cfg`, `pyproject.toml` für black…)
|
||||
- Mehr Aufwand beim Onboarding neuer Teammitglieder.
|
||||
|
||||
Ruff:
|
||||
|
||||
- Typischerweise alles in **einer** Konfiguration (`pyproject.toml`).
|
||||
- Weniger bewegliche Teile, einfachere Wartung.
|
||||
|
||||
---
|
||||
|
||||
### 4.3. Inkonsequente Codequalität im Team
|
||||
|
||||
Ohne einheitliche Tools:
|
||||
|
||||
- Jede:r schreibt etwas anders.
|
||||
- Diskussionen in Code-Reviews drehen sich um Stil statt Inhalte.
|
||||
- Fehler (z. B. ungenutzte Variablen, potentielle Bugs) werden erst spät bemerkt.
|
||||
|
||||
Mit ruff:
|
||||
|
||||
- Gemeinsame, automatisierte Regeln.
|
||||
- Automatische Fixes für vieles (z. B. im Editor oder Pre-Commit-Hooks).
|
||||
- Code-Reviews können sich auf Architektur und Logik konzentrieren.
|
||||
|
||||
---
|
||||
|
||||
### 4.4. Technische Schulden reduzieren
|
||||
|
||||
In älteren Projekten:
|
||||
|
||||
- Viele kleine Stil- und Qualitätsprobleme haben sich angesammelt.
|
||||
- Niemand möchte „alles mal eben aufräumen“, weil Tools zu langsam sind oder es zu viel ist.
|
||||
|
||||
Mit ruff:
|
||||
|
||||
- Durch die Geschwindigkeit kann man auch große Codebasen lintern.
|
||||
- Schrittweise Verbesserung möglich: z. B. zunächst nur wichtige Regelgruppen aktivieren, später mehr.
|
||||
|
||||
---
|
||||
|
||||
## 5. Herausforderungen bei der Nutzung von ruff
|
||||
|
||||
### 5.1. Zu viele Meldungen am Anfang
|
||||
|
||||
Wenn du ruff das erste Mal auf ein älteres Projekt loslässt, bekommst du oft hunderte oder tausende Meldungen.
|
||||
|
||||
Strategie:
|
||||
|
||||
- Nur einen Teil der Regeln aktivieren (z. B. nur „kritische“ oder klar hilfreiche).
|
||||
- Bestehende Verstöße einmalig ignorieren (per `--ignore` oder `--per-file-ignores`) und neue Verstöße blocken.
|
||||
- Nach und nach alte Stellen aufräumen.
|
||||
|
||||
---
|
||||
|
||||
### 5.2. Regeln verstehen und anpassen
|
||||
|
||||
Ruff hat sehr viele Regeln. Nicht alle passen zu jedem Projekt.
|
||||
|
||||
- Manche Regeln sind sehr streng (z. B. bestimmte Naming- oder Docstring-Regeln).
|
||||
- Du musst überlegen: Welche Regeln sind für unser Team sinnvoll?
|
||||
|
||||
Lösung:
|
||||
|
||||
- Regeln gezielt aktivieren/deaktivieren.
|
||||
- Dokumentieren, warum bestimmte Regeln aktiv oder abgeschaltet sind.
|
||||
|
||||
---
|
||||
|
||||
### 5.3. Wechsel von bestehender Tool-Landschaft
|
||||
|
||||
Wenn ihr schon flake8, black, isort etc. nutzt:
|
||||
|
||||
- Müssen Einstellungen in ruff nachgebaut werden.
|
||||
- Manche Teams werden black nicht sofort durch ruff format ersetzen wollen.
|
||||
|
||||
Ein pragmatischer Weg:
|
||||
|
||||
1. Zuerst ruff als **Linter** einführen (Formatierung bleibt bei black).
|
||||
2. Wenn gewünscht, später ruff format testen und ggf. black ersetzen.
|
||||
|
||||
---
|
||||
|
||||
### 5.4. Auto-Fixes mit Vorsicht genießen
|
||||
|
||||
Ruff kann sehr viel automatisch reparieren:
|
||||
|
||||
- In der Regel gut, aber:
|
||||
- Bei manchen Regeln sollte man prüfen, ob die Änderung wirklich die Absicht trifft.
|
||||
- In kritischen Bereichen (z. B. Security-sensible Logik) ggf. Auto-Fixes nicht blind akzeptieren.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
- Auto-Fixes lokal ausführen, dann diff anschauen.
|
||||
- In CI eher nur prüfen, nicht fixen.
|
||||
|
||||
---
|
||||
|
||||
### 5.5. Editor-Integration
|
||||
|
||||
Ruff hat gute Unterstützung in vielen Editoren (VS Code, PyCharm, Neovim usw.), aber:
|
||||
|
||||
- Man muss oft ein Plugin oder eine Extension installieren.
|
||||
- Manchmal überschneidet sich das mit vorhandenen Tools (z. B. Black-Extension vs. Ruff-Formatter); das muss sauber konfiguriert werden.
|
||||
|
||||
---
|
||||
|
||||
## 6. Praxisnahe Beispiele
|
||||
|
||||
### 6.1. Installation von ruff
|
||||
|
||||
Typisch über `pip`:
|
||||
|
||||
```bash
|
||||
pip install ruff
|
||||
```
|
||||
|
||||
Oder als dev-Abhängigkeit in `pyproject.toml` (Poetry/uv/pip-tools etc.).
|
||||
|
||||
---
|
||||
|
||||
### 6.2. Einfacher Aufruf
|
||||
|
||||
Linting:
|
||||
|
||||
```bash
|
||||
ruff check .
|
||||
```
|
||||
|
||||
Mit automatischen Fixes:
|
||||
|
||||
```bash
|
||||
ruff check . --fix
|
||||
```
|
||||
|
||||
Formatierung:
|
||||
|
||||
```bash
|
||||
ruff format .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.3. Minimale Konfiguration in `pyproject.toml`
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
target-version = "py311" # welche Python-Version ihr nutzt
|
||||
line-length = 88 # maximale Zeilenlänge
|
||||
exclude = ["venv", ".venv", "build", "dist"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "B", "UP"] # Regelgruppen: Errors, Flake, Imports, Bugbear, pyupgrade
|
||||
ignore = ["E501"] # Beispiel: lange Zeilen ignoriere ich (wenn ich black nutze)
|
||||
|
||||
[tool.ruff.format]
|
||||
# Beispiel: wenn ihr black-kompatible Einstellungen wollt
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
line-ending = "auto"
|
||||
```
|
||||
|
||||
- `select` gibt an, welche Regelgruppen aktiv sind.
|
||||
- `ignore` hebt einzelne Regelcodes auf (z. B. `E501` für Zeilenlänge).
|
||||
|
||||
---
|
||||
|
||||
### 6.4. Beispiel: Unbenutzte Variable und schlechtes `None`-Checking
|
||||
|
||||
Code:
|
||||
|
||||
```python
|
||||
def process(items):
|
||||
count = len(items)
|
||||
unused = 0
|
||||
|
||||
if items == None:
|
||||
print("No items")
|
||||
return count
|
||||
```
|
||||
|
||||
Ruff meldet u. a.:
|
||||
|
||||
- `F841` (unused variable `unused`)
|
||||
- `E711` (Vergleich mit `None` mit `==` statt `is`)
|
||||
|
||||
Mit `ruff check . --fix` könnte daraus werden:
|
||||
|
||||
```python
|
||||
def process(items):
|
||||
count = len(items)
|
||||
|
||||
if items is None:
|
||||
print("No items")
|
||||
return count
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.5. Beispiel: Imports aufräumen
|
||||
|
||||
Code:
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
|
||||
def get_data(url):
|
||||
r = requests.get(url)
|
||||
return r.json()
|
||||
```
|
||||
|
||||
Hier wird `os`, `sys` und `json` nicht genutzt.
|
||||
|
||||
Ruff meldet:
|
||||
|
||||
- `F401` (unused imports: os, sys, json)
|
||||
|
||||
Nach `ruff check . --fix`:
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
|
||||
def get_data(url):
|
||||
r = requests.get(url)
|
||||
return r.json()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.6. Beispiel: Einsatz mit pre-commit
|
||||
|
||||
Viele Teams nutzen [pre-commit](https://pre-commit.com/), damit Checks automatisch vor jedem Git-Commit laufen.
|
||||
|
||||
`.pre-commit-config.yaml`:
|
||||
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.5 # Beispielversion, ggf. aktualisieren
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
```
|
||||
|
||||
Damit werden:
|
||||
|
||||
- Linting + Auto-Fix vor jedem Commit ausgeführt,
|
||||
- Formatierung automatisch angewendet.
|
||||
|
||||
---
|
||||
|
||||
## 7. Zusammenfassung
|
||||
|
||||
- **Ruff** ist ein modernes, sehr schnelles Tool zur Verbesserung der Code-Qualität in Python.
|
||||
- Es kombiniert:
|
||||
- Linting (Fehler und Stil),
|
||||
- Formatierung,
|
||||
- Import-Sortierung,
|
||||
- viele Sicherheits- und Modernisierungsregeln.
|
||||
- Es ersetzt in vielen Projekten:
|
||||
- flake8 (+ viele Plugins),
|
||||
- isort,
|
||||
- teilweise black,
|
||||
- teilweise bandit.
|
||||
- Es löst Probleme wie:
|
||||
- lange Tool-Laufzeiten,
|
||||
- fragmentierte Konfiguration,
|
||||
- inkonsistente Codequalität.
|
||||
- Herausforderungen sind v. a.:
|
||||
- Auswahl und Anpassung der Regeln,
|
||||
- Umstieg von bestehenden Tools,
|
||||
- Umgang mit großen Mengen an Anfangs-Warnungen.
|
||||
|
||||
Wenn du möchtest, kann ich dir im nächsten Schritt eine **konkrete Beispielkonfiguration für ein kleines oder ein großes Projekt** vorschlagen (inkl. welche Regelgruppen sinnvoll sind und welche man am Anfang lieber auslässt).
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
`uv` ist ein neues, sehr schnelles All-in-one-Tool für das Python-Ökosystem, entwickelt von Astral (den Machern von `ruff`).
|
||||
Es ist im Kern ein **extrem schneller Paket- und Environment-Manager**, der mehrere bekannte Tools ersetzt bzw. bündelt:
|
||||
|
||||
- `pip` / `pip-tools` (Installation & Lockfiles)
|
||||
- `virtualenv` / `venv` (virtuelle Umgebungen)
|
||||
- `pyenv` (verschiedene Python-Versionen verwalten)
|
||||
- Teile von `poetry` / `pipx` (Projekt- & Tool-Management)
|
||||
|
||||
`pip install uv` installiert dabei nur einen Bootstrapper, der das eigentliche `uv`-Binary nachlädt.
|
||||
|
||||
---
|
||||
|
||||
## Wofür ist `uv` gut?
|
||||
|
||||
### 1. Pakete extrem schnell installieren
|
||||
`uv` ist in Rust geschrieben und deutlich schneller als `pip` oder `pip-tools` beim Auflösen und Installieren von Abhängigkeiten.
|
||||
|
||||
Beispiel (Drop-in für pip):
|
||||
|
||||
```bash
|
||||
uv pip install requests
|
||||
```
|
||||
|
||||
Auch mit `requirements.txt`:
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Virtuelle Umgebungen verwalten
|
||||
|
||||
```bash
|
||||
# Neue venv erstellen
|
||||
uv venv .venv
|
||||
|
||||
# venv aktivieren (z. B. in bash)
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Python-Versionen verwalten
|
||||
|
||||
Ähnlich wie `pyenv`:
|
||||
|
||||
```bash
|
||||
# Bestimmte Python-Version installieren
|
||||
uv python install 3.12
|
||||
|
||||
# Projekt mit bestimmter Version starten
|
||||
uv run --python 3.12 main.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Projekte initialisieren & reproduzierbare Builds
|
||||
|
||||
`uv` unterstützt `pyproject.toml` und Lockfiles (`uv.lock`) für reproduzierbare Umgebungen:
|
||||
|
||||
```bash
|
||||
# Neues Projekt anlegen
|
||||
uv init my-project
|
||||
|
||||
cd my-project
|
||||
uv sync # Installiert Abhängigkeiten gemäß pyproject + uv.lock
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Skripte/Tools direkt ausführen
|
||||
|
||||
Ähnlich wie `pipx`:
|
||||
|
||||
```bash
|
||||
uv run black .
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
`uv` kümmert sich im Hintergrund um die passende Umgebung und Abhängigkeiten.
|
||||
|
||||
---
|
||||
|
||||
## Kurzfassung
|
||||
|
||||
`uv` ist sinnvoll, wenn du:
|
||||
|
||||
- Python-Pakete **deutlich schneller** installieren willst,
|
||||
- **alles rund um Python-Umgebungen** (Packages, venvs, Python-Versionen, Tools) mit **einem** konsistenten Tool verwalten möchtest,
|
||||
- reproduzierbare Builds mit `pyproject.toml` und Lockfile brauchst,
|
||||
- eine moderne Alternative zu `pip`, `virtualenv`, `pyenv`, `pip-tools`, teilweise `poetry`/`pipx` suchst.
|
||||
|
||||
Wenn du aktuell `pip`, `virtualenv` & Co nutzt, kannst du mittelfristig sehr viel davon durch `uv` ersetzen.
|
||||
+469
@@ -0,0 +1,469 @@
|
||||
## 1. Grundsätzliche Definition: Was ist **uvicorn**?
|
||||
|
||||
**uvicorn** ist ein extrem performanter, asynchroner Web-Server für Python, der das **ASGI**‑Protokoll unterstützt.
|
||||
|
||||
- **ASGI** = *Asynchronous Server Gateway Interface*
|
||||
- uvicorn ist also das Bindeglied zwischen:
|
||||
- dem Web (HTTP, WebSockets)
|
||||
- und deiner Python‑Applikation (z.B. [[FastAPI]], Starlette, Django mit ASGI)
|
||||
|
||||
Uvicorn basiert intern auf sehr schnellen C‑Bibliotheken:
|
||||
- **uvloop** (schneller Event Loop, Ersatz für `asyncio`‑Loop)
|
||||
- **httptools** (schnelles HTTP‑Parsing)
|
||||
|
||||
Du verwendest uvicorn typischerweise, um eine ASGI‑App „zu starten“:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Wichtige Begriffe: ASGI, WSGI und Web-Frameworks
|
||||
|
||||
### 2.1 ASGI vs. WSGI
|
||||
|
||||
- **WSGI** (älterer Standard, z.B. für Django (klassisch), Flask):
|
||||
- synchron
|
||||
- kein natives WebSocket‑Support
|
||||
- typische Server: `gunicorn`, `uWSGI`, `mod_wsgi`
|
||||
|
||||
- **ASGI** (moderner Standard):
|
||||
- unterstützt **async/await**
|
||||
- kann **HTTP** und **WebSockets** und Background Tasks
|
||||
- typische Server: `uvicorn`, `hypercorn`, `daphne`
|
||||
|
||||
uvicorn ist also ein **ASGI-Server**, nicht WSGI.
|
||||
|
||||
### 2.2 uvicorn vs. Web-Frameworks ([[FastAPI]], Starlette, Django, Flask)
|
||||
|
||||
- **Framework** ([[FastAPI]], Starlette, Django, Flask):
|
||||
- definiert, wie du Routen, Views, Models, etc. schreibst.
|
||||
- kümmert sich um Request/Response‑Logik
|
||||
|
||||
- **uvicorn**:
|
||||
- kümmert sich um das Annehmen von Verbindungen, HTTP‑Parsing, Event‑Loop‑Handling.
|
||||
- ruft deine Applikation nur gemäß dem ASGI‑Protokoll auf.
|
||||
|
||||
Bildlich:
|
||||
**Browser** → (HTTP) → **uvicorn** → (ASGI) → **deine App** (z.B. [[FastAPI]])
|
||||
|
||||
---
|
||||
|
||||
## 3. Abgrenzung zu ähnlichen oder verwandten Begriffen
|
||||
|
||||
### 3.1 uvicorn vs. Gunicorn
|
||||
|
||||
- **gunicorn**:
|
||||
- klassischer **WSGI**-Server (für sync‑Apps wie Flask oder Django ohne ASGI).
|
||||
- kann aber mithilfe von Workern wie `uvicorn.workers.UvicornWorker` auch ASGI-App starten.
|
||||
|
||||
Beispiel: [[FastAPI]]‑App mit gunicorn + uvicorn worker:
|
||||
|
||||
```bash
|
||||
gunicorn -k uvicorn.workers.UvicornWorker main:app -b 0.0.0.0:8000
|
||||
```
|
||||
|
||||
Hier ist:
|
||||
- gunicorn = Prozessmanager und Worker‑Spawner
|
||||
- uvicorn = eigentlicher ASGI‑Server pro Worker
|
||||
|
||||
### 3.2 uvicorn vs. Hypercorn / Daphne
|
||||
|
||||
- **hypercorn**:
|
||||
- anderer ASGI‑Server (unterstützt z.B. HTTP/2, verschiedene Event Loops)
|
||||
- **daphne**:
|
||||
- ASGI‑Server aus dem Django‑Channels‑Ökosystem
|
||||
|
||||
Alle drei (uvicorn, hypercorn, daphne) machen im Kern das Gleiche:
|
||||
**ASGI‑Apps ausführen**, unterscheiden sich aber in Features, Performance und Konfigurationsmöglichkeiten.
|
||||
|
||||
### 3.3 uvicorn vs. „eingebauter Development-Server“
|
||||
|
||||
Viele Frameworks haben eingebaute Dev-Server, z.B.:
|
||||
|
||||
- Flask: `app.run(debug=True)`
|
||||
- Django: `python manage.py runserver`
|
||||
|
||||
Diese sind:
|
||||
- für **Entwicklung** gedacht
|
||||
- nicht für **Produktion** (Performance, Stabilität, Security)
|
||||
|
||||
uvicorn ist ein **richtiger** Webserver, der für **Produktion** geeignet ist (oft zusammen mit einem Reverse Proxy wie [[Nginx]]).
|
||||
|
||||
---
|
||||
|
||||
## 4. Welche Probleme löst uvicorn?
|
||||
|
||||
### 4.1 Asynchrone Web‑Backends performant betreiben
|
||||
|
||||
Mit ASGI kannst du:
|
||||
|
||||
- `async def` Endpoints schreiben
|
||||
- WebSockets nutzen
|
||||
- viele gleichzeitige Requests mit einem Event Loop bedienen
|
||||
|
||||
uvicorn ermöglicht dir, diese **asynchronen** Apps performant auszuliefern.
|
||||
|
||||
Praxisnahes Beispiel ([[FastAPI]]):
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from fastapi import FastAPI
|
||||
import asyncio
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: int):
|
||||
await asyncio.sleep(1) # simuliert eine I/O-Operation
|
||||
return {"item_id": item_id}
|
||||
```
|
||||
|
||||
Starten mit uvicorn:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
uvicorn kümmert sich darum, dass mehrere Requests gleichzeitig abgearbeitet werden können, während `asyncio.sleep` nicht blockiert.
|
||||
|
||||
### 4.2 WebSockets und Long-Lived Connections
|
||||
|
||||
ASGI (und damit uvicorn) unterstützt **WebSockets** nativ, was mit WSGI nicht geht.
|
||||
|
||||
Beispiel mit Starlette:
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from starlette.applications import Starlette
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.websockets import WebSocket
|
||||
from starlette.routing import Route, WebSocketRoute
|
||||
|
||||
async def homepage(request):
|
||||
return JSONResponse({"hello": "world"})
|
||||
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
await websocket.send_text("Willkommen!")
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
await websocket.send_text(f"Du hast gesendet: {data}")
|
||||
|
||||
routes = [
|
||||
Route("/", endpoint=homepage),
|
||||
WebSocketRoute("/ws", endpoint=websocket_endpoint),
|
||||
]
|
||||
|
||||
app = Starlette(routes=routes)
|
||||
```
|
||||
|
||||
Start:
|
||||
|
||||
```bash
|
||||
uvicorn main:app
|
||||
```
|
||||
|
||||
Mit WSGI wäre so ein WebSocket‑Endpoint nicht möglich.
|
||||
|
||||
### 4.3 Produktionstauglicher Server gegenüber Entwicklungsservern
|
||||
|
||||
- Stabilität bei hoher Last
|
||||
- Steuerung von:
|
||||
- Anzahl Worker‑Prozesse
|
||||
- Timeouts
|
||||
- Logging
|
||||
- Start via CLI, systemd, Docker, Kubernetes etc.
|
||||
|
||||
---
|
||||
|
||||
## 5. Grundlegende Verwendung von uvicorn
|
||||
|
||||
### 5.1 Installation
|
||||
|
||||
```bash
|
||||
pip install uvicorn
|
||||
# optional: schnellere Variante mit C-Extensions
|
||||
pip install "uvicorn[standard]"
|
||||
```
|
||||
|
||||
`[standard]` installiert u.a. `uvloop` und `httptools`.
|
||||
|
||||
### 5.2 Minimalbeispiel: Plain-ASGI-App
|
||||
|
||||
Du kannst eine ASGI‑App auch ohne Framework schreiben:
|
||||
|
||||
```python
|
||||
# app.py
|
||||
async def app(scope, receive, send):
|
||||
assert scope["type"] == "http"
|
||||
|
||||
# Request body lesen (vereinfachter Fall)
|
||||
await receive()
|
||||
|
||||
body = b"Hello, world"
|
||||
headers = [(b"content-type", b"text/plain")]
|
||||
|
||||
await send({
|
||||
"type": "http.response.start",
|
||||
"status": 200,
|
||||
"headers": headers,
|
||||
})
|
||||
await send({
|
||||
"type": "http.response.body",
|
||||
"body": body,
|
||||
})
|
||||
```
|
||||
|
||||
Starten:
|
||||
|
||||
```bash
|
||||
uvicorn app:app --reload
|
||||
```
|
||||
|
||||
Erklärung:
|
||||
- `app:app` = Modul `app.py`, Variable `app`
|
||||
- `--reload` = automatischer Neustart bei Codeänderung (nur dev)
|
||||
|
||||
### 5.3 Beispiel mit [[FastAPI]]
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Hello from uvicorn + FastAPI"}
|
||||
```
|
||||
|
||||
Start:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
Wichtige CLI‑Optionen:
|
||||
- `--reload`: Auto-Reload bei Codeänderungen (Dev)
|
||||
- `--host`: z.B. `0.0.0.0` um von außen erreichbar zu sein
|
||||
- `--port`: Port, z.B. `8000`
|
||||
- `--workers`: Anzahl der Prozesse (für Produktion)
|
||||
|
||||
### 5.4 Starten aus Python heraus
|
||||
|
||||
```python
|
||||
# run.py
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
)
|
||||
```
|
||||
|
||||
Start:
|
||||
|
||||
```bash
|
||||
python run.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Typische Konfigurationen und Szenarien
|
||||
|
||||
### 6.1 Entwicklung
|
||||
|
||||
- ein Worker
|
||||
- `--reload` aktiviert
|
||||
- Logging auf `debug`
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000 --log-level debug
|
||||
```
|
||||
|
||||
### 6.2 Produktion (einfach)
|
||||
|
||||
- mehrere Worker-Prozesse
|
||||
- kein `--reload`
|
||||
- Logging eher `info` oder `warning`
|
||||
|
||||
```bash
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 --log-level info
|
||||
```
|
||||
|
||||
Richtwert für Worker:
|
||||
`Anzahl CPU-Kerne * 2` (abhängig von App und Last; immer testen).
|
||||
|
||||
### 6.3 Produktion hinter einem Reverse Proxy (z.B. [[Nginx]])
|
||||
|
||||
Typischer Aufbau:
|
||||
|
||||
```
|
||||
Internet → Nginx (TLS, gzip, etc.) → uvicorn → FastAPI/Starlette/Django
|
||||
```
|
||||
|
||||
- [[Nginx]] übernimmt TLS/SSL, Load Balancing, Static Files
|
||||
- uvicorn macht die Application-Logik
|
||||
|
||||
[[Nginx]]-Konfig (stark vereinfacht) könnte so aussehen:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
```
|
||||
|
||||
uvicorn wird weiterhin wie oben gestartet.
|
||||
|
||||
---
|
||||
|
||||
## 7. Herausforderungen und typische Stolpersteine
|
||||
|
||||
### 7.1 Async/Synchron-Mix und Blockierungen
|
||||
|
||||
**Problem:**
|
||||
Du verwendest uvicorn (ASGI, async), aber in deinen Endpoints gibt es blockierende Operationen:
|
||||
|
||||
- große CPU‑Aufgaben
|
||||
- synchrones Warten auf externe APIs (z.B. `requests.get(...)`)
|
||||
- schwere Datenbank‑Queries, die nicht async sind
|
||||
|
||||
Beispiel:
|
||||
|
||||
```python
|
||||
@app.get("/slow")
|
||||
async def slow():
|
||||
import time
|
||||
time.sleep(5) # BLOCKIERT den Event-Loop
|
||||
return {"status": "ok"}
|
||||
```
|
||||
|
||||
Folge:
|
||||
- Ein Request blockiert den Event Loop → alle anderen Requests warten mit.
|
||||
|
||||
Lösungen:
|
||||
- I/O: async Libraries benutzen (z.B. `httpx` statt `requests`, `asyncpg` statt sync‑DB‑Client)
|
||||
- CPU‑lastig: in Thread‑Pool oder Process‑Pool auslagern (`run_in_threadpool` etc.)
|
||||
|
||||
### 7.2 Gemeinsamer Zustand über Worker-Prozesse
|
||||
|
||||
Wenn du `--workers > 1` nutzt, hast du **mehrere Prozesse**.
|
||||
Globaler Zustand in Python wird **nicht** zwischen Prozessen geteilt.
|
||||
|
||||
Beispiel (Problem):
|
||||
|
||||
```python
|
||||
counter = 0
|
||||
|
||||
@app.get("/count")
|
||||
def count():
|
||||
global counter
|
||||
counter += 1
|
||||
return {"counter": counter}
|
||||
```
|
||||
|
||||
Mit mehreren Workern:
|
||||
- jeder Worker hat seinen eigenen `counter`
|
||||
- Ergebnisse sind inkonsistent
|
||||
|
||||
Lösung:
|
||||
- geteilten Zustand über externe Systeme (Redis, Datenbank, etc.)
|
||||
- oder nur einen Worker nutzen, wenn globaler In-Memory-State unvermeidbar ist (aber meist unsauber).
|
||||
|
||||
### 7.3 Datenbankverbindungen und Lebenszyklus
|
||||
|
||||
uvicorn unterstützt ASGI‑`lifespan`‑Events (startup/shutdown).
|
||||
Frameworks wie [[FastAPI]]/Starlette nutzen das, um z.B. DB‑Connections zu öffnen/schließen.
|
||||
|
||||
Stolpersteine:
|
||||
- Verbindungspools pro Worker korrekt initialisieren
|
||||
- bei Shutdown sauber schließen
|
||||
- nicht „pro Request“ neue Connections aufmachen
|
||||
|
||||
Beispiel mit [[FastAPI]] (vereinfacht):
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
db = None
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
global db
|
||||
db = await some_async_db_connect()
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
await db.close()
|
||||
```
|
||||
|
||||
### 7.4 Logging und Error-Handling
|
||||
|
||||
uvicorn hat eigenes Logging; dein Framework ebenso.
|
||||
Typische Themen:
|
||||
|
||||
- Log-Format in Produktion standardisieren
|
||||
- Fehler-Logs im Zusammenspiel mit Reverse Proxy
|
||||
- Ausführliche Logs in Dev, weniger in Prod
|
||||
|
||||
Beispiel (JSON-Logging in Produktion, nur angedeutet):
|
||||
|
||||
```bash
|
||||
uvicorn main:app \
|
||||
--host 0.0.0.0 \
|
||||
--port 8000 \
|
||||
--log-config logging_config.yaml
|
||||
```
|
||||
|
||||
In `logging_config.yaml` kannst du detailliert das Logging steuern.
|
||||
|
||||
### 7.5 Plattformunterschiede (Windows vs. Linux)
|
||||
|
||||
- `--reload` nutzt File-Watcher und Signale → unter Linux sehr stabil; unter Windows kann es ein paar Besonderheiten geben.
|
||||
- In Produktion läuft uvicorn meist auf Linux‑Servern oder in Docker‑Containern.
|
||||
|
||||
---
|
||||
|
||||
## 8. Kurze Checkliste für den praktischen Einstieg
|
||||
|
||||
1. **Framework wählen**
|
||||
- [[FastAPI]] oder Starlette, wenn du intensiv async nutzen willst.
|
||||
1. **App schreiben**
|
||||
- `app = FastAPI()`, Endpoints definieren.
|
||||
3. **In Entwicklung starten**
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
4. **Vor Produktion**
|
||||
- Blocking‑Code prüfen (CPU, I/O)
|
||||
- Datenbankzugriff sauber konfigurieren (Pools, async‑Client)
|
||||
- Logging und Error‑Handling aufräumen
|
||||
5. **In Produktion starten** (einfach)
|
||||
```bash
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||
```
|
||||
6. **Optional**:
|
||||
- vor uvicorn einen [[Nginx]] oder Traefik setzen (TLS, Load Balancing)
|
||||
|
||||
---
|
||||
|
||||
## 9. Zusammenfassung
|
||||
|
||||
- **uvicorn** ist ein **ASGI‑Webserver** für Python, optimiert für **asynchrone** Web‑Apps.
|
||||
- Er ist **kein Web‑Framework**, sondern die Laufzeitumgebung für Frameworks wie **[[FastAPI]]**, **Starlette** oder moderne **Django**‑Konfigurationen.
|
||||
- Es löst die Probleme klassischer WSGI‑Server in Bezug auf **Async**, **WebSockets** und **Performance**.
|
||||
- Typische Herausforderungen liegen im Bereich:
|
||||
- korrektes Async‑Design
|
||||
- Umgang mit mehreren Workern und gemeinsamem Zustand
|
||||
- saubere Integration von Datenbanken, Logging, Deployment
|
||||
- Für dich als Python‑Entwickler ist uvicorn im Alltag vor allem:
|
||||
**das Kommando, mit dem du deine moderne Web‑API startest.**
|
||||
|
||||
Reference in New Issue
Block a user