Willkommen zur Python-Referenz
Eine umfassende, deutschsprachige Referenz für Python-Entwickler aller Erfahrungsstufen
Über diese Referenz
Diese Referenz ist entstanden aus dem Bedürfnis, praktisches Python-Wissen strukturiert und auf Deutsch verfügbar zu machen. Sie ist weder ein klassisches Tutorial noch eine reine API-Dokumentation, sondern etwas dazwischen: eine Sammlung von Konzepten, Best Practices und Beispielen, die helfen soll, sich Wissen schnell anzueignen bzw. es wieder aufzufrischen.
Für wen ist diese Referenz?
🎓 Anfänger:
Für Programmierer, die bereits Erfahrungen in einer anderen Sprache haben und mit den Grundkonzepten der Programmierung vertraut sind, bietet Teil I (Grundlagen) einen schnellen, strukturierten Einstieg in Python-Syntax, Datentypen und Kontrollstrukturen.
💼 Erfahrene Entwickler:
Entwickler, die bereits mit Python programmieren, finden in den Teilen II bis IV fortgeschrittene Themen wie OOP-Design-Patterns, Performance-Optimierung, Testing und moderne Entwicklungs-Workflows.
Die Teile III bis V zeigen, wie man sauberen, getesteten und performanten Code für produktionsreife Anwendungen schreibt und modernen Tools wie Ruff, pytest, Poetry und PyO3 nutzt.
Einen Schnellstart in Data Science bietet Teil VI, welcher NumPy, Pandas und Matplotlib/Seaborn behandelt.
Philosophie dieser Referenz
✅ Lernressource und Nachschlagewerk:
Diese Referenz ist gleichermaßen geeignet, sie sequentiell zum Lernen zu verwenden und sie über die durchdachte Struktur oder die Suchfunktion (Tastenkürzel: s) als Nachschlagewerk zu nutzen.
✅ Praktisch, nicht theoretisch:
Jedes Konzept wird mit funktionierenden Code-Beispielen erklärt. Kein unnötiger Ballast – nur das, was man wirklich braucht.
✅ Modern und aktuell:
Diese Referenz deckt Python 3.11+ ab und nutzt moderne Tools und Best Practices. Sie wird laufend aktualisiert.
✅ Deutsch mit englischem Code:
Erklärungen auf Deutsch (weil komplexe Konzepte in der Muttersprache einfacher zu verstehen sind), aber Code mit englischen Bezeichnern (wie in der professionellen Praxis üblich).
✅ Best Practices, statt “es funktioniert”:
An vielen Stellen wird nicht nur erklärt, wie etwas funktioniert, sondern auch warum und wann man es einsetzen sollte (und wann besser nicht).
Was diese Referenz NICHT ist
❌ Nicht für absolute Programmier-Anfänger: Grundlegende Programmierkenntnisse (Variablen, Schleifen, Funktionen) werden vorausgesetzt.
❌ Keine API-Dokumentation: Eine detaillierte API-Referenz bietet die offizielle Python-Dokumentation.
Links zu Online-Ressourcen
Python.org – Python Software Foundation (englisch)
- The Python Tutorial – Offizielles Tutorial für Einsteiger, deckt Grundlagen bis fortgeschrittene Konzepte ab
- The Python Language Reference – Vollständige Sprachspezifikation, Syntax und Semantik von Python
- The Python Standard Library – Referenz aller eingebauten Module und Funktionen
- PEP-8: Style Guide for Python Code – Offizieller Style-Guide für konsistenten, lesbaren Python-Code
Deutschsprachige Ressourcen
Tutorials & Kurse
- Python Lernen – Umfangreiches deutsches Tutorial von Grundlagen bis OOP
- Programmieren lernen mit Python – Strukturiertes Tutorial für Anfänger
- Python 3 Das umfassende Handbuch (Rheinwerk) – Kostenloses Online-Buch von Rheinwerk
- Python Kurs – Ausführliche Tutorials mit praktischen Beispielen
Datentypen
1 Immutable (Unveränderlich)
| Datentyp | Beschreibung | Wertebereich |
|---|---|---|
int | Ganze Zahlen | Theoretisch unbegrenzt (abhängig vom Speicher) |
float | Gleitkommazahlen | Ca. ±1.8 × 10³⁰⁸ (IEEE 754, 64-Bit) |
complex | Komplexe Zahlen | Kombination aus zwei float-Werten (Real- und Imaginärteil) |
bool | Wahrheitswerte | {True, False} |
str | Zeichenketten | Beliebige Zeichenfolgen (Unicode) |
tuple | Tupel (unveränderlich) | Beliebige Anzahl von Elementen unterschiedlicher Typen |
frozenset | Unveränderliche Menge | Ungeordnete, nicht doppelte Elemente beliebiger immutable Typen |
bytes | Byte-Sequenz | Folge von Bytes (0–255) |
NoneType | Repräsentiert “kein Wert” | {None} |
2 Mutable (Veränderlich)
| Datentyp | Beschreibung | Wertebereich |
|---|---|---|
list | Liste (Array in anderen Sprachen) | Beliebige Anzahl von Elementen unterschiedlicher Typen |
dict | Wörterbuch (Key-Value-Paare) | Schlüssel: Immutable Typen, Werte: Beliebige Typen |
set | Menge (keine doppelten Werte) | Ungeordnete, nicht doppelte Elemente beliebiger immutable Typen |
bytearray | Veränderbare Byte-Sequenz | Folge von Bytes (0–255) |
3 Strings
3.1 Allgemein
s = "Hallo"
s = 'Hallo'
s = """Hallo
Welt"""
s = ('Hallo '
'Welt')
3.2 f-Strings
s = 'Welt'
print(f'Hallo {s}') # Hallo Welt
f-Strings sind Konkatenationen vorzuziehen, da sie sich schneller schreiben lassen und besser zu lesen sind:
first_name = 'Max'
last_name = 'Mustermann'
print('Hallo ' + first_name + ' ' + last_name)
print(f'Hallo {first_name} {last_name}')
3.3 Teilstrings
s = 'Hallo'
print(s[0]) # Ausgabe: H
print(s[-1]) # Ausgabe: o
print(s[2:]) # Ausgabe: llo
print(s[:2]) # Ausgabe: Ha
print(s[2:4]) # Ausgabe: ll
3.4 String zu Liste
s = 'Hallo Welt'
l = s.split(' ')
print(l) # ['Hallo', 'Welt']
3.5 Ersetzungen
s = 'Hallo+Welt'
print(s.replace('+', ' ')) # Hallo Welt
3.6 Raw-Strings
s = 'c:\\mein\\pfad'
s_raw = r'c:\mein\pfad' # raw string
print(s, s_raw) # c:\mein\pfad c:\mein\pfad
3.7 Pfade
from pathlib import Path
str = '/user/somestuff/my_file.txt'
p = Path(str)
print(p.absolute()) # /user/somestuff/my_file.txt
print(p.parent) # /user/somestuff
print(p.stem) # my_file
print(p.is_dir()) # False
print(p.is_file()) # True
4 Zahlen
4.1 Integer
Bei der Division entsteht ein float, auch wenn das Ergebnis gerade ist. Vermeiden kann man dies mit dem //-Operator.
result = 4 / 2
print(f'{result}; Typ: {type(result)}') # 2.0; Typ: <class 'float'>
result = 4 // 2
print(f'{result}; Typ: {type(result)}') # 2; Typ: <class 'int'>
Formatierung:
n1 = 1000
n2 = 1
print(n1) # 1000
print(f'{n2:4d}') # 1
4.2 Floats
4.3 Vergleich
danger
Die Nachkommastellen werden nicht exakt abgespeichert, daher sollten float-Werte niemals mit == vergleichen werden. Stattdessen eignet sich die Verwendung von math.isclose().
Beispiel:
import math
result = 1/10 + 1/10 + 1/10
value = 0.3
print(f'{result:.32f}') # 0.30000000000000004440892098500626
print(f'{value:.32f}') # 0.29999999999999998889776975374843
print(result == value) # False
print(math.isclose(result, value)) # True
4.4 Runden
Aufrunden:
print(math.floor(1.2)) # 1
Abrunden:
print(math.ceil(1.2)) # 2
Auf 2 Nachkommastellen runden:
print(round(1.23456, 2)) # 1.23
Auf das nächstgelegene Vielfache von 10 (= 10^1) runden:
print(round(644.123, -1)) # 640.0
Auf das nächstgelegene Vielfache von 100 (= 10^2) runden:
print(round(644.123, -2)) # 600.0
4.5 Potenzen
danger
a^b ist falsch, da ^ der XOR-Operator ist!
import math
a = 2
b = 3
# Es gibt 2 Möglichkeiten:
print(a ** b) # 8
print(math.pow(a, b)) # 8.0
Datenstrukturen (Collections)
1 Listen
1.1 Zugriff auf Elemente, Teillisten
L = ['Null', 'Eins', 'Zwei', 'Drei']
# Anzahl der Elemente
print(len(L)) # 4
# Element 1
print(L[1]) # Eins
# Element 0-2
print(L[0:2]) # ['Null', 'Eins']
# Die letzten beiden Elemente
print(L[-2:]) # ['Zwei', 'Drei']
1.2 Elemente hinzufügen und löschen
L = [2, 4, 6, 8]
L.append(10) # Der Wert 10 wird hinzugefügt
print(L) # [2, 4, 6, 8, 10]
L.remove(4) # Der Wert 4 wird entfernt (NICHT Element 4)
print(L) # [2, 6, 8, 10]
del L[0:2] # Element 0-1 werden gelöscht, kein Rückgabewert
print(L) # [8, 10]
del L[1] # Element 1 wird gelöscht, kein Rückgabewert
print(L) # [8]
# Element 0 wird entfernt und zurückgegeben
print(L.pop(0)) # 8
1.3 Listen kombinieren
L1 = [1, 2, 3]
L2 = [4, 5, 6]
# Möglichkeit 1
print(L1 + L2) # [1, 2, 3, 4, 5, 6]
L1.extend(L2)
# Möglichkeit 2
print(L1) # [1, 2, 3, 4, 5, 6]
Prüfen, ob Wert in Liste ist:
L = ["A", "B", "C"]
x = "B"
print(x in L) # True
1.4 List comprehensions (kompakte Erstellung von Listen)
Beispiel 1:
numbers = []
for i in range(5):
numbers.append(i**2)
print(numbers) # [0, 1, 4, 9, 16]
# List comprehension:
numbers = [i**2 for i in range(5)]
print(numbers) # [0, 1, 4, 9, 16]
Beispiel 2:
L2 = []
for i in range(0, len(L)):
if L[i] > 1:
L2.append(L[i]*2)
print(L2) # [4, 6]
# List comprehension:
L = [1, 2, 3]
print([x*2 for x in L if x > 1]) # [4, 6]
List comprehension sind in einigen Fällen besser lesbar, können jedoch auch schnell unübersichtlich werden. Sie sind auch mit if-Bedingungen möglich.
Beispiel 1:
numbers = [i*-1 if i<3 else -1 for i in range(5)]
print(numbers) # [0, -1, -2, -1, -1]
Beispiel 2:
numbers = [i+1 for i in range(5) if i != 3]
print(numbers) # [1, 2, 3, 5]
Beim ersten Beispiel steht die if-Bedingung am Anfang, dadurch wird festgelegt, was abgespeichert wird. Beim zweiten Beispiel, bei der die if-Bedingung am Ende steht, wird festgelegt, ob überhaupt etwas abgespeichert werden soll.
1.5 Kopieren von Listen
danger
Das Kopieren einer Liste über Variablenzuweisen (z. B. l2 = l1) führt häufig zu einem nicht gewollten Verhalten, da das Ändern von l2 auch die Änderung von l1 mit sich bringt.
Beispiel:
l1 = [1, 2]
l2 = l1
l2[0] = 3
print(l1) # [3, 2]
print(l2) # [3, 2]
Dies liegt daran, dass auf diese Weite l1 und l2 auf die selbe Speicheradresse verweisen:
l1 = [1, 2]
l2 = l1
print(id(l1)) # 1957190476864
print(id(l2)) # 1957190476864
Um das zu umgehen, kopiert man Listen wie folgt:
l1 = [1, 2]
l2 = l1.copy()
l2[0] = 3
print(l1) # [1, 2]
print(l2) # [3, 2]
warning
Dies ist eine sog. shallow-copy. Bei mehrdimensionalen Listen funktioniert dies jedoch nicht! Es muss eine sog. deep-copy erstellt werden.
Beispiel für shallow-copy:
import copy
l1 = [[1, 2], [3, 4]]
l2 = l1.copy() # oder l2 = copy.copy(l1)
l2[0][0] = 5
print(l1) # [[5, 2], [3, 4]]
print(l2) # [[5, 2], [3, 4]]
print(id(l1)) # 1957176932224
print(id(l2)) # 1957176994368
print(id(l1[0][0])) # 1956557750640
print(id(l2[0][0])) # 1956557750640
Beispiel für deep-copy:
import copy
l1 = [[1, 2], [3, 4]]
l2 = copy.deepcopy(l1)
l2[0][0] = 5
print(l1) # [[1, 2], [3, 4]]
print(l2) # [[5, 2], [3, 4]]
print(id(l1)) # 1957190472320
print(id(l2)) # 1957190477184
print(id(l1[0][0])) # 1956557750512
print(id(l2[0][0])) # 1956557750640
Mehr zu Listen unter: https://www.programiz.com/python-programming/methods/list
2 Arrays
Arrays sind ähnlich wie Listen, jedoch effizienter bei numerischen Operationen, insbesondere für große Datenmengen. In Python können Arrays mit dem array-Modul oder über NumPy verwendet werden.
import array
# Ein Array mit ganzen Zahlen (Typcode 'i' für Integer)
arr = array.array('i', [1, 2, 3, 4, 5])
print(arr) # array('i', [1, 2, 3, 4, 5])
# Elementzugriff
print(arr[1]) # 2
# Element hinzufügen
arr.append(6)
print(arr) # array('i', [1, 2, 3, 4, 5, 6])
# Element entfernen
arr.remove(3)
print(arr) # array('i', [1, 2, 4, 5, 6])
3 Tupel
Tupel sind wie Listen, jedoch mit dem Unterschied, dass der Inhalt des Tupels nicht veränderbar ist.
t = 1, 2, 3
t = (1, 2, 3)
print(t[0]) # 1
3.1 Tuple unpacking
t = (1, 2, 3)
val1, val2, val3 = t
print(val1, val2, val3) # 1 2 3
val1, _, val3 = t
print(val1, val3) # 1 3
val1, *t2 = t
print(val1, t2) # 1 [2, 3]
4 Dictionaries
Dictionaries werden benutzt, um Daten in key-value-Paaren zu speichern.
Erstellung von Dictionaries:
# Möglichkeit 1:
baujahr = {'Ford': 2019, 'Honda': 2013}
print(baujahr) # {'Ford': 2019, 'Honda': 2013}
# Möglichkeit 2:
baujahr = dict(Ford = 2019, Honda = 2013)
print(baujahr) # {'Ford': 2019, 'Honda': 2013}
# Möglichkeit 3: Schlüssel werden als Tupel übergeben)
baujahr = dict.fromkeys( ('Ford', 'Honda') )
baujahr['Ford'] = 2019
baujahr['Honda'] = 2013
print(baujahr) # {'Ford': 2019, 'Honda': 2013}
Zugriff auf die Schlüssel und Werte von Dictionaries in Schleifen:
D = {'Ford': 2019, 'Honda': 2013}
print('Schlüssel des Dictionaries:')
for key in D:
print(key)
# Ford
# Honda
print('\nWerte des Dictionaries:')
for value in D.values():
print(value)
# 2019
# 2013
print('\nSchlüssel und Werte des Dictionaries:')
for key, value in D.items():
print(f'{key}: {value}')
# Ford: 2019
# Honda: 2013
5 Sets bzw. Mengen
Siehe auch operatoren.
Operationen:
S1 = {'Banane', 'Paprika', 'Zitrone'}
S2 = {'Apfel', 'Banane', 'Birne', 'Gurke', 'Paprika'}
# Vereinigung
print(S1 | S2) # {'Zitrone', 'Gurke', 'Birne', 'Paprika', 'Apfel', 'Banane'}
# Schnittmenge
print(S1 & S2) # {'Paprika', 'Banane'}
# Differenz (S1 ohne S2)
print(S1 - S2) # {'Zitrone'}
# Symmetrische Differenz (= Vereinigung ohne Schnittmenge)
print(S1 ^ S2) # {'Apfel', 'Zitrone', 'Gurke', 'Birne'}
print('\nAlternativen:')
# Vereinigung
print(S1.union(S2)) # {'Zitrone', 'Gurke', 'Birne', 'Paprika', 'Apfel', 'Banane'}
# Schnittmenge
print(S1.intersection(S2)) # {'Paprika', 'Banane'}
# Differenz (S1 ohne S2)
print(S1.difference(S2)) # {'Zitrone'}
# Symmetrische Differenz (= Vereinigung ohne Schnittmenge)
print(S1.symmetric_difference(S2)) # {'Apfel', 'Zitrone', 'Gurke', 'Birne'}
Mehr zu Datentypen: https://docs.python.org/3/library/stdtypes.html
6 Das collections-Modul
Das collections-Modul bietet spezialisierte Container-Datentypen, die über die eingebauten Listen, Tupel, Dictionaries und Sets hinausgehen.
6.1 Counter – Zählen von Elementen
Counter ist ein Dictionary-Subtyp zum Zählen von hashbaren Objekten.
6.1.1 Grundlegende Verwendung
from collections import Counter
# Aus Liste erstellen
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
counter = Counter(words)
print(counter) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
# Aus String erstellen (zählt Zeichen)
text = "mississippi"
counter = Counter(text)
print(counter) # Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
# Direkte Initialisierung
counter = Counter(a=3, b=2, c=1)
print(counter) # Counter({'a': 3, 'b': 2, 'c': 1})
6.1.2 Häufigste Elemente
from collections import Counter
votes = ['Alice', 'Bob', 'Alice', 'Charlie', 'Bob', 'Alice']
counter = Counter(votes)
# Häufigste n Elemente
print(counter.most_common(2)) # [('Alice', 3), ('Bob', 2)]
# Alle Elemente nach Häufigkeit
print(counter.most_common()) # [('Alice', 3), ('Bob', 2), ('Charlie', 1)]
6.1.3 Counter-Operationen
from collections import Counter
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
# Addition
print(c1 + c2) # Counter({'a': 4, 'b': 3})
# Subtraktion (negative Werte werden entfernt)
print(c1 - c2) # Counter({'a': 2})
# Vereinigung (Maximum)
print(c1 | c2) # Counter({'a': 3, 'b': 2})
# Schnittmenge (Minimum)
print(c1 & c2) # Counter({'a': 1, 'b': 1})
6.1.4 Praktische Beispiele
Wortfrequenz-Analyse:
from collections import Counter
text = """Python ist eine wunderbare Programmiersprache.
Python macht Spaß und Python ist mächtig."""
words = text.lower().split()
word_count = Counter(words)
print(word_count.most_common(3))
# [('python', 3), ('ist', 2), ('eine', 1)]
Duplikate finden:
from collections import Counter
data = [1, 2, 3, 2, 4, 5, 3, 6, 7, 3]
counter = Counter(data)
# Elemente, die mehr als einmal vorkommen
duplicates = [item for item, count in counter.items() if count > 1]
print(duplicates) # [2, 3]
6.2 defaultdict – Dictionary mit Standardwerten
defaultdict erstellt automatisch Standardwerte für fehlende Schlüssel.
6.2.1 Grundkonzept
from collections import defaultdict
# Mit list als Factory
d = defaultdict(list)
d['fruits'].append('apple')
d['fruits'].append('banana')
d['vegetables'].append('carrot')
print(d) # defaultdict(<class 'list'>, {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']})
# Mit int als Factory (Standardwert 0)
counter = defaultdict(int)
for word in ['a', 'b', 'a', 'c', 'b', 'a']:
counter[word] += 1
print(counter) # defaultdict(<class 'int'>, {'a': 3, 'b': 2, 'c': 1})
6.2.2 Verschiedene Factory-Funktionen
from collections import defaultdict
# list - leere Liste
d1 = defaultdict(list)
print(d1['key']) # []
# int - 0
d2 = defaultdict(int)
print(d2['key']) # 0
# str - leerer String
d3 = defaultdict(str)
print(d3['key']) # ''
# set - leeres Set
d4 = defaultdict(set)
print(d4['key']) # set()
# Lambda für custom Defaults
d5 = defaultdict(lambda: 'N/A')
print(d5['key']) # 'N/A'
6.2.3 Vergleich: dict vs. defaultdict
# Normales dict
d = {}
# d['key'].append('value') # KeyError!
# Mit setdefault (umständlich)
d.setdefault('key', []).append('value')
# Mit defaultdict (elegant)
from collections import defaultdict
d = defaultdict(list)
d['key'].append('value') # Funktioniert!
6.2.4 Praktische Beispiele
Gruppieren nach Schlüssel:
from collections import defaultdict
students = [
('Alice', 'Math'),
('Bob', 'Physics'),
('Charlie', 'Math'),
('Diana', 'Physics'),
('Eve', 'Math')
]
by_subject = defaultdict(list)
for name, subject in students:
by_subject[subject].append(name)
print(dict(by_subject))
# {'Math': ['Alice', 'Charlie', 'Eve'], 'Physics': ['Bob', 'Diana']}
Verschachtelte defaultdict:
from collections import defaultdict
# Zweistufiges defaultdict
tree = lambda: defaultdict(tree)
users = tree()
users['john']['age'] = 30
users['john']['city'] = 'New York'
users['alice']['age'] = 25
print(dict(users))
# {'john': {'age': 30, 'city': 'New York'}, 'alice': {'age': 25}}
6.3 deque – Double-Ended Queue
deque (ausgesprochen “deck”) ist eine Liste, die für schnelle Zugriffe an beiden Enden optimiert ist.
6.3.1 Grundoperationen
from collections import deque
# Erstellen
d = deque([1, 2, 3])
print(d) # deque([1, 2, 3])
# Am Ende hinzufügen/entfernen (wie Liste)
d.append(4)
print(d) # deque([1, 2, 3, 4])
last = d.pop()
print(last, d) # 4 deque([1, 2, 3])
# Am Anfang hinzufügen/entfernen (O(1) statt O(n))
d.appendleft(0)
print(d) # deque([0, 1, 2, 3])
first = d.popleft()
print(first, d) # 0 deque([1, 2, 3])
6.3.2 Rotation und Erweiterung
from collections import deque
d = deque([1, 2, 3, 4, 5])
# Rotation nach rechts
d.rotate(2)
print(d) # deque([4, 5, 1, 2, 3])
# Rotation nach links
d.rotate(-2)
print(d) # deque([1, 2, 3, 4, 5])
# Mehrere Elemente hinzufügen
d.extend([6, 7])
print(d) # deque([1, 2, 3, 4, 5, 6, 7])
d.extendleft([0, -1])
print(d) # deque([-1, 0, 1, 2, 3, 4, 5, 6, 7])
6.3.3 Maximale Länge (Ringbuffer)
from collections import deque
# Deque mit maximaler Länge
d = deque(maxlen=3)
d.append(1)
d.append(2)
d.append(3)
print(d) # deque([1, 2, 3], maxlen=3)
d.append(4) # Ältestes Element (1) wird automatisch entfernt
print(d) # deque([2, 3, 4], maxlen=3)
6.3.4 Performance-Vergleich: list vs. deque
from collections import deque
import time
# Liste
data_list = []
start = time.time()
for i in range(100000):
data_list.insert(0, i) # O(n) - langsam!
print(f"List insert(0): {time.time() - start:.3f}s")
# Deque
data_deque = deque()
start = time.time()
for i in range(100000):
data_deque.appendleft(i) # O(1) - schnell!
print(f"Deque appendleft: {time.time() - start:.3f}s")
Typisches Ergebnis:
- List insert(0): ~3.5s
- Deque appendleft: ~0.01s
6.3.5 Praktische Beispiele
FIFO-Queue (First In, First Out):
from collections import deque
queue = deque()
# Elemente hinzufügen
queue.append('Task 1')
queue.append('Task 2')
queue.append('Task 3')
# Elemente verarbeiten (FIFO)
while queue:
task = queue.popleft()
print(f"Processing: {task}")
Sliding Window:
from collections import deque
def moving_average(values, window_size):
"""Berechnet gleitenden Durchschnitt"""
window = deque(maxlen=window_size)
averages = []
for value in values:
window.append(value)
averages.append(sum(window) / len(window))
return averages
data = [10, 20, 30, 40, 50, 60]
result = moving_average(data, window_size=3)
print(result) # [10.0, 15.0, 20.0, 30.0, 40.0, 50.0]
Browser History (Back/Forward):
from collections import deque
class BrowserHistory:
def __init__(self):
self.back_stack = deque()
self.forward_stack = deque()
self.current = None
def visit(self, url):
if self.current:
self.back_stack.append(self.current)
self.current = url
self.forward_stack.clear()
def back(self):
if self.back_stack:
self.forward_stack.append(self.current)
self.current = self.back_stack.pop()
return self.current
def forward(self):
if self.forward_stack:
self.back_stack.append(self.current)
self.current = self.forward_stack.pop()
return self.current
# Verwendung
browser = BrowserHistory()
browser.visit('google.com')
browser.visit('python.org')
browser.visit('github.com')
print(browser.back()) # python.org
print(browser.back()) # google.com
print(browser.forward()) # python.org
6.4 ChainMap – Mehrere Dictionaries verketten
ChainMap gruppiert mehrere Dictionaries zu einem einzigen View.
6.4.1 Grundkonzept
from collections import ChainMap
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
dict3 = {'c': 5, 'd': 6}
# ChainMap erstellen
chain = ChainMap(dict1, dict2, dict3)
print(chain['a']) # 1 (aus dict1)
print(chain['b']) # 2 (aus dict1, nicht dict2!)
print(chain['c']) # 4 (aus dict2, nicht dict3!)
print(chain['d']) # 6 (aus dict3)
Wichtig: Bei Lookup wird das erste Dictionary mit dem Schlüssel verwendet.
6.4.2 Modifikationen
from collections import ChainMap
user_config = {'theme': 'dark', 'font_size': 12}
default_config = {'theme': 'light', 'font_size': 14, 'auto_save': True}
config = ChainMap(user_config, default_config)
print(config['theme']) # 'dark' (aus user_config)
print(config['auto_save']) # True (aus default_config)
# Änderungen gehen ins ERSTE Dictionary
config['theme'] = 'blue'
print(user_config) # {'theme': 'blue', 'font_size': 12}
print(default_config) # Unverändert
# Neuer Schlüssel wird auch ins ERSTE Dictionary eingefügt
config['new_key'] = 'value'
print(user_config) # {'theme': 'blue', 'font_size': 12, 'new_key': 'value'}
6.4.3 Methoden
from collections import ChainMap
dict1 = {'a': 1}
dict2 = {'b': 2}
chain = ChainMap(dict1, dict2)
# Neues Dictionary vorne hinzufügen
chain = chain.new_child({'c': 3})
print(chain) # ChainMap({'c': 3}, {'a': 1}, {'b': 2})
# Erstes Dictionary entfernen
chain = chain.parents
print(chain) # ChainMap({'a': 1}, {'b': 2})
# Alle Maps anzeigen
print(chain.maps) # [{'a': 1}, {'b': 2}]
6.4.4 Praktische Beispiele
Konfiguration mit Fallbacks:
from collections import ChainMap
import os
# Priorität: CLI-Args > Env-Vars > Config-File > Defaults
defaults = {
'host': 'localhost',
'port': 8000,
'debug': False
}
config_file = {
'host': '0.0.0.0',
'port': 3000
}
env_vars = {
k.lower().replace('app_', ''): v
for k, v in os.environ.items()
if k.startswith('APP_')
}
cli_args = {
'debug': True
}
config = ChainMap(cli_args, env_vars, config_file, defaults)
print(config['host']) # '0.0.0.0' (aus config_file)
print(config['debug']) # True (aus cli_args)
print(config['port']) # 3000 (aus config_file)
Scope-Management (z.B. für Interpreter):
from collections import ChainMap
class Scope:
def __init__(self):
self.scopes = ChainMap({})
def push_scope(self):
"""Neuer Scope (z.B. neue Funktion)"""
self.scopes = self.scopes.new_child()
def pop_scope(self):
"""Scope verlassen"""
self.scopes = self.scopes.parents
def set(self, name, value):
"""Variable im aktuellen Scope setzen"""
self.scopes[name] = value
def get(self, name):
"""Variable nachschlagen (mit Fallback zu äußeren Scopes)"""
return self.scopes.get(name)
# Verwendung
scope = Scope()
scope.set('x', 10) # Global: x=10
scope.push_scope() # Neue Funktion
scope.set('x', 20) # Lokal: x=20
print(scope.get('x')) # 20
scope.pop_scope() # Funktion verlassen
print(scope.get('x')) # 10 (global wieder sichtbar)
6.5 namedtuple – Tupel mit benannten Feldern
namedtuple erstellt Tuple-Subklassen mit benannten Feldern.
6.5.1 Grundlegende Verwendung
from collections import namedtuple
# namedtuple-Klasse definieren
Point = namedtuple('Point', ['x', 'y'])
# Instanzen erstellen
p1 = Point(10, 20)
p2 = Point(x=30, y=40)
# Zugriff per Name oder Index
print(p1.x) # 10
print(p1[0]) # 10
print(p1.y) # 20
# Tuple unpacking funktioniert
x, y = p1
print(x, y) # 10 20
6.5.2 Verschiedene Erstellungsmethoden
from collections import namedtuple
# Methode 1: Liste von Strings
Point = namedtuple('Point', ['x', 'y'])
# Methode 2: String mit Leerzeichen
Point = namedtuple('Point', 'x y')
# Methode 3: String mit Kommas
Point = namedtuple('Point', 'x, y')
6.5.3 Methoden von namedtuple
from collections import namedtuple
Point = namedtuple('Point', 'x y')
p = Point(10, 20)
# Als Dictionary
print(p._asdict()) # {'x': 10, 'y': 20}
# Felder anzeigen
print(p._fields) # ('x', 'y')
# Neues Objekt mit geänderten Werten
p2 = p._replace(x=30)
print(p2) # Point(x=30, y=20)
# Aus iterable erstellen
Point._make([40, 50]) # Point(x=40, y=50)
6.5.4 Default-Werte
from collections import namedtuple
# Mit defaults (Python 3.7+)
Point = namedtuple('Point', ['x', 'y', 'z'], defaults=[0, 0, 0])
p1 = Point()
print(p1) # Point(x=0, y=0, z=0)
p2 = Point(10)
print(p2) # Point(x=10, y=0, z=0)
p3 = Point(10, 20)
print(p3) # Point(x=10, y=20, z=0)
6.5.5 Vergleich: namedtuple vs. dict vs. class
from collections import namedtuple
# namedtuple
Person = namedtuple('Person', 'name age')
p1 = Person('Alice', 30)
# Dictionary
p2 = {'name': 'Alice', 'age': 30}
# Klasse
class PersonClass:
def __init__(self, name, age):
self.name = name
self.age = age
p3 = PersonClass('Alice', 30)
# Speicherverbrauch
import sys
print(f"namedtuple: {sys.getsizeof(p1)} bytes") # ~56
print(f"dict: {sys.getsizeof(p2)} bytes") # ~232
print(f"class: {sys.getsizeof(p3)} bytes") # ~48 + __dict__
6.5.6 Praktische Beispiele
CSV-Daten verarbeiten:
from collections import namedtuple
import csv
# CSV-Daten
csv_data = """name,age,city
Alice,30,New York
Bob,25,London
Charlie,35,Paris"""
# namedtuple aus CSV-Header
lines = csv_data.strip().split('\n')
header = lines[0].split(',')
Person = namedtuple('Person', header)
# Daten parsen
people = []
for line in lines[1:]:
values = line.split(',')
person = Person(*values)
people.append(person)
for p in people:
print(f"{p.name} is {p.age} years old from {p.city}")
Koordinaten-System:
from collections import namedtuple
import math
Point = namedtuple('Point', 'x y')
def distance(p1, p2):
"""Euklidische Distanz zwischen zwei Punkten"""
return math.sqrt((p2.x - p1.x)**2 + (p2.y - p1.y)**2)
p1 = Point(0, 0)
p2 = Point(3, 4)
print(f"Distance: {distance(p1, p2)}") # 5.0
RGB-Farben:
from collections import namedtuple
Color = namedtuple('Color', 'red green blue')
# Vordefinierte Farben
BLACK = Color(0, 0, 0)
WHITE = Color(255, 255, 255)
RED = Color(255, 0, 0)
def to_hex(color):
return f'#{color.red:02x}{color.green:02x}{color.blue:02x}'
print(to_hex(RED)) # #ff0000
6.6 Zusammenfassung
| Collection | Verwendung | Vorteil |
|---|---|---|
Counter | Elemente zählen | Einfache Häufigkeitsanalyse |
defaultdict | Dictionary mit Auto-Initialisierung | Kein KeyError, weniger Code |
deque | Queue/Stack mit Zugriff an beiden Enden | O(1) append/pop an beiden Enden |
ChainMap | Mehrere Dictionaries verketten | Konfiguration mit Fallbacks |
namedtuple | Tupel mit benannten Feldern | Lesbar, leichtgewichtig |
Kernprinzip: Das collections-Modul bietet spezialisierte Datenstrukturen für häufige Anwendungsfälle, die effizienter und lesbarer sind als selbstgebaute Lösungen.
Operatoren
Siehe auch datenstrukturen-collections, Abschnitt “Sets bzw. Mengen”.
1 Vergleichsoperatoren
| Ausdruck | Bedeutung |
|---|---|
a == b | a ist gleich b |
a != b | a ist ungleich b |
a < b | a ist kleiner als b |
a > b | a ist größer als b |
a <= b | a ist kleiner oder gleich b |
a >= b | a ist größer oder gleich b |
2 Arithmetische Operatoren
| Ausdruck | Bedeutung |
|---|---|
a + b | a wird zu b addiert |
a - b | b wird von a subtrahiert |
a / b | a wird durch b geteilt |
a // b | Ganzzahldivision von a durch b |
a % b | Rest von a durch b |
a * b | a wird mit b multipliziert |
a ** b | a hoch b (Potenz) |
3 Bitweise Operatoren
| Ausdruck | Bedeutung |
|---|---|
a & b | Bitweises AND |
a | b | Bitweises OR |
a ^ b | Bitweises XOR |
~a | Bitweises NOT (Eins-Komplement) |
a << b | Bitweise Linksverschiebung |
a >> b | Bitweise Rechtsverschiebung |
4 Logische Operatoren
| Ausdruck | Bedeutung |
|---|---|
a and b | Beide sind wahr (AND) |
a or b | Einer ist wahr (OR) |
not a | a ist falsch (NOT) |
5 Zusammengesetzte Zuweisungsoperatoren
| Ausdruck | Bedeutung |
|---|---|
a += b | Wert addieren und zuweisen (a = a + b) |
a -= b | Wert subtrahieren und zuweisen (a = a - b) |
a /= b | Wert teilen und zuweisen (a = a / b) |
a //= b | Ganzzahldivision und zuweisen (a = a // b) |
a *= b | Wert multiplizieren und zuweisen (a = a * b) |
a **= b | Potenzieren und zuweisen (a = a ** b) |
a |= b | Bitweises ODER und zuweisen (a = a | b) |
a &= b | Bitweises UND und zuweisen (a = a & b) |
a ^= b | Bitweises XOR und zuweisen (a = a ^ b) |
a <<= b | Linksverschiebung und zuweisen (a = a << b) |
a >>= b | Rechtsverschiebung und zuweisen (a = a >> b) |
6 Walrus Operator := (Assignment Expression)
Der Walrus Operator (:=) wurde in Python 3.8 eingeführt und ermöglicht Zuweisungen innerhalb von Ausdrücken.
Syntax:
(variable := expression)
6.1 Grundlegendes Beispiel
# Ohne Walrus Operator
data = input("Enter text: ")
if len(data) > 5:
print(f"Text is {len(data)} characters long")
# Mit Walrus Operator (kompakter)
if (n := len(input("Enter text: "))) > 5:
print(f"Text is {n} characters long")
6.2 In while-Schleifen
# Ohne Walrus Operator
line = input("Enter command: ")
while line != "quit":
print(f"You entered: {line}")
line = input("Enter command: ")
# Mit Walrus Operator (DRY - Don't Repeat Yourself)
while (line := input("Enter command: ")) != "quit":
print(f"You entered: {line}")
6.3 In List Comprehensions
# Liste von Quadraten, nur wenn Quadrat > 10
numbers = [1, 2, 3, 4, 5, 6]
# Ohne Walrus Operator (berechnet x**2 zweimal)
squares = [x**2 for x in numbers if x**2 > 10]
# Mit Walrus Operator (berechnet nur einmal)
squares = [square for x in numbers if (square := x**2) > 10]
# [16, 25, 36]
6.4 Bei regulären Ausdrücken
import re
# Ohne Walrus Operator
text = "Email: user@example.com"
match = re.search(r'[\w\.-]+@[\w\.-]+', text)
if match:
print(f"Found: {match.group()}")
# Mit Walrus Operator
if (match := re.search(r'[\w\.-]+@[\w\.-]+', text)):
print(f"Found: {match.group()}")
6.5 Wichtige Hinweise
Klammern erforderlich:
# ❌ Syntaxfehler
if n := 5 > 3:
pass
# ✅ Richtig
if (n := 5) > 3:
pass
Nicht in allen Kontexten erlaubt:
# ❌ Nicht als standalone statement
n := 5 # SyntaxError
# ✅ Normale Zuweisung verwenden
n = 5
Wann verwenden:
- Wenn ein Wert berechnet UND in einer Bedingung verwendet wird
- Bei while-Schleifen mit komplexen Bedingungen
- In List Comprehensions zur Vermeidung doppelter Berechnungen
Wann nicht verwenden:
- Wenn normale Zuweisung ausreicht
- Wenn es die Lesbarkeit verschlechtert
Kontrollstrukturen
1 Bedingungen
1.1 if, elif und else
a = 1
b = 2
if a > b:
print('a ist größer als b')
elif a < b: # = elseif bzw. else if in anderen Sprachen
print('a ist kleiner als b')
else:
print('a ist gleich b')
Häufig kann man direkte Vergleiche weglassen:
check = True
string = 'Text'
if check is True:
print('Bedingung erfüllt.')
if len(string) != 0:
print('String ist nicht leer.')
# Kürzer:
if check:
print('Bedingung erfüllt.')
if string:
print('String ist nicht leer.')
1.2 Shorthand
a = 1
b = 2
if a < b: print('a ist kleiner als b')
1.3 Ternary operators (conditional expressions)
a = 1
b = 2
kommentar = 'a ist größer als b' if a > b else 'a ist kleiner als b'
print(kommentar) # a ist kleiner als b
1.4 ShortHand Ternary
Beispiel 1:
print( True or 'Some' ) # True
Beispiel 2:
print( False or 'Some' ) # Some
Beispiel 3:
output = None
msg = output or 'Keine Daten'
print(msg) # Keine Daten
2 Switch-Case-Äquivalent für Python-Version < 3.10
Vor Python-Version 3.10 gibt es keine Switch-Case-Anweisung, sie muss simuliert werden:
def switcher(age):
if age < 18:
msg = 'Minderjährig'
elif age < 67:
msg = 'Volljährig'
elif age in range(0, 150):
msg = 'Rentner'
else:
msg = 'Fehler'
return msg
print( witcher(50)) # Volljährig
3 Pattern Matching mit match-case (Python 3.10+)
Seit Python 3.10 gibt es strukturelles Pattern Matching mit match-case. Dies ist deutlich mächtiger als einfache Switch-Case-Statements aus anderen Sprachen.
3.1 Einfaches Matching (Literal Patterns)
def http_status(status):
match status:
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _: # Wildcard (default)
return "Unknown Status"
print(http_status(404)) # Not Found
3.2 Mehrere Werte (OR Patterns)
def classify_http_status(status):
match status:
case 200 | 201 | 204:
return "Success"
case 400 | 401 | 403 | 404:
return "Client Error"
case 500 | 502 | 503:
return "Server Error"
case _:
return "Unknown"
print(classify_http_status(201)) # Success
3.3 Guards (if-Bedingungen)
age = 50
match age:
case _ if age < 0:
msg = "Invalid age"
case _ if age < 18:
msg = "Minor"
case _ if age < 67:
msg = "Adult"
case _ if age < 150:
msg = "Senior"
case _:
msg = "Invalid age"
print(msg) # Adult
3.4 Sequence Patterns (Listen, Tupel)
# Listen/Tupel matchen
point = (0, 5)
match point:
case (0, 0):
print("Origin")
case (0, y):
print(f"On Y-axis at y={y}")
case (x, 0):
print(f"On X-axis at x={x}")
case (x, y):
print(f"Point at ({x}, {y})")
# Output: On Y-axis at y=5
Variable-length Patterns:
data = [1, 2, 3, 4, 5]
match data:
case []:
print("Empty list")
case [x]:
print(f"Single element: {x}")
case [x, y]:
print(f"Two elements: {x}, {y}")
case [first, *rest]:
print(f"First: {first}, Rest: {rest}")
# Output: First: 1, Rest: [2, 3, 4, 5]
Exakte Länge mit Wildcard:
coordinates = (10, 20, 30)
match coordinates:
case (x, y):
print(f"2D: {x}, {y}")
case (x, y, z):
print(f"3D: {x}, {y}, {z}")
case (x, y, z, _):
print(f"4D+: First three {x}, {y}, {z}")
# Output: 3D: 10, 20, 30
3.5 Mapping Patterns (Dictionaries)
user = {"name": "Alice", "age": 30, "role": "admin"}
match user:
case {"role": "admin", "name": name}:
print(f"Admin user: {name}")
case {"role": "user", "name": name}:
print(f"Regular user: {name}")
case {"name": name}:
print(f"User without role: {name}")
# Output: Admin user: Alice
Wichtig: Dictionaries matchen partial (zusätzliche Keys werden ignoriert):
data = {"type": "point", "x": 10, "y": 20, "color": "red"}
match data:
case {"type": "point", "x": x, "y": y}:
print(f"Point at ({x}, {y})")
# "color" wird ignoriert
# Output: Point at (10, 20)
3.6 Class Patterns (Strukturelles Matching)
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
@dataclass
class Circle:
center: Point
radius: int
shape = Circle(Point(0, 0), 5)
match shape:
case Circle(center=Point(x=0, y=0), radius=r):
print(f"Circle at origin with radius {r}")
case Circle(center=Point(x=x, y=y), radius=r):
print(f"Circle at ({x}, {y}) with radius {r}")
# Output: Circle at origin with radius 5
Kürzere Syntax:
match shape:
case Circle(Point(0, 0), r):
print(f"Circle at origin with radius {r}")
case Circle(Point(x, y), r):
print(f"Circle at ({x}, {y}) with radius {r}")
3.7 AS Patterns (Capture Whole + Parts)
Mit as kann man sowohl das Gesamtobjekt als auch Teile davon erfassen:
data = [1, 2, 3, 4]
match data:
case [x, *rest] as full_list:
print(f"First: {x}")
print(f"Rest: {rest}")
print(f"Full list: {full_list}")
# Output:
# First: 1
# Rest: [2, 3, 4]
# Full list: [1, 2, 3, 4]
3.8 Verschachtelte Patterns
command = ("move", {"x": 10, "y": 20})
match command:
case ("move", {"x": x, "y": y}):
print(f"Move to ({x}, {y})")
case ("resize", {"width": w, "height": h}):
print(f"Resize to {w}x{h}")
case ("rotate", {"angle": angle}):
print(f"Rotate by {angle}°")
# Output: Move to (10, 20)
3.9 Praktische Beispiele
3.9.1 JSON-API Response Handling
def handle_response(response):
match response:
case {"status": "success", "data": data}:
return f"Success: {data}"
case {"status": "error", "code": 404}:
return "Resource not found"
case {"status": "error", "code": code, "message": msg}:
return f"Error {code}: {msg}"
case {"status": "error"}:
return "Unknown error"
case _:
return "Invalid response format"
# Test
print(handle_response({"status": "success", "data": {"id": 1}}))
print(handle_response({"status": "error", "code": 500, "message": "Server error"}))
3.9.2 Command Parser
def execute_command(cmd):
match cmd.split():
case ["quit"] | ["exit"]:
return "Exiting..."
case ["help"]:
return "Available commands: help, quit, list, add, delete"
case ["list"]:
return "Listing all items..."
case ["add", item]:
return f"Adding {item}"
case ["add", *items]:
return f"Adding multiple items: {items}"
case ["delete", item]:
return f"Deleting {item}"
case _:
return "Unknown command"
print(execute_command("add apple")) # Adding apple
print(execute_command("add apple banana")) # Adding multiple items: ['apple', 'banana']
3.9.3 AST-ähnliche Strukturen
from dataclasses import dataclass
@dataclass
class BinaryOp:
op: str
left: any
right: any
@dataclass
class Constant:
value: int
def evaluate(expr):
match expr:
case Constant(value):
return value
case BinaryOp("+", left, right):
return evaluate(left) + evaluate(right)
case BinaryOp("-", left, right):
return evaluate(left) - evaluate(right)
case BinaryOp("*", left, right):
return evaluate(left) * evaluate(right)
case _:
raise ValueError(f"Unknown expression: {expr}")
# Test: (2 + 3) * 5
expr = BinaryOp("*",
BinaryOp("+", Constant(2), Constant(3)),
Constant(5)
)
print(evaluate(expr)) # 25
3.9.4 Event Handler
def handle_event(event):
match event:
case {"type": "click", "x": x, "y": y, "button": "left"}:
print(f"Left click at ({x}, {y})")
case {"type": "click", "x": x, "y": y, "button": "right"}:
print(f"Right click at ({x}, {y})")
case {"type": "keypress", "key": "Enter"}:
print("Enter pressed")
case {"type": "keypress", "key": key, "ctrl": True}:
print(f"Ctrl+{key} pressed")
case {"type": "scroll", "delta": delta} if delta > 0:
print("Scrolling up")
case {"type": "scroll", "delta": delta} if delta < 0:
print("Scrolling down")
handle_event({"type": "click", "x": 100, "y": 200, "button": "left"})
handle_event({"type": "keypress", "key": "S", "ctrl": True})
3.10 Best Practices
✅ DO:
- Nutze Pattern Matching für komplexe Datenstrukturen
- Verwende Guards für zusätzliche Bedingungen
- Ordne Patterns von spezifisch zu allgemein
- Nutze
_als Wildcard/Default-Case
❌ DON’T:
- Verwende nicht
matchfür einfache if-elif-Ketten - Vermeide zu komplexe, verschachtelte Patterns
- Default-Case (
case _:) nicht vergessen
3.11 Pattern Matching vs. if-elif
Wann match verwenden:
# ✅ Gut für strukturelle Daten
match point:
case (0, 0): return "Origin"
case (x, 0): return f"X-axis: {x}"
case (0, y): return f"Y-axis: {y}"
case (x, y): return f"Point: ({x}, {y})"
Wann if-elif verwenden:
# ✅ Besser für einfache Vergleiche
if age < 18:
return "Minor"
elif age < 65:
return "Adult"
else:
return "Senior"
3.12 Zusammenfassung
| Pattern-Typ | Beispiel | Verwendung |
|---|---|---|
| Literal | case 200: | Exakter Wert |
| Wildcard | case _: | Match alles (default) |
| Capture | case x: | Wert in Variable speichern |
| OR | case 200 | 201 | 204: | Mehrere Werte |
| Sequence | case [x, y, z]: | Listen/Tupel mit fester Länge |
| Sequence (rest) | case [first, *rest]: | Variable Länge |
| Mapping | case {"key": value}: | Dictionaries |
| Class | case Point(x, y): | Objekte/Dataclasses |
| Guard | case x if x > 0: | Zusätzliche Bedingung |
| AS | case [x, *rest] as full: | Ganzes + Teile erfassen |
4 Schleifen
4.1 for
L = [0, 1, 2, 3]
count = len(L)
# von 0 bis count-1
for i in range(count):
print(L[i], end=', ' if i < count-1 else '\n') # 0, 1, 2, 3
# von 1 bis count-1
for i in range(1, count):
print(L[i], end=', ' if i < count-1 else '\n') # 1, 2, 3
# von 0 bis count-1 mit Schrittweite 2
for i in range(0, count, 2):
print(L[i], end=', ' if i < count-2 else '\n') # 0, 2
# von count-1 bis 0 (rückwärts)
for i in range(count-1, -1, -1):
print(L[i], end=', ' if i != 0 else '\n') # 3, 2, 1, 0
D = {'A': 1, 'B': 3, 'C': 7}
for key in D:
print(key, end=' ') # A, B, C
print('')
for key, value in D.items():
print(f'{key}={value}', end=' ') # A=1 B=3 C=7
Mit Zählervariable:
lst = ['A', 'B', 'C']
for i in range(len(lst)):
print(lst[i])
# Kürzer:
for value in lst:
print(value)
# Wird der Index benötigt:
for i, value in enumerate(lst):
print(f'{i}: {value}')
Bildung eines Iterators mittels zip():
a = [1, 2, 3]
b = [4, 5, 6]
for i in range(len(a)):
print(f'{a[i]} und {b[i]}')
# Besser:
for av, bv in zip(a, b):
print(f'{av} und {bv}')
# Mit Index:
for i, (av, bv) in enumerate(zip(a, b)):
print(f'{i}: {av} und {bv}')
4.2 while
i = 0
m = 3
while i <= m:
print(i)
i += 1
i = 0
m = 3
run = True
while run:
print(i)
if i == m: run = False
i += 1
i = 0
m = 3
while True:
print(i)
if i == m: break
i += 1
Funktionen
1 *args und **kwargs
*args und **kwargs sind optionale Paramenter. args (positional arguments) ist eine Liste oder ein Tupel. kwargs (keyword arguments) ist ein Dictionary.
benennung der positional und keyword rguments)
Die Namen sind nicht vorgeschrieben, wichtig sind nur die Sternchen vor den Variablennamen. Die Benennung *args und **kwargs ist jedoch üblich.
Beispiel zu positional arguments:
def summiere(*zahlen):
return sum(zahlen)
ergebnis = summiere(1, 2, 3, 4, 5) # 15
Beispiel zu keyword arguments:
def details(**info):
for schluessel, wert in info.items():
print(f"{schluessel}: {wert}")
details(name="Max", alter=25, beruf="Entwickler")
Ausgabe:
name: Max
alter: 25
beruf: Entwickler
2 Lambda-Funktion (anonyme Funktion)
quadrieren = lambda x: x ** 2
ergebnis = quadrieren(4) # 16
3 Funktion mit mehreren Rückgabewerten
def rechne(a, b):
return a + b, a * b
summe, produkt = rechne(3, 4)
print(summe, produkt) # 7 12
4 Rekursive Funktion
def fakultaet(n):
if n == 0:
return 1
return n * fakultaet(n - 1)
ergebnis = fakultaet(5) # 120
5 Scope (Gültigkeitsbereich) von Variablen
x = 10 # Globale Variable
def meine_funktion():
x = 5 # Lokale Variable
print(x)
meine_funktion() # 5
print(x) # 10
6 Benannte Parameter (keyword parameter)
6.1 Standardverhalten
def my_function(a):
return a * 2
res1 = my_function(2) # Positionsargument
res2 = my_function(a=2) # Keyword-Argument
Beide Varianten (my_function(2) und my_function(a=2)) funktionieren.
6.2 / - Positional-Only Parameter (nur Positionsargumente)
def my_function(a, /):
return a * 2
res1 = my_function(2) # OK: Positionsargument
res2 = my_function(a=2) # Fehler: Darf nicht als Keyword-Argument übergeben werden
Der Schrägstrich (/) bedeutet, dass alle Parameter davor NUR als Positionsargumente übergeben werden dürfen.
warum positional-only?
- Wird oft bei eingebauten Funktionen wie
len()genutzt (len(obj)stattlen(obj=obj)) - Macht API-Design klarer
- Verhindert, dass Parameter versehentlich als Schlüsselwort verwendet werden
6.3 * – Keyword-Only Parameter (nur benannte Argumente erlaubt)
def my_function(*, a):
return a * 2
res1 = my_function(2) # Fehler: Darf nicht als Positionsargument übergeben werden
res2 = my_function(a=2) # OK: Muss als benanntes Argument übergeben werden
Das Sternchen (*) bedeutet, dass alle Parameter danach NUR als Keyword-Argument übergeben werden dürfen.
warum keyword-only?
Erhöht die Lesbarkeit von Funktionen Verhindert Verwechslungen bei der Reihenfolge der Argumente
6.4 Kombination aus / und *
def my_function(a, /, b, *, c):
return a + b + c
res1 = my_function(1, 2, c=3) # OK
res2 = my_function(1, b=2, c=3) # OK
res3 = my_function(a=1, 2, c=3) # Fehler: a darf nicht als Keyword-Argument übergeben werden
res4 = my_function(1, 2, 3) # Fehler: c muss als benanntes Argument übergeben werden
Erklärung:
aist positional-only, weil es vor/steht. $\Rightarrow$ Darf nicht alsa=1übergeben werden. •bkann positional oder keyword sein, weil es zwischen/und*steht. •cist keyword-only, weil es nach*steht.
6.5 Zusammenfassung / und *
| Schreibweise | Bedeutung |
|---|---|
def f(a) | Standard: Positions- und Keyword-Argumente erlaubt |
def f(a, /) | a ist positional-only (kein a= erlaubt) |
def f(*, a) | a ist keyword-only (kein f(2), nur f(a=2)) |
def f(a, /, b, *, c) | a $\rightarrow$ nur positional, b $\rightarrow$ beides erlaubt, c $\rightarrow$ nur keyword |
best practice:
/für klare API-Schnittstellen*für mehr Lesbarkeit und weniger Fehler
7 Optionale Listen mit Standardwert
danger
Verwendet man optionale Listen als Funktionsparameter und gibt diesen einen Standardwert, ist zu beachten, dass dieser bei der Definition der Funktion festgelegt wird, NICHT beim Aufruf der Funktion. Dies führt häufig zu einem nicht gewolltem Verhalten.
Beispiel:
def append(n, l=[]):
l.append(n)
return l
print(append(0)) # [0]
print(append(0)) # [0, 0] != [0]
Daher ist es bei den meisten Anwendungsfällen besser, die Liste innerhalb der Funktion zu initialisieren:
def append(n, l=None):
if l is None:
l = []
l.append(n)
return l
print(append(0)) # [0]
print(append(0)) # [0]
8 partial()
Mit partial() wird eine neue Funktion erstellt, bei der einige Argumente einer bestehenden Funktion bereits vorbelegt sind. Dies ist praktisch, wenn man eine bestimmte Funktion wiederholt aufruft, wobei einige Parameter immer gleich sind.
Syntax:
from functools import partial
neue_funktion = partial(funktion, arg1, arg2, ...)
Beispiel:
from functools import partial
def multiply(x, y):
return x * y
# Erstelle eine neue Funktion, die den gegebenen Wert immer mit 2 multipliziert
double_value = partial(multiply, 2)
print(double_value(5)) # Ausgabe: 10
print(double_value(10)) # Ausgabe: 20
Alternative mit lambda:
# [...]
double_value = lambda y: multiply(2, y)
# [...]
9 Rückgabe mehrerer Werte mit yield
Neben der Möglichkeit, mehrere Werte über eine Liste oder ein Tupel zurückzugeben, gibt es das Schlüsselwort yield:
#![allow(unused)]
fn main() {
def my_func():
yield 'Hello'
yield 'World'
yield 123
return_values = my_func()
for val in return_values:
print(val)
}
Ausgabe:
Hello
World
123
Ein- und Ausgabe
1 Ausgabe mit print()
1.1 Mit und ohne Zeilenumbruch
x = 1
y = 2
print(x)
print(y)
print(x, y, sep=', ', end=' ') # Kein Zeilenumbruch am Ende
print('(Meine Werte)')
print(x, end=', ') # Komma statt Zeilenumbruch am Ende
print(y)
# Ausgabe:
# 1
# 2
# 1, 2 (Meine Werte)
# 1, 2
besonderheit bei der verwendung von `print()` mit `end=''`
Normalerweise geht die Ausgabe von print() in den Puffer. Wenn der end-Parameter verändert wird, wird der Puffer nicht mehr gespült, d. h. aus Effizienzgründen kann es sein, dass die Ausgabe nicht sofort erfolgt, wenn print() aufgerufen wird (z. B. in Schleifen). Abhilfe schafft die Verwendung des Parameters flush:
print('Hallo', end='', flush=True)
1.2 Formatierung
m = 123456789 # Masse in kg
g = 9.81 # Erdbeschleunigung
F = m * g / 1000 # Gewichtskraft in kN
print(f'F = {F} kN') #F = 1211111.10009 kN
print(f'F = {round(F, 2)} kN') #F = 1211111.1 kN
print(f'F = {F:.2f} kN') #F = 1211111.10 kN
print(f'F = {F:.2e} kN') #F = 1.21e+06 kN
print('F = %.2e kN'% (F)) #F = 1.21e+06 kN
2 Logging
import logging
# Setup
level = logging.DEBUG
format = '[%(levelname)s] %(asctime)s - %(message)s'
logging.basicConfig(level=level, format=format)
# Beispiele
logging.info('Normale Info')
logging.debug('Debug-Info')
logging.error('Fehler...')
Ausgabe:
[INFO] 2025-02-27 23:22:36,954 - Normale Info
[DEBUG] 2025-02-27 23:22:36,955 - Debug-Info
[ERROR] 2025-02-27 23:22:36,956 - Fehler...
3 Tabellen
3.1 Mit “Bordmitteln” (ohne zus. Pakete)
Beispiel 1:
s1 = 'a'
s2 = 'ab'
s3 = 'abc'
s4 = 'abcd'
print(f'{s1:>10}') # a
print(f'{s2:>10}') # cd
print(f'{s3:>10}') # bcd
print(f'{s4:>10}') # abcd
Beispiel 2:
for x in range(1, 11):
print(f'{x:05} {x*x:3} {x*x*x:4}')
Ausgabe:
00001 1 1
00002 4 8
00003 9 27
00004 16 64
00005 25 125
00006 36 216
00007 49 343
00008 64 512
00009 81 729
00010 100 1000
3.2 tabulate und prettytable
from tabulate import tabulate # License: MIT
from prettytable import PrettyTable # License: BSD (3 clause)
head = ['Name', 'Alter'] # Überschriften
data = [['Max', 33], ['Monika', 29]] # Inhalt/Zeilen
# Paket "tabulate"
# https://github.com/astanin/python-tabulate
print(tabulate(tabular_data=data, headers=head, tablefmt='pretty',
colalign=('left', 'right')))
# Alternative: tablefmt='fancy_outline'
print()
# Paket "prettytable"
# https://github.com/jazzband/prettytable
t = PrettyTable(head)
t.add_rows(data)
t.add_row(['Werner', 44])
t.align['Name'] = 'l'
t.align['Alter'] = 'r'
print(t)
print()
Ausgabe:
+--------+-------+
| Name | Alter |
+--------+-------+
| Max | 33 |
| Monika | 29 |
+--------+-------+
+--------+-------+
| Name | Alter |
+--------+-------+
| Max | 33 |
| Monika | 29 |
| Werner | 44 |
+--------+-------+
3.3 texttable
from texttable import Texttable # License: MIT
# Paket "texttable"
# https://github.com/foutaise/texttable/
t = Texttable()
t.set_cols_align(['l', 'c', 'r', 'l'])
t.set_cols_valign(['t', 'm', 'm', 'b'])
head = ['Name', 'Spitzname', 'Alter', 'Kommentar']
data = [['Herr\nMaximilian\nHansen', 'Max', 33, '2 Kinder'],
['Frau\nMonika\nPetersen', 'Moni', 29, 'kein\nKommentar']]
data.insert(0, head)
t.add_rows(data)
print(t.draw())
print()
t.set_deco(Texttable.HEADER)
print(t.draw())
Ausgabe:
+------------+-----------+-------+-----------+
| Name | Spitzname | Alter | Kommentar |
+============+===========+=======+===========+
| Herr | | | |
| Maximilian | Max | 33 | |
| Hansen | | | 2 Kinder |
+------------+-----------+-------+-----------+
| Frau | | | |
| Monika | Moni | 29 | kein |
| Petersen | | | Kommentar |
+------------+-----------+-------+-----------+
Name Spitzname Alter Kommentar
==========================================
Herr
Maximilian Max 33
Hansen 2 Kinder
Frau
Monika Moni 29 kein
Petersen Kommentar
4 Eingabe mit input()
Eine Benutzereingabe aus der Konsole kann wie folgt eingelesen werden:
print('Gib etwas ein:')
s = input()
print(f'Du hast den folgenden Text eingegeben: {s}')
5 Inhalt einer Textdatei lesen
5.1 Einzelne Zeichen oder Zeilen lesen
# Datei öffnen
with open('textdok.txt', 'r') as f:
# Inhalt lesen
print(f.read(5)) # Erste 5 zeichen der Datei
print(f.readline()) # Erste Zeile, die noch nicht gelesen wurde
print(f.readline()) # Nächste Zeile, usw.
note
with-statement:
f.close() um die Datei zu schließen kann aufgrund des with-statements (Kontextmanager-Statement) entfallen. Die Datei wird auch geschlossen, wenn es innerhalb des with-statements zu einem Fehler kommt. Daher ist diese Variante immer zu bevorzugen, um Resourcenlecks, Dateisperren und Datenverlust zu vermeiden! Siehe auch Kontext-Manager.
5.2 Komplette Datei lesen
# Datei öffnen
with open('textdok.txt', 'r') as f:
# Inhalt lesen
text = f.read() # Als String
text = f.readlines() # Als Liste
Alternative Möglichkeit, um die Zeilen einer Textdatei in einer Liste zu speichern (der Zeilenumbruch \n am Ende der Strings entfällt hierbei):
# Datei öffnen
with open('textdok.txt', 'r'):
# Inhalt lesen
text = []
for line in f:
text.append(line.replace('\n', ''))
print(text)
6 CSV-Datei lesen
import csv
lines = []
with open('tabelle.csv', newline='') as csvfile:
reader = csv.reader(csvfile, delimiter=';', quotechar='|')
for row in reader:
lines.append(row)
print(lines)
Alternative mit Pandas:
import pandas as pd
# Data frame erstellen
# header = None, wenn keine Überschriften vorhanden sind
df = pd.read_csv('beispieldateien/tabelle.csv', header=0, sep=";", decimal=",",
names=['x-Wert', 'y-Wert'])
print(df)
print()
print(df.iloc[6]['x-Wert'])
print(df.iloc[6][1])
7 Weitere Dateioperationen
import os
os.rename(from, to) # Umbennen
os.remove(path) # Löschen
os.chmod(file, 0700) # Dateiberechtigungen
os.stat(file) # Dateiinformationen wie Größe, Datum, usw.
8 Pfade mit os und pathlib.Path
Es gibt es zwei gängige Möglichkeiten, mit Dateipfaden und dem Dateisystem zu arbeiten:
- Das ältere
os-Modul (gemeinsam mitos.path) - Das modernere
pathlib-Modul
8.1 Übersicht
| Funktion | os / os.path | pathlib.Path |
|---|---|---|
| Pfad erstellen | os.path.join() | Path() / Path.joinpath() |
| Existenz prüfen | os.path.exists() | Path.exists() |
| Datei/Verzeichnis prüfen | os.path.isfile() / .isdir() | Path.is_file() / Path.is_dir() |
| Absoluter Pfad | os.path.abspath() | Path.resolve() |
| Datei lesen/schreiben | open(path) | Path.read_text() / Path.write_text() |
| Verzeichnisinhalt auflisten | os.listdir() | Path.iterdir() |
8.2 Beispiele mit os
import os
# Pfad zusammensetzen
pfad = os.path.join("verzeichnis", "datei.txt")
# Prüfen, ob Pfad existiert
if os.path.exists(pfad):
print("Pfad existiert.")
# Absoluten Pfad bekommen
absolut = os.path.abspath(pfad)
8.3 Beispiele mit pathlib.Path
from pathlib import Path
# Pfad zusammensetzen
pfad = Path("verzeichnis") / "datei.txt"
# Existenz prüfen
if pfad.exists():
print("Pfad existiert.")
# Absoluten Pfad bekommen
absolut = pfad.resolve()
8.4 Vorteile von pathlib.Path
- Objektorientiert:
Pathist eine Klasse mit Methoden, was zu lesbarerem Code führt. - Plattformunabhängig:
/funktioniert auf allen Systemen (intern wird automatisch das richtige Trennzeichen verwendet). - Übersichtlicher: Viele Methoden wie
.read_text()oder.mkdir()sind direkt verfügbar. - Bessere Lesbarkeit durch Methodenkette statt Funktionsverschachtelung.
was sollte man verwenden?
pathlib wurde mit Python 3.4 eingeführt und ist mittlerweile der empfohlene Standard für Pfadoperationen. Nur bei sehr alten Projekten oder bei Kompatibilität mit Python 2 sollte noch os.path verwendet werden.
8.5 Kombination mit anderen Modulen
pathlib.Path lässt sich gut mit anderen Modulen kombinieren, z. B.:
import shutil
pfad = Path("backup") / "datei.txt"
shutil.copy(pfad, Path("ziel") / "kopie.txt")
8.6 Zusammenfassung
- Für neue Projekte: Immer pathlib.Path verwenden.
- Für alte Projekte oder einfache Kompatibilität:
os.pathkann noch genutzt werden. - Beide Module bieten ähnliche Funktionalität, aber
pathlibist moderner, klarer und objektorientiert.
9 Serialisierung – Daten speichern und laden
Serialisierung ist der Prozess, Python-Objekte in ein speicherbares Format zu konvertieren (und wieder zurück). Dies ermöglicht das Speichern von Daten in Dateien oder den Austausch zwischen Systemen.
9.1 JSON (JavaScript Object Notation)
JSON ist ein textbasiertes, sprachunabhängiges Datenformat. Es ist lesbar, weit verbreitet und ideal für APIs und Konfigurationsdateien.
9.1.1 Grundlegende Verwendung
import json
# Python-Objekt
data = {
'name': 'Alice',
'age': 30,
'hobbies': ['reading', 'coding'],
'active': True
}
# In JSON-String konvertieren
json_string = json.dumps(data)
print(json_string)
# {"name": "Alice", "age": 30, "hobbies": ["reading", "coding"], "active": true}
# Zurück zu Python-Objekt
parsed = json.loads(json_string)
print(parsed['name']) # Alice
9.1.2 JSON-Dateien lesen/schreiben
import json
# In Datei schreiben
data = {'users': ['Alice', 'Bob'], 'count': 2}
with open('data.json', 'w') as f:
json.dump(data, f, indent=2)
# Aus Datei lesen
with open('data.json', 'r') as f:
loaded_data = json.load(f)
print(loaded_data)
Formatierung mit indent und sort_keys:
data = {'name': 'Bob', 'age': 25, 'city': 'Berlin'}
# Schön formatiert
json_str = json.dumps(data, indent=4, sort_keys=True)
print(json_str)
# {
# "age": 25,
# "city": "Berlin",
# "name": "Bob"
# }
9.1.3 Unterstützte Datentypen
| Python | JSON |
|---|---|
dict | object |
list, tuple | array |
str | string |
int, float | number |
True | true |
False | false |
None | null |
NICHT direkt unterstützt: set, datetime, Decimal, custom Objekte
9.1.4 Custom Objekte serialisieren
import json
from datetime import datetime
class Person:
def __init__(self, name, age, birthday):
self.name = name
self.age = age
self.birthday = birthday
# Custom Encoder
class PersonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Person):
return {
'name': obj.name,
'age': obj.age,
'birthday': obj.birthday.isoformat()
}
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)
person = Person('Alice', 30, datetime(1994, 5, 15))
json_str = json.dumps(person, cls=PersonEncoder)
print(json_str)
# {"name": "Alice", "age": 30, "birthday": "1994-05-15T00:00:00"}
Custom Decoder:
def person_decoder(dct):
"""Konvertiert Dictionary zurück zu Person"""
if 'name' in dct and 'age' in dct and 'birthday' in dct:
return Person(
dct['name'],
dct['age'],
datetime.fromisoformat(dct['birthday'])
)
return dct
loaded = json.loads(json_str, object_hook=person_decoder)
print(type(loaded)) # <class '__main__.Person'>
9.1.5 Sets und Tuples behandeln
import json
data = {
'tags': {'python', 'coding', 'tutorial'}, # Set
'coordinates': (10, 20) # Tuple
}
# Set → List, Tuple → List
json_str = json.dumps({
'tags': list(data['tags']),
'coordinates': list(data['coordinates'])
})
# Beim Laden zurückkonvertieren
loaded = json.loads(json_str)
loaded['tags'] = set(loaded['tags'])
loaded['coordinates'] = tuple(loaded['coordinates'])
9.1.6 Pretty-Print für Debugging
import json
data = {'users': [{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}]}
# Kompakt (für Speicherung/Übertragung)
compact = json.dumps(data, separators=(',', ':'))
print(compact)
# {"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]}
# Lesbar (für Debugging)
readable = json.dumps(data, indent=2)
print(readable)
9.2 YAML (YAML Ain’t Markup Language)
YAML ist ein menschenlesbares Datenformat, ideal für Konfigurationsdateien. Erfordert Installation: pip install pyyaml
9.2.1 Grundlegende Verwendung
import yaml
# Python-Objekt
config = {
'database': {
'host': 'localhost',
'port': 5432,
'credentials': {
'username': 'admin',
'password': 'secret'
}
},
'features': ['logging', 'caching', 'monitoring']
}
# In YAML-String konvertieren
yaml_string = yaml.dump(config)
print(yaml_string)
# database:
# host: localhost
# port: 5432
# credentials:
# password: secret
# username: admin
# features:
# - logging
# - caching
# - monitoring
# Zurück zu Python
parsed = yaml.safe_load(yaml_string)
print(parsed['database']['host']) # localhost
9.2.2 YAML-Dateien lesen/schreiben
import yaml
# Schreiben
config = {
'server': {'host': '0.0.0.0', 'port': 8000},
'debug': True
}
with open('config.yaml', 'w') as f:
yaml.dump(config, f, default_flow_style=False)
# Lesen
with open('config.yaml', 'r') as f:
loaded = yaml.safe_load(f)
print(loaded)
9.2.3 YAML vs. JSON
Vorteile von YAML:
- Menschenlesbarer (keine Klammern, weniger Syntax)
- Kommentare möglich
- Komplexe Datenstrukturen (Anker, Aliase)
- Multiline Strings
Nachteile von YAML:
- Langsamer als JSON
- Komplexere Syntax (Einrückung wichtig!)
- Sicherheitsrisiko mit
yaml.load()(immersafe_load()verwenden!)
# config.yaml - Beispiel
# Server-Konfiguration
server:
host: localhost
port: 8080
# Multi-line String
description: |
Dies ist eine
mehrzeilige
Beschreibung.
# Anker & Alias (Wiederverwendung)
default: &default_settings
timeout: 30
retries: 3
production:
<<: *default_settings
host: prod.example.com
development:
<<: *default_settings
host: localhost
9.2.4 Sicherheit: safe_load() vs. load()
import yaml
# ❌ GEFÄHRLICH - kann beliebigen Python-Code ausführen!
# data = yaml.load(file, Loader=yaml.Loader)
# ✅ SICHER - nur einfache Python-Objekte
with open('config.yaml', 'r') as f:
data = yaml.safe_load(f)
9.2.5 Custom Objekte in YAML
import yaml
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# Representer registrieren
def person_representer(dumper, person):
return dumper.represent_mapping('!person', {
'name': person.name,
'age': person.age
})
yaml.add_representer(Person, person_representer)
# Constructor registrieren
def person_constructor(loader, node):
values = loader.construct_mapping(node)
return Person(values['name'], values['age'])
yaml.add_constructor('!person', person_constructor)
# Verwendung
person = Person('Alice', 30)
yaml_str = yaml.dump(person)
print(yaml_str)
# !person {age: 30, name: Alice}
loaded = yaml.load(yaml_str, Loader=yaml.Loader)
print(type(loaded)) # <class '__main__.Person'>
9.3 Pickle – Python-spezifische Serialisierung
Pickle serialisiert Python-Objekte in Binärformat. Nur für Python-zu-Python Kommunikation geeignet.
9.3.1 Grundlegende Verwendung
import pickle
# Python-Objekt
data = {
'numbers': [1, 2, 3],
'text': 'Hello',
'nested': {'a': 1, 'b': 2}
}
# Serialisieren (Bytes)
pickled = pickle.dumps(data)
print(pickled) # b'\x80\x04\x95...'
# Deserialisieren
unpickled = pickle.loads(pickled)
print(unpickled) # {'numbers': [1, 2, 3], ...}
9.3.2 Pickle-Dateien
import pickle
# Schreiben (Binärmodus!)
data = {'users': ['Alice', 'Bob'], 'count': 2}
with open('data.pkl', 'wb') as f:
pickle.dump(data, f)
# Lesen
with open('data.pkl', 'rb') as f:
loaded = pickle.load(f)
print(loaded)
9.3.3 Komplexe Objekte pickleln
Pickle kann fast alles serialisieren:
import pickle
from datetime import datetime
class User:
def __init__(self, name, created):
self.name = name
self.created = created
def greet(self):
return f"Hello, I'm {self.name}"
user = User('Alice', datetime.now())
# Pickle kann custom Objekte direkt serialisieren
pickled = pickle.dumps(user)
loaded = pickle.loads(pickled)
print(loaded.greet()) # Hello, I'm Alice
print(type(loaded.created)) # <class 'datetime.datetime'>
9.3.4 Pickle-Protokolle
Pickle hat verschiedene Protokoll-Versionen:
import pickle
data = {'key': 'value'}
# Protokoll 0: ASCII (lesbar, langsam)
p0 = pickle.dumps(data, protocol=0)
# Protokoll 4: Binär, schnell (Standard ab Python 3.8)
p4 = pickle.dumps(data, protocol=4)
# Höchstes verfügbares Protokoll
p_latest = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
print(f"Protocol 0: {len(p0)} bytes")
print(f"Protocol 4: {len(p4)} bytes")
9.3.5 Pickle-Sicherheitsrisiken
⚠️ WARNUNG: Pickle ist UNSICHER für nicht-vertrauenswürdige Daten!
import pickle
# ❌ GEFÄHRLICH - Kann beliebigen Code ausführen!
# untrusted_data = receive_from_network()
# obj = pickle.loads(untrusted_data) # NICHT tun!
# ✅ Nur Daten aus vertrauenswürdigen Quellen laden
with open('my_data.pkl', 'rb') as f:
obj = pickle.load(f)
9.3.6 Was kann NICHT gepickled werden?
- Lambda-Funktionen (außer mit
dill) - Verschachtelte Funktionen
- File Handles
- Netzwerk-Connections
- Thread/Lock Objekte
import pickle
# ❌ Funktioniert NICHT
try:
data = lambda x: x * 2
pickle.dumps(data)
except Exception as e:
print(f"Error: {e}") # Can't pickle lambda functions
# ✅ Alternative: dill-Bibliothek
import dill
pickled = dill.dumps(lambda x: x * 2)
9.3.7 Custom Pickle-Verhalten
import pickle
class Database:
def __init__(self, host):
self.host = host
self.connection = self._connect() # Nicht serialisierbar
def _connect(self):
# Simulierte Verbindung
return f"Connection to {self.host}"
def __getstate__(self):
"""Was gepickled wird"""
state = self.__dict__.copy()
del state['connection'] # Verbindung entfernen
return state
def __setstate__(self, state):
"""Wie es wiederhergestellt wird"""
self.__dict__.update(state)
self.connection = self._connect() # Neu verbinden
db = Database('localhost')
pickled = pickle.dumps(db)
restored = pickle.loads(pickled)
print(restored.connection) # Connection to localhost
9.4 Vergleich: JSON vs. YAML vs. Pickle
| Aspekt | JSON | YAML | Pickle |
|---|---|---|---|
| Lesbarkeit | ✅ Gut | ✅✅ Sehr gut | ❌ Binär |
| Performance | ✅ Schnell | ⚠️ Langsamer | ✅✅ Am schnellsten |
| Plattform | ✅ Sprachunabhängig | ✅ Sprachunabhängig | ❌ Nur Python |
| Datentypen | ⚠️ Limitiert | ✅ Erweitert | ✅✅ Alle Python-Typen |
| Größe | ⚠️ Mittel | ⚠️ Größer | ✅ Kompakt |
| Sicherheit | ✅ Sicher | ⚠️ safe_load()! | ❌ Unsicher |
| Kommentare | ❌ Nein | ✅ Ja | ❌ Nein |
| Use Case | APIs, Config | Config, CI/CD | Python Cache, IPC |
9.5 Wann was verwenden?
Verwende JSON wenn:
- ✅ Austausch mit anderen Systemen/Sprachen
- ✅ API-Kommunikation
- ✅ Web-Anwendungen
- ✅ Einfache Datenstrukturen
- ✅ Sicherheit wichtig
Verwende YAML wenn:
- ✅ Konfigurationsdateien
- ✅ Menschliche Lesbarkeit wichtig
- ✅ Komplexe Hierarchien
- ✅ Kommentare benötigt
- ✅ CI/CD Pipelines (Docker, Kubernetes, etc.)
Verwende Pickle wenn:
- ✅ Nur Python-zu-Python Kommunikation
- ✅ Komplexe Python-Objekte
- ✅ Performance kritisch
- ✅ Temporärer Cache
- ❌ NICHT für nicht-vertrauenswürdige Daten
9.6 Praktische Beispiele
9.6.1 Konfigurationsdatei mit JSON
import json
from pathlib import Path
class Config:
def __init__(self, config_file='config.json'):
self.config_file = Path(config_file)
self.data = self.load()
def load(self):
"""Lädt Config oder erstellt Default"""
if self.config_file.exists():
with open(self.config_file, 'r') as f:
return json.load(f)
return self.get_default()
def save(self):
"""Speichert aktuelle Config"""
with open(self.config_file, 'w') as f:
json.dump(self.data, f, indent=2)
def get_default(self):
return {
'database': {'host': 'localhost', 'port': 5432},
'debug': False,
'max_connections': 100
}
def get(self, key, default=None):
return self.data.get(key, default)
# Verwendung
config = Config()
print(config.get('database'))
config.data['debug'] = True
config.save()
9.6.2 Objekt-Caching mit Pickle
import pickle
from pathlib import Path
import time
class Cache:
def __init__(self, cache_dir='cache'):
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
def get(self, key):
"""Lädt gecachtes Objekt"""
cache_file = self.cache_dir / f"{key}.pkl"
if cache_file.exists():
with open(cache_file, 'rb') as f:
return pickle.load(f)
return None
def set(self, key, value):
"""Speichert Objekt im Cache"""
cache_file = self.cache_dir / f"{key}.pkl"
with open(cache_file, 'wb') as f:
pickle.dump(value, f)
def clear(self):
"""Löscht alle Cache-Dateien"""
for file in self.cache_dir.glob('*.pkl'):
file.unlink()
# Verwendung
cache = Cache()
# Teure Berechnung
def expensive_operation():
time.sleep(2)
return {'result': [i**2 for i in range(1000)]}
# Mit Cache
result = cache.get('computation')
if result is None:
print("Computing...")
result = expensive_operation()
cache.set('computation', result)
else:
print("From cache!")
9.6.3 Multi-Format Serializer
import json
import pickle
import yaml
from pathlib import Path
class Serializer:
@staticmethod
def save(data, filepath, format='json'):
"""Speichert Daten in gewünschtem Format"""
path = Path(filepath)
if format == 'json':
with open(path, 'w') as f:
json.dump(data, f, indent=2)
elif format == 'yaml':
with open(path, 'w') as f:
yaml.dump(data, f)
elif format == 'pickle':
with open(path, 'wb') as f:
pickle.dump(data, f)
else:
raise ValueError(f"Unknown format: {format}")
@staticmethod
def load(filepath):
"""Lädt Daten basierend auf Dateiendung"""
path = Path(filepath)
if path.suffix == '.json':
with open(path, 'r') as f:
return json.load(f)
elif path.suffix in ['.yaml', '.yml']:
with open(path, 'r') as f:
return yaml.safe_load(f)
elif path.suffix == '.pkl':
with open(path, 'rb') as f:
return pickle.load(f)
else:
raise ValueError(f"Unknown format: {path.suffix}")
# Verwendung
data = {'users': ['Alice', 'Bob'], 'count': 2}
Serializer.save(data, 'data.json', 'json')
Serializer.save(data, 'data.yaml', 'yaml')
Serializer.save(data, 'data.pkl', 'pickle')
loaded = Serializer.load('data.json')
print(loaded)
9.7 Best Practices
✅ DO:
- Verwende JSON für APIs und Webdienste
- Verwende YAML für Konfigurationsdateien
- Verwende Pickle nur für vertrauenswürdige Python-interne Daten
- Immer
yaml.safe_load()stattyaml.load() - Fehlerbehandlung beim Laden
- Versionierung bei Änderungen am Datenformat
❌ DON’T:
- Pickle für nicht-vertrauenswürdige Daten
yaml.load()ohne Loader (Sicherheitsrisiko!)- JSON für binäre Daten (Base64 verwenden falls nötig)
- Sensitive Daten ohne Verschlüsselung speichern
- Große Dateien komplett im Speicher laden
9.8 Zusammenfassung
| Format | Zweck | Vorteil |
|---|---|---|
| JSON | API, Config, Datenaustausch | Standard, sicher, schnell |
| YAML | Config, CI/CD, menschenlesbar | Kommentare, lesbar |
| Pickle | Python-Cache, temporäre Speicherung | Alle Python-Typen, schnell |
Kernprinzip: Wähle das Serialisierungsformat basierend auf Use Case, Sicherheit und Interoperabilität. JSON ist der sichere Standard, YAML für Konfiguration, Pickle nur für vertrauenswürdige Python-interne Daten.
Kommentare
1 Ein- und Mehrzeilige Kommentare
# Line comment
'''
Multi
line
comment
'''
"""
Multi
line
comment
(wird hauptsächlich für Docstrings verwendet)
"""
2 Docstrings
2.1 NumPy-Style
def dividiere(a: float, b: float) -> float:
"""
Dividiert zwei Zahlen.
Parameters
----------
a : float
Die erste Zahl.
b : float
Die zweite Zahl.
Returns
-------
float
Das Ergebnis der Division.
Raises
------
ZeroDivisionError
Falls `b` gleich Null ist.
TypeError
Falls einer der Eingaben kein Float oder Int ist.
"""
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("Beide Werte müssen Zahlen sein.")
if b == 0:
raise ZeroDivisionError("Division durch Null ist nicht erlaubt.")
return a / b
Vorteile:
- Besonders gut für wissenschaftliche Bibliotheken (
NumPy,SciPy,Pandas) - Strukturiert mit Abschnittsüberschriften (
Parameters,Returns,Raises)
2.2 Google-Style
Dies ist das Google-Dokumentationsformat. Es ist einfach zu lesen und wird oft in größeren Projekten verwendet.
def addiere(a: int, b: int) -> int:
"""
Addiert zwei Zahlen und gibt das Ergebnis zurück.
Args:
a (int): Die erste Zahl.
b (int): Die zweite Zahl.
Returns:
int: Die Summe der beiden Zahlen.
Raises:
TypeError: Falls einer der Eingaben kein Integer ist.
"""
if not isinstance(a, int) or not isinstance(b, int):
raise TypeError("Beide Werte müssen vom Typ int sein.")
return a + b
Vorteile:
- Leicht verständlich
- Strukturierte Abschnitte (
Args,Returns,Raises)
2.3 reStructuredText (reST)
Dieses Format wird in Sphinx-Dokumentationen häufig verwendet.
def multipliziere(a: float, b: float) -> float:
"""
Multipliziert zwei Zahlen.
:param a: Die erste Zahl.
:type a: float
:param b: Die zweite Zahl.
:type b: float
:return: Das Produkt von a und b.
:rtype: float
:raises TypeError: Falls einer der Eingaben kein Float ist.
"""
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("Beide Werte müssen Zahlen sein.")
return a * b
Vorteile:
- Unterstützt von Sphinx für automatisch generierte Dokumentationen
- Klar strukturierte Parameter (
:param,:return,:rtype,:raises)
2.4 Vergleich
| Format | Vorteile | Geeignet für |
|---|---|---|
| NumPy | Wissenschaftliche Standards | Data Science, ML, Forschung |
| Einfach zu lesen, klare Struktur | Allgemeine Python-Projekte | |
| reST (Sphinx) | Unterstützt in automatisierten Dokus | Große Dokumentationen, APIs |
Siehe auch:
Python Docstrings Tutorial : Examples & Format for Pydoc, Numpy, Sphinx Doc Strings, insbesondere Abschnitt Comparison of docstring formats*
Fehlerbehandlung
1 Fehlertypen
| Fehlertyp | Beschreibung |
|---|---|
SyntaxError | Fehler in der Code-Syntax |
TypeError | Falscher Typ einer Variablen oder eines Arguments |
ValueError | Ungültiger Wert für eine Operation |
IndexError | Zugriff auf nicht vorhandenen Index in einer Liste |
KeyError | Zugriff auf nicht vorhandenen Schlüssel in einem Dictionary |
ZeroDivisionError | Division durch Null |
FileNotFoundError | Datei nicht gefunden |
ImportError | Modul kann nicht importiert werden |
2 Grundlegende Fehlerbehandlung mit try und except
try:
number = int(input("Enter a number: "))
print(10 / number)
except ZeroDivisionError:
print("Error: Division by zero is not allowed.")
except ValueError:
print("Error: Please enter a valid number.")
3 Mehrere except-Blöcke
try:
data = {"name": "Alice"}
print(data["age"])
except KeyError as e:
print(f"Missing key: {e}")
except Exception as e:
print(f"General error: {e}")
4 Generischer except-Block (nicht empfohlen)
try:
# Code that may raise an error
print(10 / 0)
except Exception as e:
print(f"An error occurred: {e}")
hinweis
Dies fängt zwar alle Fehler ab, kann jedoch das Debugging deutlich erschweren!
5 else- und finally-Blöcke
try:
with open("file.txt", "r") as file:
content = file.read()
except FileNotFoundError:
print("File not found.")
else:
print("File read successfully.")
finally:
print("This block always executes.")
6 Eigene Fehler mit raise auslösen
def positive_number(number):
if number < 0:
raise ValueError("Number must be positive!")
return number
try:
positive_number(-5)
except ValueError as e:
print(f"Error: {e}")
7 Eigene Fehlerklassen definieren
class CustomError(Exception):
pass
try:
raise CustomError("This is a custom error!")
except CustomError as e:
print(f"Custom error: {e}")
8 Logging statt print verwenden
import logging
logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
try:
1 / 0
except ZeroDivisionError as e:
logging.error(f"An error occurred: {e}")
9 Zusammenfassung
tryundexceptdienen der Fehlerbehandlung.- Der
else-Block wird ausgeführt, wenn kein Fehler auftritt. - Der
finally-Block wird immer ausgeführt. - Mit
raisekönnen eigene Fehler erzeugt werden. - Eigene Fehlerklassen werden für spezifische Fehler genutzt.
- Logging ermöglicht eine bessere Fehlerverfolgung.
Grundlagen der Objektorientierung
1 Erstellung einer einfachen Klasse
class Person:
"""Eine Klasse, die eine Person repräsentiert."""
def __init__(self, name, age):
"""Konstruktor für die Klasse Person."""
self.name = name
self.age = age
def introduce(self):
"""Gibt eine Vorstellung der Person aus."""
print(f"Hallo, mein Name ist {self.name} und ich bin {self.age} Jahre alt.")
# Objekt erstellen
person1 = Person("Alice", 30)
person1.introduce()
2 Vererbung in Python
class Student(Person):
"""Eine Klasse, die einen Studenten repräsentiert."""
def __init__(self, name, age, major):
"""Konstruktor für die Klasse Student."""
super().__init__(name, age) # Aufruf des Konstruktors der Elternklasse
self.major = major
def introduce(self):
"""Überschreibt die Methode der Elternklasse."""
print(f"Hallo, mein Name ist {self.name}, ich bin {self.age} Jahre alt und studiere {self.major}.")
# Objekt erstellen
student1 = Student("Bob", 22, "Informatik")
student1.introduce()
3 Mehrfachvererbung
class Worker:
"""Eine Klasse, die einen Arbeiter repräsentiert."""
def __init__(self, job):
"""Konstruktor für die Klasse Worker."""
self.job = job
def work(self):
"""Gibt den Beruf des Arbeiters aus."""
print(f"Ich arbeite als {self.job}.")
class StudentWorker(Student, Worker):
"""Eine Klasse für einen Studenten, der auch arbeitet."""
def __init__(self, name, age, major, job):
"""Konstruktor für die Klasse StudentWorker."""
Student.__init__(self, name, age, major)
Worker.__init__(self, job)
def introduce(self):
"""Erweitert die Vorstellungsmethode."""
print(f"Ich bin {self.name}, {self.age} Jahre alt, studiere {self.major} und arbeite als {self.job}.")
# Objekt erstellen
student_worker = StudentWorker("Clara", 25, "Maschinenbau", "Werkstudent")
student_worker.introduce()
4 Abstrakte Klassen
from abc import ABC, abstractmethod
class Animal(ABC):
"""Eine abstrakte Klasse für Tiere."""
@abstractmethod
def make_sound(self):
"""Abstrakte Methode, die von Unterklassen implementiert werden muss."""
pass
class Dog(Animal):
"""Eine Klasse, die einen Hund repräsentiert."""
def make_sound(self):
"""Implementiert die abstrakte Methode."""
print("Wuff Wuff!")
# Objekt erstellen
dog = Dog()
dog.make_sound()
5 Getter und Setter mit property
class BankAccount:
"""Eine Klasse für ein Bankkonto."""
def __init__(self, balance):
self._balance = balance # Geschützte Variable
@property
def balance(self) -> int:
"""Getter für den Kontostand."""
return self._balance
@balance.setter
def balance(self, amount):
"""Setter für den Kontostand mit Validierung."""
if amount < 0:
print("Fehler: Der Kontostand kann nicht negativ sein.")
else:
self._balance = amount
# Objekt erstellen
account = BankAccount(1000)
print(account.balance)
account.balance = 500
print(account.balance)
account.balance = -100 # Fehler
6 __str__ und __repr__ Methoden
class Car:
"""Eine Klasse für ein Auto."""
def __init__(self, brand, model):
self.brand = brand
self.model = model
def __str__(self):
"""Lesbare Darstellung des Objekts."""
return f"Auto: {self.brand} {self.model}"
def __repr__(self):
"""Detaillierte Darstellung für Entwickler."""
return f"Car('{self.brand}', '{self.model}')"
# Objekt erstellen
car = Car("BMW", "X5")
print(car) # __str__ Methode
print(repr(car)) # __repr__ Methode
7 Zusammenfassung
classdefiniert eine Klasse.__init__ist der Konstruktor.super()ruft Methoden der Elternklasse auf.- Abstrakte Klassen werden mit
ABCdefiniert. propertyermöglicht kontrollierten Zugriff auf Attribute.__str__und__repr__geben eine Darstellung des Objekts zurück.
Fortgeschrittene Objektorientierung
1 type(), isinstance() und issubclass()
Diese eingebauten Funktionen helfen beim Umgang mit Typen und Vererbung.
type(obj)gibt den exakten Typ des Objekts zurück.isinstance(obj, cls)prüft, obobjeine Instanz vonclsoder einer abgeleiteten Klasse ist.issubclass(sub, super)prüft, obsubeine Unterklasse vonsuperist.
class Animal: pass
class Dog(Animal): pass
a = Animal()
d = Dog()
print(type(d)) # <class '__main__.Dog'>
print(isinstance(d, Animal)) # True
print(issubclass(Dog, Animal)) # True
Diese Funktionen sind wichtig für dynamisches Verhalten, Validierung und Typprüfung.
2 __init__ vs __new__
__new__ist für das Erzeugen eines neuen Objekts zuständig.__init__wird anschließend aufgerufen, um das Objekt zu initialisieren.
class Custom:
def __new__(cls, *args, **kwargs):
print('Creating instance')
return super().__new__(cls)
def __init__(self, value):
print('Initializing with', value)
self.value = value
obj = Custom(42)
__new__ wird z. B. bei unveränderlichen Typen wie str oder tuple benötigt, wenn diese beeinflusst werden sollen oder in der Metaprogrammierung (Singleton-Pattern).
3 Methodenarten
3.1 StaticMethods und ClassMethods
@staticmethoddefiniert eine Methode, die keinen Zugriff aufselfoderclsbenötigt.@classmethodarbeitet mitclsund kann so auf die Klasse zugreifen.
class Example:
@staticmethod
def utility():
print('Static method called')
@classmethod
def construct(cls):
print(f'Creating instance of {cls.__name__}')
return cls()
Example.utility()
obj = Example.construct()
classmethod wird häufig für alternative Konstruktoren verwendet.
3.2 Properties (Private Attribute)
Mit @property können Methoden wie Attribute verwendet werden. Das ist nützlich für gekapselte Attribute, z. B. mit Validierung oder automatischer Berechnung.
class Person:
def __init__(self, name):
self._name = name # Private Konvention
@property
def name(self):
return self._name
@name.setter
def name(self, new):
self._name = new
p = Person('Alice')
print(p.name)
p.name = 'Bob'
print(p.name)
Durch Properties kann die API einfach bleiben, während intern komplexe Logik stattfinden kann.
4 Dunder Methods
Dunder (Double Underscore) Methoden ermöglichen benutzerdefiniertes Verhalten für Operatoren und eingebaute Funktionen (__str__, __len__, __getitem__ usw.).
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f'({self.x}, {self.y})'
def __eq__(self, other):
return self.x == other.x and self.y == other.y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(str(p1)) # (1, 2)
print(p1 == p2) # True
Diese Methoden machen Objekte “pythonisch”.
5 Abstraktion und Vererbung
5.1 Abstract Methods
Das abc-Modul erlaubt die Definition abstrakter Basisklassen. Methoden mit @abstractmethod müssen in Subklassen implementiert werden.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def area(self):
return 3.14 * 5 * 5
Abstrakte Methoden zwingen Unterklassen zur Implementierung und helfen beim Design stabiler APIs.
5.2 Method Resolution Order
Python verwendet das C3 Linearization-Verfahren, um die Reihenfolge von Vererbungen zu bestimmen. Die mro()-Methode zeigt die Aufrufreihenfolge.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.mro()) # [D, B, C, A, object]
Wichtig bei Mehrfachvererbung.
6 Interation und Indizierung
6.1 Iterator-Klasse und Generator-Funktion
Ein Iterator benötigt die Methoden __iter__ und __next__. Alternativ kann yield verwendet werden.
class Count:
def __init__(self, max):
self.max = max
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current >= self.max:
raise StopIteration
self.current += 1
return self.current
Generatoren vereinfachen Iteration:
def count_up_to(max):
current = 0
while current < max:
current += 1
yield current
6.2 Indexing-Klasse
Mithilfe von __getitem__ und __setitem__ können Objekte wie Listen verwendet werden.
class CustomList:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index]
def __setitem__(self, index, value):
self.data[index] = value
Sehr nützlich für eigene Containerklassen.
7 Datenpräsentation und Speicheroptimierung
7.1 Dataclass
Dataclasses automatisieren Konstruktor, Vergleich und Darstellung.
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
7.1.1 Slots
Mit __slots__ wird der Speicherverbrauch reduziert und der Zugriff beschleunigt.
class Slim:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
Seit Python 3.10 ist folgendes möglich (gleichwertiger Code):
@dataclass(slots=True)
class Slim:
x: int
y: int
was macht `__slots__` bzw. `slots=true`?
Normalerweise speichert Python Objektattribute in einem internen Dictionary namens __dict__. Das ist flexibel, jedoch nicht speichereffizient oder performant.
Wenn __slots__ bzw. slots=True verwendet wird, wird dieses Dictionary durch ein festeres Layout ersetzt, bei dem nur die im Slot definierten Felder erlaubt sind.
Warum verbessert das die Leistung?
-
Weniger Speicherverbrauch: Jedes Objekt braucht weniger Speicher, weil kein
__dict__mehr angelegt wird. -
Schnellerer Zugriff auf Attribute: Statt eines Dictionary-Lookups wird ein schnellerer, indexbasierter Zugriff verwendet.
-
Bessere Caching-Effekte: Schlankere Objekte passen besser in den Cache, was zu weiteren Geschwindigkeitsgewinnen führen kann – besonders bei großen Listen von Objekten.
# Ohne __slots__ (Standard)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# Mit __slots__
class PersonSlotted:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
Speichervergleich:
import sys
p1 = Person('Alice', 30)
p2 = PersonSlotted('Alice', 30)
print(sys.getsizeof(p1.__dict__)) # ~232 bytes
print(sys.getsizeof(p2)) # ~56 bytes
Vorteile von __slots__
1. Reduzierter Speicherverbrauch:
- Kein
__dict__pro Instanz - Besonders wichtig bei vielen Objekten (z.B. 1 Million Instanzen)
2. Schnellerer Attributzugriff:
- Direkter Array-Zugriff statt Dictionary-Lookup
- ~20-30% schneller
3. Typsicherheit:
- Nur definierte Attribute erlaubt
- Verhindert Tippfehler
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(10, 20)
p.z = 30 # AttributeError: 'Point' object has no attribute 'z'
7.1.2 frozen=True
Das frozen=True-Argument macht die Instanz unveränderlich (immutable):
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
print(p.x) # -> 1
p.x = 10 # -> Fehler: cannot assign to field 'x'
- Man kann keine Attribute mehr ändern nach der Erstellung der Instanz.
- Python generiert automatisch einen
setattr, der Änderungen verhindert. - Die Klasse wird hashbar (vorausgesetzt alle Felder sind auch hashbar), was z. B. erlaubt, sie als Schlüssel in einem
dictoder als Elemente in einemsetzu verwenden. - Die Verwendung von
frozen=Trueführt zu einer Leistungssteigerung, diese ist jedoch minimal.
7.2 Namedtuple
Ein namedtuple ist ein leichtgewichtiger, unveränderlicher Datenträger mit benannten Feldern.
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
print(p.x, p.y)
Effizient und leserlich – eine gute Alternative zu kleinen Klassen.
8 Enum
Enum erlaubt es, symbolische Konstanten mit Namen zu versehen.
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
Gleichbedeutend mit dem obigen Beispiel ist:
from enum import Enum
class Color(Enum):
RED = auto()
GREEN = auto()
BLUE = auto()
Enums verbessern die Lesbarkeit und Typsicherheit im Code.
9 Class Decorator
Class Decorators modifizieren Klassen beim Erzeugen. Sie eignen sich für Registrierung, Debugging oder Vererbung.
def debug(cls):
original_init = cls.__init__
def new_init(self, *args, **kwargs):
print(f'Creating {cls.__name__} with {args}, {kwargs}')
original_init(self, *args, **kwargs)
cls.__init__ = new_init
return cls
@debug
class Product:
def __init__(self, name):
self.name = name
p = Product('Book')
Class Decorators sind ein mächtiges Meta-Programmierungstool.
10 Metaclasses – Klassen von Klassen
Metaclasses sind ein fortgeschrittenes Feature, mit dem man das Verhalten beim Erstellen von Klassen selbst kontrollieren kann. Sie sind die “Klassen von Klassen”.
10.1 Grundkonzept
In Python ist alles ein Objekt – auch Klassen. Klassen sind Instanzen von Metaclasses.
# Normale Hierarchie
class Dog:
pass
dog = Dog()
# Was ist was?
print(type(dog)) # <class '__main__.Dog'> - dog ist Instanz von Dog
print(type(Dog)) # <class 'type'> - Dog ist Instanz von type
print(type(type)) # <class 'type'> - type ist seine eigene Metaclass
# isinstance-Checks
print(isinstance(dog, Dog)) # True
print(isinstance(Dog, type)) # True
Die Kette:
dog → Instanz von → Dog → Instanz von → type (Metaclass)
10.2 type() als Metaclass
type kann auf zwei Arten verwendet werden:
1. Als Funktion (Typ abfragen):
x = 5
print(type(x)) # <class 'int'>
2. Als Metaclass (Klasse erstellen):
# Normale Klassendefinition
class Dog:
def bark(self):
return "Woof!"
# Äquivalent mit type()
Dog = type('Dog', (), {'bark': lambda self: "Woof!"})
dog = Dog()
print(dog.bark()) # "Woof!"
Syntax von type() zur Klassenerstellung:
type(name, bases, dict)
name: Klassenname (String)bases: Tuple der Basisklassendict: Dictionary mit Attributen und Methoden
Beispiel mit Vererbung:
# Basisklasse
class Animal:
def breathe(self):
return "Breathing..."
# Mit type() erstellen
Dog = type(
'Dog', # Name
(Animal,), # Basisklassen
{
'species': 'Canis familiaris',
'bark': lambda self: "Woof!"
}
)
dog = Dog()
print(dog.breathe()) # "Breathing..." (geerbt)
print(dog.bark()) # "Woof!"
print(dog.species) # "Canis familiaris"
10.3 Eigene Metaclass erstellen
Eine Metaclass ist eine Klasse, die von type erbt.
class Meta(type):
def __new__(mcs, name, bases, attrs):
print(f"Creating class {name}")
# Klasse erstellen
cls = super().__new__(mcs, name, bases, attrs)
return cls
class MyClass(metaclass=Meta):
pass
# Output beim Import/Ausführung:
# Creating class MyClass
Parameter von __new__:
mcs: Die Metaclass selbst (wieclsbei@classmethod)name: Name der zu erstellenden Klassebases: Tuple der Basisklassenattrs: Dictionary der Klassenattribute
10.4 __new__ vs. __init__ in Metaclasses
class Meta(type):
def __new__(mcs, name, bases, attrs):
"""Wird WÄHREND der Klassenerstellung aufgerufen"""
print(f"__new__: Creating {name}")
cls = super().__new__(mcs, name, bases, attrs)
return cls
def __init__(cls, name, bases, attrs):
"""Wird NACH der Klassenerstellung aufgerufen"""
print(f"__init__: Initializing {name}")
super().__init__(name, bases, attrs)
class MyClass(metaclass=Meta):
pass
# Output:
# __new__: Creating MyClass
# __init__: Initializing MyClass
Wann was verwenden:
__new__: Wenn man die Klasse vor ihrer Erstellung modifizieren will__init__: Wenn man die Klasse nach ihrer Erstellung modifizieren will
10.5 Praktische Anwendungsfälle
10.5.1 Attribute validieren
class ValidatedMeta(type):
def __new__(mcs, name, bases, attrs):
# Alle Attribute müssen mit Großbuchstaben beginnen
for key in attrs:
if not key.startswith('_'): # Private ignorieren
if not key[0].isupper():
raise ValueError(
f"Attribute {key} must start with uppercase letter"
)
return super().__new__(mcs, name, bases, attrs)
# ✅ Funktioniert
class GoodClass(metaclass=ValidatedMeta):
Name = "valid"
Age = 25
# ❌ Fehler
# class BadClass(metaclass=ValidatedMeta):
# name = "invalid" # ValueError!
10.5.2 Automatische Registrierung
class RegistryMeta(type):
_registry = {}
def __new__(mcs, name, bases, attrs):
cls = super().__new__(mcs, name, bases, attrs)
# Klasse automatisch registrieren
mcs._registry[name] = cls
return cls
@classmethod
def get_registry(mcs):
return mcs._registry
class Plugin(metaclass=RegistryMeta):
pass
class AudioPlugin(Plugin):
pass
class VideoPlugin(Plugin):
pass
# Alle Plugins automatisch registriert
print(RegistryMeta.get_registry())
# {'Plugin': <class '__main__.Plugin'>,
# 'AudioPlugin': <class '__main__.AudioPlugin'>,
# 'VideoPlugin': <class '__main__.VideoPlugin'>}
10.5.3 Singleton-Pattern
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self):
print("Connecting to database...")
# Erste Instanz
db1 = Database() # "Connecting to database..."
# Zweite "Instanz" - gibt dieselbe zurück
db2 = Database() # (kein Output)
print(db1 is db2) # True
10.5.4 Automatische __repr__ Methode
class AutoReprMeta(type):
def __new__(mcs, name, bases, attrs):
# Automatisch __repr__ generieren
def auto_repr(self):
attrs_str = ', '.join(
f"{k}={v!r}"
for k, v in self.__dict__.items()
)
return f"{name}({attrs_str})"
# Nur hinzufügen, wenn nicht vorhanden
if '__repr__' not in attrs:
attrs['__repr__'] = auto_repr
return super().__new__(mcs, name, bases, attrs)
class Point(metaclass=AutoReprMeta):
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(10, 20)
print(p) # Point(x=10, y=20) - automatisch generiert!
10.5.5 Interface/Abstract Base Class erzwingen
class InterfaceMeta(type):
def __new__(mcs, name, bases, attrs):
# Prüfe, ob alle abstrakten Methoden implementiert sind
if bases: # Nicht für die Basisklasse selbst
for base in bases:
if hasattr(base, '_required_methods'):
for method in base._required_methods:
if method not in attrs:
raise TypeError(
f"{name} must implement {method}()"
)
return super().__new__(mcs, name, bases, attrs)
class Shape(metaclass=InterfaceMeta):
_required_methods = ['area', 'perimeter']
# ❌ Fehler - area fehlt
# class Circle(Shape):
# def perimeter(self):
# return 2 * 3.14 * self.radius
# ✅ Funktioniert
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
def perimeter(self):
return 2 * 3.14 * self.radius
10.6 Metaclass-Vererbung
class MetaA(type):
def __new__(mcs, name, bases, attrs):
print(f"MetaA creating {name}")
return super().__new__(mcs, name, bases, attrs)
class MetaB(MetaA):
def __new__(mcs, name, bases, attrs):
print(f"MetaB creating {name}")
return super().__new__(mcs, name, bases, attrs)
class MyClass(metaclass=MetaB):
pass
# Output:
# MetaB creating MyClass
# MetaA creating MyClass
10.7 __call__ in Metaclasses
__call__ wird aufgerufen, wenn eine Instanz der Klasse erstellt wird.
class CounterMeta(type):
def __init__(cls, name, bases, attrs):
super().__init__(name, bases, attrs)
cls._instance_count = 0
def __call__(cls, *args, **kwargs):
# Wird bei MyClass() aufgerufen
instance = super().__call__(*args, **kwargs)
cls._instance_count += 1
print(f"Created instance #{cls._instance_count}")
return instance
class MyClass(metaclass=CounterMeta):
pass
obj1 = MyClass() # Created instance #1
obj2 = MyClass() # Created instance #2
obj3 = MyClass() # Created instance #3
print(MyClass._instance_count) # 3
10.8 __prepare__ – Dictionary für Klassenattribute vorbereiten
__prepare__ bestimmt, welches Dictionary für die Klassenattribute verwendet wird (normalerweise ein normales dict).
from collections import OrderedDict
class OrderedMeta(type):
@classmethod
def __prepare__(mcs, name, bases):
"""Wird VOR __new__ aufgerufen"""
print(f"Preparing namespace for {name}")
return OrderedDict()
def __new__(mcs, name, bases, namespace):
print(f"Attributes in order: {list(namespace.keys())}")
return super().__new__(mcs, name, bases, dict(namespace))
class MyClass(metaclass=OrderedMeta):
z = 3
a = 1
m = 2
# Output:
# Preparing namespace for MyClass
# Attributes in order: ['__module__', '__qualname__', 'z', 'a', 'm']
10.9 Metaclass-Konflikte vermeiden
Bei Mehrfachvererbung können Metaclass-Konflikte auftreten:
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
class B(metaclass=MetaB):
pass
# ❌ Fehler: metaclass conflict
# class C(A, B):
# pass
# ✅ Lösung: Gemeinsame Metaclass
class MetaC(MetaA, MetaB):
pass
class C(A, B, metaclass=MetaC):
pass
10.10 Metaclasses vs. Alternativen
Metaclasses sind mächtig, aber oft gibt es einfachere Alternativen.
10.10.1 Class Decorators
# Mit Metaclass
class AutoStrMeta(type):
def __new__(mcs, name, bases, attrs):
def __str__(self):
return f"{name} instance"
attrs['__str__'] = __str__
return super().__new__(mcs, name, bases, attrs)
class MyClass(metaclass=AutoStrMeta):
pass
# Mit Decorator (einfacher!)
def auto_str(cls):
def __str__(self):
return f"{cls.__name__} instance"
cls.__str__ = __str__
return cls
@auto_str
class MyClass:
pass
10.10.2 __init_subclass__ (Python 3.6+)
# Mit Metaclass
class RegistryMeta(type):
_registry = []
def __new__(mcs, name, bases, attrs):
cls = super().__new__(mcs, name, bases, attrs)
mcs._registry.append(cls)
return cls
# Mit __init_subclass__ (moderner!)
class Plugin:
_registry = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls._registry.append(cls)
class AudioPlugin(Plugin):
pass
print(Plugin._registry) # [<class 'AudioPlugin'>]
10.11 Debugging von Metaclasses
class DebugMeta(type):
def __new__(mcs, name, bases, attrs):
print(f"\n=== Creating class {name} ===")
print(f"Metaclass: {mcs}")
print(f"Bases: {bases}")
print(f"Attributes: {list(attrs.keys())}")
cls = super().__new__(mcs, name, bases, attrs)
print(f"Class created: {cls}")
print(f"MRO: {cls.__mro__}")
return cls
class Parent:
parent_attr = "parent"
class Child(Parent, metaclass=DebugMeta):
child_attr = "child"
# Output zeigt detaillierte Informationen über Klassenerstellung
10.12 Best Practices
✅ Wann Metaclasses verwenden:
- Framework-/Library-Entwicklung
- Automatische Registrierung/Plugin-Systeme
- Enforcing von Code-Standards
- DSL (Domain Specific Language) Implementation
- Komplexe ORM-Systeme (wie Django Models)
❌ Wann NICHT verwenden:
- Für alltägliche Programmierung
- Wenn Class Decorators ausreichen
- Wenn
__init_subclass__ausreicht - Wenn es den Code unleserlich macht
Alternativen prüfen:
- Class Decorators (meistens ausreichend)
__init_subclass__(Python 3.6+)- Descriptor Protocol
- Erst dann: Metaclasses
Zitat von Tim Peters:
“Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t.”
10.13 Zusammenfassung
| Konzept | Beschreibung |
|---|---|
type | Standard-Metaclass aller Klassen |
__new__ | Klasse während Erstellung modifizieren |
__init__ | Klasse nach Erstellung initialisieren |
__call__ | Instanzerstellung kontrollieren |
__prepare__ | Namespace-Dictionary vorbereiten |
metaclass= | Custom Metaclass zuweisen |
Kernprinzip: Metaclasses kontrollieren die Klassenerstellung selbst. Sie sind ein sehr mächtiges Werkzeug, sollten aber sparsam eingesetzt werden. In den meisten Fällen sind Class Decorators oder __init_subclass__ die bessere Wahl.
Entscheidungsbaum:
- Brauche ich wirklich Metaprogrammierung? → Oft: Nein
- Reicht ein Class Decorator? → Meistens: Ja
- Reicht
__init_subclass__? → Oft: Ja - Brauche ich volle Kontrolle über Klassenerstellung? → Dann: Metaclass
11 Itertools – Leistungsstarke Iterator-Werkzeuge
Das itertools-Modul bietet spezialisierte Iterator-Funktionen für effiziente Schleifen und funktionale Programmierung. Alle Funktionen sind speichereffizient, da sie Iteratoren statt Listen zurückgeben.
11.1 Unendliche Iteratoren
11.1.1 count() – Unendliches Zählen
from itertools import count
# Zählt ab 10 in 2er-Schritten
counter = count(start=10, step=2)
for i in counter:
if i > 20:
break
print(i) # 10, 12, 14, 16, 18, 20
# Mit zip für begrenzte Iteration
for i, letter in zip(count(1), ['a', 'b', 'c']):
print(f"{i}: {letter}")
# 1: a
# 2: b
# 3: c
11.1.2 cycle() – Elemente wiederholen
from itertools import cycle
# Zyklisch durch Elemente iterieren
colors = cycle(['red', 'green', 'blue'])
for i, color in enumerate(colors):
if i >= 7:
break
print(color)
# red, green, blue, red, green, blue, red
Praktisches Beispiel – Zeilen abwechselnd einfärben:
from itertools import cycle
rows = ['Row 1', 'Row 2', 'Row 3', 'Row 4', 'Row 5']
colors = cycle(['white', 'gray'])
for row, color in zip(rows, colors):
print(f"{row} - {color}")
11.1.3 repeat() – Element wiederholen
from itertools import repeat
# Unbegrenzt
for item in repeat('X'):
print(item) # X, X, X, ... (unendlich)
break
# Begrenzt
for item in repeat('X', 3):
print(item) # X, X, X
# Praktisch mit map
result = list(map(pow, [2, 3, 4], repeat(3)))
print(result) # [8, 27, 64] (2³, 3³, 4³)
11.2 Kombinatorische Iteratoren
11.2.1 product() – Kartesisches Produkt
from itertools import product
# Alle Kombinationen
colors = ['red', 'blue']
sizes = ['S', 'M', 'L']
for color, size in product(colors, sizes):
print(f"{color}-{size}")
# red-S, red-M, red-L, blue-S, blue-M, blue-L
# Äquivalent zu verschachtelten Schleifen
for color in colors:
for size in sizes:
print(f"{color}-{size}")
# Mit repeat-Parameter
for item in product(range(2), repeat=3):
print(item)
# (0,0,0), (0,0,1), (0,1,0), (0,1,1), (1,0,0), (1,0,1), (1,1,0), (1,1,1)
11.2.2 permutations() – Permutationen
from itertools import permutations
# Alle Anordnungen von 3 Elementen
items = ['A', 'B', 'C']
for perm in permutations(items):
print(perm)
# ('A','B','C'), ('A','C','B'), ('B','A','C'), ('B','C','A'), ('C','A','B'), ('C','B','A')
# Permutationen mit Länge 2
for perm in permutations(items, 2):
print(perm)
# ('A','B'), ('A','C'), ('B','A'), ('B','C'), ('C','A'), ('C','B')
# Anzahl: n! / (n-r)! für Länge r
import math
n, r = 3, 2
count = math.factorial(n) // math.factorial(n - r)
print(count) # 6
11.2.3 combinations() – Kombinationen (ohne Wiederholung)
from itertools import combinations
# Alle 2er-Kombinationen
items = ['A', 'B', 'C', 'D']
for combo in combinations(items, 2):
print(combo)
# ('A','B'), ('A','C'), ('A','D'), ('B','C'), ('B','D'), ('C','D')
# Anzahl: n! / (r! * (n-r)!)
import math
n, r = 4, 2
count = math.factorial(n) // (math.factorial(r) * math.factorial(n - r))
print(count) # 6
11.2.4 combinations_with_replacement() – Kombinationen mit Wiederholung
from itertools import combinations_with_replacement
items = ['A', 'B', 'C']
for combo in combinations_with_replacement(items, 2):
print(combo)
# ('A','A'), ('A','B'), ('A','C'), ('B','B'), ('B','C'), ('C','C')
11.3 Terminierende Iteratoren
11.3.1 chain() – Iterables verketten
from itertools import chain
# Mehrere Iterables zu einem kombinieren
list1 = [1, 2, 3]
list2 = [4, 5, 6]
list3 = [7, 8, 9]
for item in chain(list1, list2, list3):
print(item) # 1, 2, 3, 4, 5, 6, 7, 8, 9
# Äquivalent zu:
result = list1 + list2 + list3
# chain.from_iterable für verschachtelte Iterables
nested = [[1, 2], [3, 4], [5, 6]]
flattened = list(chain.from_iterable(nested))
print(flattened) # [1, 2, 3, 4, 5, 6]
11.3.2 compress() – Filtern mit Boolean-Mask
from itertools import compress
data = ['A', 'B', 'C', 'D', 'E']
selectors = [True, False, True, False, True]
result = list(compress(data, selectors))
print(result) # ['A', 'C', 'E']
# Praktisches Beispiel
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
is_even = [n % 2 == 0 for n in numbers]
evens = list(compress(numbers, is_even))
print(evens) # [2, 4, 6, 8, 10]
11.3.3 dropwhile() und takewhile() – Bedingte Iteration
from itertools import dropwhile, takewhile
data = [1, 4, 6, 4, 1]
# dropwhile: Überspringt bis Bedingung False wird
result = list(dropwhile(lambda x: x < 5, data))
print(result) # [6, 4, 1] (ab erstem x >= 5)
# takewhile: Nimmt bis Bedingung False wird
result = list(takewhile(lambda x: x < 5, data))
print(result) # [1, 4] (bis erstes x >= 5)
11.3.4 filterfalse() – Umgekehrtes filter()
from itertools import filterfalse
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# filter gibt True-Werte zurück
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4, 6, 8, 10]
# filterfalse gibt False-Werte zurück
odds = list(filterfalse(lambda x: x % 2 == 0, numbers))
print(odds) # [1, 3, 5, 7, 9]
11.3.5 groupby() – Gruppieren nach Schlüssel
from itertools import groupby
# Daten müssen nach Gruppierungsschlüssel SORTIERT sein!
data = [
('Alice', 'A'),
('Bob', 'B'),
('Charlie', 'C'),
('David', 'A'),
('Eve', 'B')
]
# Nach zweitem Element gruppieren (NICHT sortiert → falsche Gruppen!)
for key, group in groupby(data, key=lambda x: x[1]):
print(f"{key}: {list(group)}")
# A: [('Alice', 'A')]
# B: [('Bob', 'B')]
# C: [('Charlie', 'C')]
# A: [('David', 'A')] ← Neue Gruppe!
# B: [('Eve', 'B')] ← Neue Gruppe!
# Richtig: Erst sortieren
data_sorted = sorted(data, key=lambda x: x[1])
for key, group in groupby(data_sorted, key=lambda x: x[1]):
print(f"{key}: {list(group)}")
# A: [('Alice', 'A'), ('David', 'A')]
# B: [('Bob', 'B'), ('Eve', 'B')]
# C: [('Charlie', 'C')]
Praktisches Beispiel – Nach Länge gruppieren:
from itertools import groupby
words = ['a', 'bb', 'ccc', 'dd', 'e', 'fff']
words_sorted = sorted(words, key=len)
for length, group in groupby(words_sorted, key=len):
print(f"Length {length}: {list(group)}")
# Length 1: ['a', 'e']
# Length 2: ['bb', 'dd']
# Length 3: ['ccc', 'fff']
11.3.6 islice() – Slice für Iteratoren
from itertools import islice, count
# Wie list-slicing, aber für Iteratoren
data = range(10)
# islice(iterable, stop)
result = list(islice(data, 5))
print(result) # [0, 1, 2, 3, 4]
# islice(iterable, start, stop)
result = list(islice(data, 2, 7))
print(result) # [2, 3, 4, 5, 6]
# islice(iterable, start, stop, step)
result = list(islice(data, 0, 10, 2))
print(result) # [0, 2, 4, 6, 8]
# Sehr nützlich für unendliche Iteratoren
result = list(islice(count(10), 5))
print(result) # [10, 11, 12, 13, 14]
11.3.7 starmap() – map mit Argument-Unpacking
from itertools import starmap
# map wendet Funktion auf einzelne Elemente an
result = list(map(pow, [2, 3, 4], [5, 2, 3]))
print(result) # [32, 9, 64]
# starmap entpackt Tupel als Argumente
data = [(2, 5), (3, 2), (4, 3)]
result = list(starmap(pow, data))
print(result) # [32, 9, 64]
# Praktisches Beispiel
points = [(1, 2), (3, 4), (5, 6)]
result = list(starmap(lambda x, y: x + y, points))
print(result) # [3, 7, 11]
11.3.8 tee() – Iterator duplizieren
from itertools import tee
data = range(5)
it1, it2 = tee(data, 2) # 2 unabhängige Kopien
# Beide können unabhängig verwendet werden
print(list(it1)) # [0, 1, 2, 3, 4]
print(list(it2)) # [0, 1, 2, 3, 4]
# Warnung: Original-Iterator nicht mehr verwenden!
11.3.9 zip_longest() – Zip mit Auffüllung
from itertools import zip_longest
# Normales zip stoppt bei kürzester Sequenz
a = [1, 2, 3]
b = ['a', 'b']
print(list(zip(a, b))) # [(1, 'a'), (2, 'b')]
# zip_longest füllt mit None auf
print(list(zip_longest(a, b)))
# [(1, 'a'), (2, 'b'), (3, None)]
# Mit custom fillvalue
print(list(zip_longest(a, b, fillvalue='?')))
# [(1, 'a'), (2, 'b'), (3, '?')]
11.4 Akkumulatoren
11.4.1 accumulate() – Kumulative Werte
from itertools import accumulate
import operator
# Standardmäßig: Addition
numbers = [1, 2, 3, 4, 5]
result = list(accumulate(numbers))
print(result) # [1, 3, 6, 10, 15] (laufende Summe)
# Mit custom Operation
result = list(accumulate(numbers, operator.mul))
print(result) # [1, 2, 6, 24, 120] (laufendes Produkt)
# Maximum-Tracking
numbers = [5, 2, 8, 1, 9, 3]
result = list(accumulate(numbers, max))
print(result) # [5, 5, 8, 8, 9, 9]
# Mit Lambda
result = list(accumulate(numbers, lambda x, y: x if x > y else y))
print(result) # [5, 5, 8, 8, 9, 9]
11.5 Praktische Kombinationen
11.5.1 Paarweise Iteration
from itertools import tee
def pairwise(iterable):
"""s -> (s0,s1), (s1,s2), (s2,s3), ..."""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
# Verwendung
data = [1, 2, 3, 4, 5]
for pair in pairwise(data):
print(pair)
# (1, 2), (2, 3), (3, 4), (4, 5)
# Ab Python 3.10 eingebaut:
from itertools import pairwise
for pair in pairwise(data):
print(pair)
11.5.2 Fenster-Iteration (Sliding Window)
from itertools import islice
def sliding_window(iterable, n):
"""Gleitet mit Fenster der Größe n über iterable"""
iterators = tee(iterable, n)
for i, it in enumerate(iterators):
for _ in range(i):
next(it, None)
return zip(*iterators)
# Verwendung
data = [1, 2, 3, 4, 5, 6]
for window in sliding_window(data, 3):
print(window)
# (1, 2, 3), (2, 3, 4), (3, 4, 5), (4, 5, 6)
11.5.3 Batching (Chunks)
from itertools import islice
def batched(iterable, n):
"""iterable -> [chunk1, chunk2, ...]"""
iterator = iter(iterable)
while True:
batch = list(islice(iterator, n))
if not batch:
break
yield batch
# Verwendung
data = range(10)
for batch in batched(data, 3):
print(batch)
# [0, 1, 2], [3, 4, 5], [6, 7, 8], [9]
# Ab Python 3.12 eingebaut:
# from itertools import batched
11.5.4 Flatten (Verschachtelung auflösen)
from itertools import chain
def flatten(nested_list):
"""[[1,2], [3,4]] -> [1, 2, 3, 4]"""
return chain.from_iterable(nested_list)
nested = [[1, 2], [3, 4], [5, 6]]
result = list(flatten(nested))
print(result) # [1, 2, 3, 4, 5, 6]
# Für beliebige Verschachtelung (rekursiv)
def deep_flatten(nested):
for item in nested:
if isinstance(item, (list, tuple)):
yield from deep_flatten(item)
else:
yield item
deeply_nested = [1, [2, [3, [4, 5]]]]
result = list(deep_flatten(deeply_nested))
print(result) # [1, 2, 3, 4, 5]
11.6 Performance-Vorteile
import time
from itertools import islice, count
# ❌ Ineffizient: Liste erstellen
start = time.time()
large_list = list(range(10_000_000))
first_100 = large_list[:100]
print(f"List: {time.time() - start:.3f}s")
# ✅ Effizient: Iterator verwenden
start = time.time()
first_100 = list(islice(count(), 100))
print(f"Iterator: {time.time() - start:.3f}s")
# Speichervergleich
import sys
numbers_list = list(range(1_000_000))
numbers_iter = range(1_000_000)
print(f"List: {sys.getsizeof(numbers_list)} bytes") # ~8 MB
print(f"Iterator: {sys.getsizeof(numbers_iter)} bytes") # ~48 bytes
11.7 Best Practices
✅ DO:
- Nutze Iteratoren für große Datenmengen (speichereffizient)
- Kombiniere itertools-Funktionen für komplexe Operationen
- Sortiere Daten vor
groupby() - Verwende
chain.from_iterable()statt verschachtelter Loops
❌ DON’T:
- Konvertiere Iteratoren nicht unnötig zu Listen
- Vergiss nicht, dass Iteratoren nur einmal durchlaufen werden können
- Verwende
groupby()nicht ohne vorheriges Sortieren - Original-Iterator nach
tee()nicht weiterverwenden
11.8 Zusammenfassung
| Funktion | Zweck | Rückgabe |
|---|---|---|
count() | Unendliches Zählen | 10, 11, 12, … |
cycle() | Elemente zyklisch wiederholen | A, B, C, A, B, … |
repeat() | Element wiederholen | X, X, X, … |
product() | Kartesisches Produkt | (A,1), (A,2), … |
permutations() | Alle Anordnungen | (A,B), (B,A), … |
combinations() | Kombinationen ohne Wiederholung | (A,B), (A,C), … |
chain() | Iterables verketten | 1, 2, 3, 4, … |
compress() | Filtern mit Boolean-Mask | [True], [False], … |
groupby() | Gruppieren (nach Sortierung!) | Gruppen nach Key |
islice() | Slice für Iteratoren | Teilbereich |
accumulate() | Kumulative Werte | 1, 3, 6, 10, … |
Kernprinzip: itertools bietet speichereffiziente, kombinierbare Iterator-Funktionen für funktionale Programmierung und große Datenmengen. Iteratoren sind lazy (verzögerte Auswertung) und können nur einmal durchlaufen werden.
12 Zusammenfassung
-
Typprüfung und Instanzen
- Mit
type(),isinstance()undissubclass()prüft man Objekttypen und Vererbungsbeziehungen.
- Mit
-
Objekt-Erzeugung
__new__erzeugt das Objekt (besonders bei Immutable-Typen wichtig).__init__initialisiert das Objekt nach der Erzeugung.
-
Methodenarten
@staticmethod: Kein Zugriff auf Klassen- oder Instanzdaten.@classmethod: Zugriff auf die Klasse (cls).@property: Ermöglicht kontrollierten Zugriff auf Attribute wie bei einem Feld.
-
Spezialmethoden (Dunder Methods)
- Methoden wie
__str__,__eq__,__getitem__erlauben es, benutzerdefinierte Objekte wie eingebaute Typen zu behandeln.
- Methoden wie
-
Abstraktion und Vererbung
- Abstrakte Klassen (
ABC,@abstractmethod) erzwingen Implementierungen in Unterklassen. - Die Method Resolution Order (MRO) bestimmt die Aufrufreihenfolge bei Mehrfachvererbung.
- Abstrakte Klassen (
-
Iteration und Indexierung
- Iterator-Klassen und Generator-Funktionen ermöglichen eigene Iterationslogik.
- Mit
__getitem__und__setitem__lassen sich Objekte wie Listen verwenden.
-
Speicher- und Datenrepräsentation
@dataclassreduziert Boilerplate für Datenobjekte.__slots__spart Speicher durch festen Attributsatz.frozen=Truemacht die Dataclass unveränderbar.
namedtupleist eine kompakte, unveränderliche Datenstruktur mit Feldnamen.
-
Enumerationen
- Mit
Enumkann man symbolische Konstanten definieren, die lesbar und typsicher sind.
- Mit
-
Klassen-Dekoratoren
- Decorators für Klassen können beim Erzeugen einer Klasse deren Verhalten ändern oder erweitern – ideal für Logging, Validierung oder automatische Registrierung.
Fortgeschrittene Funktionstechniken
Closures, Decorators und verwandte Konzepte sind Kernelemente der funktionalen Programmierung in Python. Sie ermöglichen sauberen, wiederverwendbaren und eleganten Code.
1 Closures
Ein Closure ist eine verschachtelte Funktion, die auf Variablen der äußeren Funktion auch nach deren Ausführung noch zugreifen kann. Dadurch entsteht ein erhaltener Zustand ohne Verwendung von Klassen.
1.1 Beispiel: Einfaches Closure
def greeting(name):
# innere Funktion greift auf 'name' zu
def say_hello():
print(f'Hallo {name}!')
return say_hello
greet = greeting('Stefan')
greet() # Ausgabe: Hallo Stefan!
1.2 Beispiel: Closure mit Zustand
def counter():
count = 0
def increment():
# Zugriff auf äußere Variable mit nonlocal
nonlocal count
count += 1
return count
return increment
c = counter()
print(c()) # 1
print(c()) # 2
2 Lambda-Funktionen
Lambda-Funktionen sind anonyme Funktionen, meist in einer Zeile geschrieben. Sie sind besonders praktisch in Kombination mit map, filter und anderen höherwertigen Funktionen.
Syntax:
lambda arguments: expression
Beispiel:
add = lambda x, y: x + y
print(add(3, 5)) # 8
3 Higher-Order Functions
Eine Higher-Order Function ist eine Funktion, die andere Funktionen entgegennimmt oder zurückgibt.
def apply_twice(func, value):
return func(func(value))
print(apply_twice(lambda x: x + 1, 3)) # 5
4 map, filter, reduce
map(func, iterable): Wendetfuncauf jedes Element an.filter(func, iterable): Filtert Elemente basierend auf Wahrheitswert vonfunc.reduce(func, iterable, initial): Akkumuliert alle Elemente zu einem einzigen Wert.
numbers = [1, 2, 3, 4, 5]
# map: Transformation
squared = list(map(lambda x: x**2, numbers)) # [1, 4, 9, 16, 25]
# filter: Bedingung
even = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]
# reduce: Akkumulation
from functools import reduce
summed = reduce(lambda acc, x: acc + x, numbers, 0) # 15
5 Partial Functions
Vorkonfigurierte Funktionen mit functools.partial erlauben das Vorbelegen von Argumenten einer Funktion. Das ist nützlich, wenn man spezialisierte Varianten einer Funktion benötigt.
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(2)) # 8
partialerstellt eine neue Funktion mit fixierten Parametern.- Hilfreich z. B. beim Konfigurieren von Callbacks oder API-Funktionen.
6 Function Factories (Funktions-Fabriken)
Funktionen, die andere Funktionen erzeugen – meist Closures. Ideal, wenn man eine Reihe verwandter Funktionen mit leicht unterschiedlichem Verhalten benötigt.
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
print(double(10)) # 20
factorbleibt in jeder zurückgegebenen Funktion erhalten.- Praktisch bei mathematischen Operationen, Filtern, usw.
7 Decorators
Ein Decorator erweitert das Verhalten einer Funktion, ohne ihren Code zu verändern. Er basiert auf Closures und Higher-Order Functions.
7.1 Einfacher Decorator
def my_decorator(func):
def wrapper():
print('Before the function runs.')
func()
print('After the function runs.')
return wrapper
@my_decorator
def say_hello():
print('Hello!')
say_hello()
@my_decoratorersetztsay_hellodurchwrapper.- Ideal für Logging, Fehlerbehandlung, Zeitmessung etc.
7.2 Decorators mit Argumenten
Decorator-Funktionen können Argumente übernehmen, wenn sie flexibel konfiguriert werden sollen. Dazu ist eine zusätzliche Verschachtelung notwendig.
def logger(func):
def wrapper(*args, **kwargs):
print(f'Arguments: {args}, {kwargs}')
return func(*args, **kwargs)
return wrapper
@logger
def add(x, y):
return x + y
add(3, 5)
*argsund**kwargsfangen alle Argumente ab.- So bleiben Decorators universell einsetzbar.
7.3 Decorators mit Parametern
def speaker(volume):
def decorator(func):
def wrapper():
print(f'[{volume.upper()}]')
func()
return wrapper
return decorator
@speaker('quiet')
def whisper():
print('psst...')
whisper()
speaker("quiet")gibt den eigentlichen Decorator zurück.- Mehr Flexibilität durch parametrisierte Dekoration.
7.4 Eingebaute Decorators
@staticmethod: Definiert eine Methode ohneself@classmethod: Zugriff auf Klasse statt Instanz@property: Erlaubt methodenartigen Zugriff auf Attribute
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14 * self._radius ** 2
7.5 Mehrere Decorators kombinieren
@decorator_one
@decorator_two
def some_function():
pass
decorator_twowird zuerst angewendet, danndecorator_one.- Reihenfolge beachten, wenn Decorators interagieren.
8 functools – Höherwertige Funktionen und Decorators
Das functools-Modul bietet Werkzeuge für funktionale Programmierung und erweiterte Decorator-Funktionalität.
8.1 lru_cache – Memoization
lru_cache (Least Recently Used Cache) speichert Funktionsergebnisse und vermeidet redundante Berechnungen.
8.1.1 Grundlegende Verwendung
from functools import lru_cache
@lru_cache
def fibonacci(n):
if n in (0, 1):
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # Instant, ohne Cache würde das ewig dauern
# Cache-Statistiken
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
8.1.2 Cache-Größe konfigurieren
from functools import lru_cache
# Maximale Cache-Größe festlegen
@lru_cache(maxsize=128)
def expensive_computation(x, y):
return x ** y
# Unbegrenzter Cache
@lru_cache(maxsize=None)
def compute(x):
return x * 2
# Cache leeren
expensive_computation.cache_clear()
8.1.3 Wann lru_cache verwenden?
✅ Gut für:
- Rekursive Funktionen (Fibonacci, Factorial)
- Teure Berechnungen mit wiederholten Inputs
- Datenbankabfragen mit gleichen Parametern
- API-Calls mit identischen Requests
❌ Nicht verwenden bei:
- Funktionen mit mutable Argumenten (Listen, Dicts)
- Funktionen mit Seiteneffekten
- Sehr großen oder seltenen Inputs
8.1.4 cache vs. lru_cache
from functools import cache, lru_cache
# cache: Unbegrenzter Cache (Python 3.9+)
@cache
def compute(x):
return x ** 2
# Äquivalent zu:
@lru_cache(maxsize=None)
def compute(x):
return x ** 2
8.1.5 Performance-Beispiel
import time
from functools import lru_cache
# Ohne Cache
def fib_slow(n):
if n < 2:
return n
return fib_slow(n-1) + fib_slow(n-2)
# Mit Cache
@lru_cache
def fib_fast(n):
if n < 2:
return n
return fib_fast(n-1) + fib_fast(n-2)
# Vergleich
start = time.time()
fib_slow(30)
print(f"Ohne Cache: {time.time() - start:.3f}s") # ~0.3s
start = time.time()
fib_fast(30)
print(f"Mit Cache: {time.time() - start:.6f}s") # ~0.000050s
8.2 wraps – Decorator-Metadaten erhalten
wraps erhält die Metadaten der ursprünglichen Funktion beim Dekorieren.
from functools import wraps
# ❌ Ohne wraps: Metadaten gehen verloren
def bad_decorator(func):
def wrapper(*args, **kwargs):
"""Wrapper docstring"""
return func(*args, **kwargs)
return wrapper
@bad_decorator
def greet(name):
"""Greet a person"""
return f"Hello, {name}"
print(greet.__name__) # 'wrapper' (falsch!)
print(greet.__doc__) # 'Wrapper docstring' (falsch!)
# ✅ Mit wraps: Metadaten bleiben erhalten
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper docstring"""
return func(*args, **kwargs)
return wrapper
@good_decorator
def greet(name):
"""Greet a person"""
return f"Hello, {name}"
print(greet.__name__) # 'greet' (richtig!)
print(greet.__doc__) # 'Greet a person' (richtig!)
Immer @wraps in Decorators verwenden!
8.3 partial – Funktionen teilweise anwenden
Bereits in Abschnitt 5 erwähnt, hier mehr Details:
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(2)) # 8
# Mit positionalen Argumenten
double = partial(power, 2)
print(double(3)) # 8 (2³)
8.3.1 Praktische Beispiele
Logging mit festem Format:
from functools import partial
import logging
# Basis-Logger
def log(level, message):
logging.log(level, message)
# Spezialisierte Logger
debug = partial(log, logging.DEBUG)
info = partial(log, logging.INFO)
error = partial(log, logging.ERROR)
debug("Debug message")
error("Error occurred")
Callback-Funktionen:
from functools import partial
def send_notification(user, message, priority):
print(f"[{priority}] To {user}: {message}")
# Vorkonfigurierte Benachrichtigungen
notify_admin = partial(send_notification, user="admin", priority="HIGH")
notify_user = partial(send_notification, priority="NORMAL")
notify_admin("Server down!")
notify_user(user="alice", message="Welcome!")
8.4 reduce – Akkumulation
reduce reduziert ein Iterable auf einen einzelnen Wert durch wiederholte Anwendung einer Funktion.
from functools import reduce
# Summe
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, numbers)
print(total) # 15
# Mit Startwert
total = reduce(lambda acc, x: acc + x, numbers, 10)
print(total) # 25
# Produkt
product = reduce(lambda acc, x: acc * x, numbers)
print(product) # 120
# Maximum
maximum = reduce(lambda acc, x: x if x > acc else acc, numbers)
print(maximum) # 5
Besser mit operator:
from functools import reduce
import operator
numbers = [1, 2, 3, 4, 5]
# Addition
print(reduce(operator.add, numbers)) # 15
# Multiplikation
print(reduce(operator.mul, numbers)) # 120
# String-Konkatenation
words = ['Hello', ' ', 'World', '!']
print(reduce(operator.add, words)) # 'Hello World!'
Moderne Alternativen:
# ✅ Besser als reduce für einfache Fälle
numbers = [1, 2, 3, 4, 5]
# sum statt reduce für Addition
total = sum(numbers) # Bevorzugt!
# max/min eingebaut
maximum = max(numbers)
minimum = min(numbers)
# math.prod für Produkt (Python 3.8+)
import math
product = math.prod(numbers)
8.5 singledispatch – Funktions-Overloading
singledispatch ermöglicht verschiedene Implementierungen basierend auf dem Typ des ersten Arguments.
from functools import singledispatch
@singledispatch
def process(data):
"""Default-Implementierung"""
raise NotImplementedError(f"Cannot process type {type(data)}")
@process.register
def _(data: int):
return data * 2
@process.register
def _(data: str):
return data.upper()
@process.register
def _(data: list):
return len(data)
# Verwendung
print(process(5)) # 10
print(process("hello")) # "HELLO"
print(process([1,2,3])) # 3
Mit Type Hints (Python 3.7+):
from functools import singledispatch
from typing import List
@singledispatch
def to_json(obj):
raise TypeError(f"Cannot serialize {type(obj)}")
@to_json.register(int)
@to_json.register(float)
def _(obj):
return str(obj)
@to_json.register(str)
def _(obj):
return f'"{obj}"'
@to_json.register(list)
def _(obj):
items = ', '.join(to_json(item) for item in obj)
return f'[{items}]'
@to_json.register(dict)
def _(obj):
items = ', '.join(f'"{k}": {to_json(v)}' for k, v in obj.items())
return f'{{{items}}}'
# Verwendung
print(to_json(42)) # "42"
print(to_json("hello")) # '"hello"'
print(to_json([1, "two", 3])) # '[1, "two", 3]'
print(to_json({"a": 1, "b": "two"})) # '{"a": 1, "b": "two"}'
Registrierte Typen anzeigen:
print(to_json.registry) # Zeigt alle registrierten Typen
print(to_json.registry[int]) # Zeigt Implementierung für int
8.6 cmp_to_key – Vergleichsfunktion zu Schlüsselfunktion
Konvertiert alte-style Vergleichsfunktionen (die -1, 0, 1 zurückgeben) zu modernen Key-Funktionen.
from functools import cmp_to_key
# Alte-style Vergleichsfunktion
def compare(x, y):
"""Sortiert nach Länge, dann alphabetisch"""
if len(x) != len(y):
return len(x) - len(y)
if x < y:
return -1
elif x > y:
return 1
return 0
# Konvertierung
words = ['apple', 'pie', 'a', 'cherry', 'on']
sorted_words = sorted(words, key=cmp_to_key(compare))
print(sorted_words) # ['a', 'on', 'pie', 'apple', 'cherry']
Praktisches Beispiel – Custom Card Sorting:
from functools import cmp_to_key
class Card:
SUITS = {'Hearts': 4, 'Diamonds': 3, 'Clubs': 2, 'Spades': 1}
RANKS = {'A': 14, 'K': 13, 'Q': 12, 'J': 11}
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __repr__(self):
return f"{self.rank} of {self.suit}"
def compare_cards(card1, card2):
"""Erst nach Farbe, dann nach Wert"""
# Farben-Vergleich
suit_diff = Card.SUITS[card1.suit] - Card.SUITS[card2.suit]
if suit_diff != 0:
return suit_diff
# Wert-Vergleich
rank1 = Card.RANKS.get(card1.rank, int(card1.rank))
rank2 = Card.RANKS.get(card2.rank, int(card2.rank))
return rank1 - rank2
cards = [
Card('K', 'Hearts'),
Card('2', 'Spades'),
Card('A', 'Hearts'),
Card('5', 'Diamonds')
]
sorted_cards = sorted(cards, key=cmp_to_key(compare_cards))
for card in sorted_cards:
print(card)
# 2 of Spades
# 5 of Diamonds
# K of Hearts
# A of Hearts
8.7 total_ordering – Vergleichsoperatoren automatisch generieren
Generiert alle Vergleichsoperatoren aus __eq__ und einem weiteren (__lt__, __le__, __gt__, oder __ge__).
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __eq__(self, other):
return self.grade == other.grade
def __lt__(self, other):
return self.grade < other.grade
# __le__, __gt__, __ge__ werden automatisch generiert!
alice = Student('Alice', 85)
bob = Student('Bob', 92)
print(alice < bob) # True
print(alice <= bob) # True (automatisch generiert)
print(alice > bob) # False (automatisch generiert)
print(alice >= bob) # False (automatisch generiert)
print(alice == bob) # False
8.8 cached_property – Lazy Property mit Cache
Wie @property, aber Wert wird nur einmal berechnet und dann gecacht.
from functools import cached_property
import time
class DataProcessor:
def __init__(self, data):
self.data = data
@cached_property
def processed_data(self):
"""Teure Berechnung"""
print("Computing...")
time.sleep(2)
return [x * 2 for x in self.data]
# Verwendung
processor = DataProcessor([1, 2, 3, 4, 5])
# Erste Verwendung: Berechnung
print(processor.processed_data) # "Computing..." dann [2, 4, 6, 8, 10]
# Zweite Verwendung: Cache
print(processor.processed_data) # [2, 4, 6, 8, 10] (instant!)
Unterschied zu @property:
class Example:
@property
def normal_prop(self):
print("Computing...")
return expensive_computation()
@cached_property
def cached_prop(self):
print("Computing...")
return expensive_computation()
obj = Example()
# @property: Jedes Mal neu berechnet
obj.normal_prop # "Computing..."
obj.normal_prop # "Computing..." (erneut!)
# @cached_property: Nur einmal berechnet
obj.cached_prop # "Computing..."
obj.cached_prop # (kein "Computing...")
8.9 Praktische Kombinationen
8.9.1 Decorator mit LRU Cache
from functools import lru_cache, wraps
import time
def timed_lru_cache(maxsize=128):
"""Decorator kombiniert LRU-Cache mit Timing"""
def decorator(func):
cached_func = lru_cache(maxsize=maxsize)(func)
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = cached_func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.6f}s")
return result
wrapper.cache_info = cached_func.cache_info
wrapper.cache_clear = cached_func.cache_clear
return wrapper
return decorator
@timed_lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
fibonacci(30) # Erste Berechnung: langsam
fibonacci(30) # Cache-Hit: schnell
8.9.2 Partial mit Singledispatch
from functools import singledispatch, partial
@singledispatch
def format_value(value, precision=2):
return str(value)
@format_value.register(float)
def _(value, precision=2):
return f"{value:.{precision}f}"
@format_value.register(int)
def _(value, precision=2):
return f"{value:,}"
# Partial für feste Präzision
format_2dp = partial(format_value, precision=2)
format_4dp = partial(format_value, precision=4)
print(format_2dp(3.14159)) # "3.14"
print(format_4dp(3.14159)) # "3.1416"
print(format_2dp(1000000)) # "1,000,000"
8.10 Zusammenfassung
| Funktion | Zweck |
|---|---|
lru_cache | Memoization / Ergebnisse cachen |
cache | Unbegrenzter Cache (3.9+) |
wraps | Metadaten in Decorators erhalten |
partial | Funktionen teilweise anwenden |
reduce | Iterable auf einzelnen Wert reduzieren |
singledispatch | Funktions-Overloading nach Typ |
cmp_to_key | Vergleichsfunktion → Key-Funktion |
total_ordering | Alle Vergleichsoperatoren generieren |
cached_property | Property mit einmaliger Berechnung |
Best Practices:
- Immer
@wrapsin Decorators verwenden lru_cachefür teure, wiederholte Berechnungensingledispatchstatt if-elif Typ-Checkstotal_orderingfür vergleichbare Klassencached_propertyfür lazy initialization
9 Throttle und Debounce
Im Folgenden werden die Konzepte Throttle, Debounce und deren Kombination anhand eines Minimalbeispiels (Tkinter-Fenster mit Texteingabe-Widget) gezeigt.
9.1 Throttle: Maximal alle X Sekunden ausführen
Ziel:
Eine Funktion wird nicht bei jeder Eingabe, sondern nur alle X Sekunden ausgeführt.
Verwendungszweck:
- Live-Speichern
- Logging
- Netzwerk-Anfragen beim Scrollen oder Eingeben
Beispiel:
import tkinter as tk
import time
class ThrottleApp:
text: tk.Text
time_last_call: float = 0
def __init__(self, root):
# Erstelle Textbox und binde das KeyRelease-Ereignis an on_key()
self.text = tk.Text(root, height=10, width=40)
self.text.pack()
self.text.bind('<KeyRelease>', self.on_key)
def on_key(self, event: tk.Event):
now = time.time()
# Throttle: Ignoriere Aufruf, wenn letzter weniger als 2 s her ist
if now - self.time_last_call < 2:
return
self.time_last_call = now
self.handle_text(self.text.get('1.0', tk.END).strip())
def handle_text(self, text: str):
print(f'[{time.strftime('%X')}] [Throttle] Verarbeitung: {text}')
## Erstelle und starte Tkinter-App
root = tk.Tk()
app = ThrottleApp(root)
root.mainloop()
9.2 Debounce: Ausführen, wenn keine neuen Events mehr kommen
Ziel:
Eine Funktion wird erst ausgeführt, wenn der Benutzer X Sekunden lang nichts mehr gemacht hat.
Verwendungszweck:
- Auto-Save nach Tipp-Pause
- Autovervollständigung
- Validierung nach Eingabe
Beispiel:
import tkinter as tk
import threading
import time
class DebounceApp:
text: tk.Text
debounce_timer: threading.Timer | None = None
def __init__(self, root):
# Erstelle Textbox und binde das KeyRelease-Ereignis an on_key()
self.text = tk.Text(root, height=10, width=40)
self.text.pack()
self.text.bind('<KeyRelease>', self.on_key)
def on_key(self, event: tk.Event):
# Debounce: Setze den Timer zurück
if self.debounce_timer:
self.debounce_timer.cancel()
# Debounce: Starte den Timer, Aktion nach 3 Sekunden
self.debounce_timer = threading.Timer(
3.0, self.handle_text, args=(self.text.get('1.0', tk.END).strip(),)
)
self.debounce_timer.start()
def handle_text(self, text: str):
print(f'[{time.strftime('%X')}] [Debounce] Finale Verarbeitung: {text}')
## Erstelle und starte Tkinter-App
root = tk.Tk()
app = DebounceApp(root)
root.mainloop()
9.3 Kombination von Throttle und Debounce
Ziel:
Eine Funktion wird alle X Sekunden ausgeführt und wenn der Benutzer Y Sekunden lang nichts mehr gemacht hat.
Verwendungszweck:
- Throttle: z. B. regelmäßige Zwischenspeicherung
- Debounce: Finale Verarbeitung, wenn nichts mehr kommt
Beispiel:
import tkinter as tk
import threading
import time
class ThrottleDebounceApp:
text: tk.Text
last_throttle_time: float = 0
debounce_timer: threading.Timer | None = None
throttle_interval = 2.0
debounce_interval = 3.0
def __init__(self, root):
# Erstelle Textbox und binde das KeyRelease-Ereignis an on_key()
self.text = tk.Text(root, height=10, width=40)
self.text.pack()
self.text.bind('<KeyRelease>', self.on_key)
def on_key(self, event: tk.Event):
now = time.time()
# Throttle: Verarbeitung, wenn Throttle-Zeit überschritten
if now - self.last_throttle_time >= self.throttle_interval:
self.handle_text(
reason='Throttle', text=self.text.get('1.0', tk.END).strip()
)
self.last_throttle_time = now
# Debounce: Setze den Timer zurück
if self.debounce_timer:
self.debounce_timer.cancel()
# Debounce: Starte den Timer, Aktion nach 3 Sekunden
self.debounce_timer = threading.Timer(
self.debounce_interval,
self.handle_text,
kwargs={
'reason': 'Debounce',
'text': self.text.get('1.0', tk.END).strip()
}
)
self.debounce_timer.start()
def handle_text(self, reason: str, text: str):
print(f'[{time.strftime("%X")}] [{reason}] Verarbeitung: {text}')
## Tkinter-App starten
root = tk.Tk()
app = ThrottleDebounceApp(root)
root.mainloop()
9.4 Zusammenfassung
| Verhalten | Beschreibung | Beispiel-Use-Case |
|---|---|---|
| Throttle | Maximal alle X Sekunden ausführen | Live-Preview, Autosave |
| Debounce | Nur wenn X Sekunden nichts passiert | Validierung, End-Save |
| Kombination | Regelmäßig + abschließend nach Ruhe | Markdown-Live + Final Save |
10 Zusammenfassung
| Konzept | Nutzen |
|---|---|
| Closures | Zustand bewahren, Daten kapseln |
| Lambda | Kürzere anonyme Funktionen |
| Higher-Order Funcs | Flexible Funktionskomposition |
| map/filter/reduce | Funktionale Verarbeitung von Listen |
| Partial Functions | Vorbelegte Funktionen |
| Decorators | Funktion erweitern ohne Quellcode zu ändern |
| functools | Werkzeuge für funktionale Programmierung (cache, wraps, etc.) |
| Throttle & Debounce | Event-Rate-Limiting für Performance und User Experience |
Typannotationen
Typannotation (Type Annotation) ist eine Möglichkeit in Python, den erwarteten Typ von Variablen, Funktionsparametern und Rückgabewerten anzugeben.
vorteile der typannotation
Obwohl Python eine dynamisch typisierte Sprache ist, erlaubt die Typannotation eine bessere Lesbarkeit, Wartbarkeit und eine reduzierte Fehleranfälligkeit im Code.
1 Grundlagen der Typannotation
Typannotation wird mit einem Doppelpunkt : und dem Typnamen gemacht. Der Rückgabetyp einer Funktion wird mit -> angegeben.
1.1 Beispiel:
def addiere(x: int, y: int) -> int:
return x + y
In diesem Beispiel wird angegeben, dass sowohl x als auch y vom Typ int sind und die Funktion einen int zurückgibt.
2 Typannotation bei Variablen
name: str = "Anna"
alter: int = 30
aktiv: bool = True
3 Optionaler Typ
Manchmal kann eine Variable auch None sein. Dafür verwendet man Optional:
from typing import Optional
def finde_benutzer(id: int) -> Optional[str]:
if id == 1:
return "Anna"
return None
4 Any
Der Typ Any aus dem typing-Modul signalisiert, dass jede Art von Wert erlaubt ist. Dies ist hilfreich, wenn der genaue Typ nicht bekannt oder variabel ist.
from typing import Any
def drucke_wert(wert: Any) -> None:
print(wert)
5 Callable
Callable wird verwendet, um Funktionen als Parameter oder Rückgabewerte zu typisieren.
from typing import Callable
def verarbeite(funktion: Callable[[int, int], int]) -> int:
return funktion(2, 3)
In diesem Beispiel ist funktion eine Funktion, die zwei int-Werte nimmt und einen int zurückgibt.
6 Typannotation in Klassen
Auch in Klassen können Typannotationen verwendet werden:
class Person:
name: str
alter: int
def __init__(self, name: str, alter: int) -> None:
self.name = name
self.alter = alter
6.1 Verweis auf eigene Klasse als Typ
Will man innerhalb einer Klasse auf die eigene Klasse verweisen (z. B. bei einer Methode, die ein Objekt der gleichen Klasse zurückgibt), verwendet man einen String (um zirkuläre Importe zu vermeiden) oder ab Python 3.11 from __future__ import annotations:
class Knoten:
def __init__(self, wert: int, nachfolger: 'Knoten' = None) -> None:
self.wert = wert
self.nachfolger = nachfolger
Ab Python 3.11:
from __future__ import annotations
class Knoten:
def __init__(self, wert: int, nachfolger: Knoten = None) -> None:
self.wert = wert
self.nachfolger = nachfolger
7 Typalias
Man kann eigene Typen definieren, um komplexe Strukturen lesbarer zu machen:
from typing import List, Tuple
Koordinaten = Tuple[float, float]
Pfad = List[Koordinaten]
8 Generics
Generics ermöglichen es, die Typsicherheit zu verbessern, indem man Typen nicht fest vorgibt, sondern parametrisierbar macht.
Sie werden genutzt, um auszudrücken, welche Datentypen eine Funktion oder Klasse erwartet und zurückgibt, ohne auf einen konkreten Typ festgelegt zu sein.
Beispiel:
from typing import TypeVar, List
T = TypeVar('T') # generischer Typ
def first(items: List[T]) -> T:
return items[0]
print(first([1, 2, 3])) # int
print(first(['a', 'b', 'c'])) # str
Die Funktion first() kann mit Listen beliebiger Typen arbeiten, der Rückgabewert passt sich dem jeweiligen Typ an.
Vorteile von Generics?
- Wiederverwendbarer Code mit genauer Typprüfung
- Vermeidet Fehler, die bei gemischten Datentypen auftreten
- Bessere Unterstützung in IDEs & für Autocomplete
8.1 Generics in Klassen
Generics können nicht nur in Funktionen, sondern auch in Klassen verwendet werden, um flexibel mit unterschiedlichen Datentypen zu arbeiten. Dabei wird ein Typparameter definiert, der den Typ von Attributen, Parametern oder Rückgabewerten innerhalb der Klasse repräsentiert. So kann dieselbe Klassendefinition für verschiedene konkrete Typen wiederverwendet werden, ohne die Typprüfung zu verlieren.
Beispiel:
from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, content: T):
self.content = content
def get(self) -> T:
return self.content
b1 = Box(123) # Box[int]
b2 = Box('Hallo') # Box[str]
Die Klasse Box ist generisch und nimmt einen Typparameter T an. Wird ein Objekt der Klasse erzeugt, bestimmt der tatsächliche Typ des Inhalts (int, str, …) automatisch den Typ für T. Dadurch weiß der Type Checker und auch die IDE, welchen konkreten Typ die Methode get() zurückgibt. Das verbessert Autovervollständigung und verhindert Typfehler zur Entwicklungszeit.
8.2 TypeVar mit Einschränkungen (bound und constraints)
In manchen Situationen soll ein generischer Typ nur bestimmte Typen erlauben. Dafür bietet TypeVar zwei Möglichkeiten: bound und constraints:
- Mit
boundwird eine Obergrenze festgelegt (z. B. eine Basisklasse oder ein Typ wiefloat). - Mit constraints dagegen wird eine Menge gültiger Typen definiert, aus denen ausgewählt werden darf.
8.2.1 Bound
bound schränkt TypeVar so ein, dass nur Instanzen eines bestimmten Typs oder seiner Unterklassen gültig sind. So kann sichergestellt werden, dass alle Operationen, die auf diesem Typ ausgeführt werden, garantiert verfügbar sind. Das ist besonders nützlich, wenn Funktionen mit bestimmten Eigenschaften oder Methoden arbeiten sollen.
Beispiel:
from typing import TypeVar
T = TypeVar('T', bound=float)
def multiply(x: T, factor: float) -> T:
return x * factor
T darf nur float oder davon abgeleitete Typen sein. Durch bound=float kann T z. B. nicht str oder list sein. Wird ein anderer Typ übergeben, erkennt der Type Checker dies als Fehler.
8.2.2 Constraints
constraints wird verwendet, um eine Auswahl fest definierter Datentypen zu erlauben. Anders als bei bound liegt hier kein typisches Vererbungs-Beziehungskonzept zugrunde, sondern eine Liste akzeptierter Typen.
Beispiel:
U = TypeVar('U', int, float)
def add(a: U, b: U) -> U:
return a + b
Hier darf U nur int oder float sein. Ein Aufruf wie add('a', 'b') wäre ein Typfehler, auch wenn die Operation zur Laufzeit funktionieren würde – somit wird Typunsicherheit vermieden.
8.3 Mehrere Typparameter (Generic[T, U])
Manchmal benötigen Klassen oder Funktionen mehr als einen generischen Typ. Ein typisches Beispiel ist eine Datenstruktur, die Schlüssel und Werte speichert (analog zu dict). Mit mehreren Typparametern lassen sich Abhängigkeiten zwischen mehreren Typen genau definieren.
Beispiel:
from typing import Generic, TypeVar
T = TypeVar('T')
U = TypeVar('U')
class Pair(Generic[T, U]):
def __init__(self, first: T, second: U):
self.first = first
self.second = second
def get_first(self) -> T:
return self.first
def get_second(self) -> U:
return self.second
p1 = Pair('Anna', 37) # Pair[str, int]
p2 = Pair(3.14, True) # Pair[float, bool]
- Pair akzeptiert zwei unterschiedliche generische Typen.
- Beim Erzeugen eines Objekts entscheidet sich, welche konkreten Typen
TundUannehmen. - Methoden kennen jeweils den passenden Rückgabetyp. Das verbessert Autovervollständigung und Typprüfung
Einsatzbeispiele:
- Schlüssel-Wert-Paare z. B. Cache oder Konfiguration
- Vergleichsergebnisse
z. B.
Result,Error - Wrapper für zwei verschiedenartige Daten z. B. DB-Record + Metadaten
Beispiel für generische Mapping-Struktur:
from typing import Generic, TypeVar, Dict
K = TypeVar('K')
V = TypeVar('V')
class Storage(Generic[K, V]):
def __init__(self):
self.data: Dict[K, V] = {}
def add(self, key: K, value: V) -> None:
self.data[key] = value
def get(self, key: K) -> V:
return self.data[key]
s = Storage[str, int]()
s.add('Age', 30)
print(s.get('Age'))
TypeVar('K') und TypeVar('V') definieren zwei generische Typparameter für Schlüssel und Werte. Durch class Storage(Generic[K, V]) wird die Klasse so parametrisierbar, dass sie mit beliebigen Typkombinationen verwendet werden kann. Das interne Dictionary Dict[K, V] speichert Werte vom Typ V unter Schlüsseln vom Typ K. Die Methoden add() und get() übernehmen und liefern konsistent diese Typen. Beim Erzeugen der Instanz (Storage[str, int]) werden die konkreten Typen festgelegt, wodurch Typsicherheit und bessere IDE-Unterstützung erreicht werden.
8.4 Generics bei Dict, List, Tuple
Viele eingebaute Python-Datentypen unterstützen Generics standardmäßig. Es kann festgelegt werden, welche Typen Elemente enthalten sollen. Dadurch erkennt der Type Checker z. B., ob ein falscher Wert in eine Liste geschrieben wird oder ob auf ein Dictionary mit einem falschen Schlüsseltyp zugegriffen wird.
Beispiel:
from typing import Dict, List, Tuple
numbers: List[int] = [1, 2, 3]
person: Tuple[str, int] = ('Max', 32)
scores: Dict[str, float] = {'Anna': 1.3, 'Benjamin': 2.0}
8.5 TypedDict und Protocol (erweiterte Typsystem-Funktionen)
Neben Generics stellt die typing-Bibliothek weitere Mechanismen zur Verfügung, um komplexere Strukturen und Schnittstellen präzise zu typisieren. Dazu gehören TypedDict für strukturiertes Arbeiten mit Dictionaries sowie Protocol für strukturelle Typprüfung (ähnlich zu Interfaces in anderen Sprachen).
8.5.1 TypedDict
TypedDict ermöglicht das Definieren von Dictionaries mit festen Schlüsselnamen und Wert-typen. Damit lässt sich verhindern, dass Keys fehlen, vertauscht werden oder Werte falscher Typen enthalten.
Beispiel:
from typing import TypedDict
class Person(TypedDict):
name: str
age: int
p: Person = {'name': 'Tom', 'age': 30}
Die IDE weiß so, welche Felder vorhanden sein müssen und welche Typen zugeordnet sind.
8.5.2 Protocol (duck typing + Typprüfung)
Protocol ermöglicht strukturelle Typprüfung, d. h. es muss nicht eine Klasse explizit erben, sondern nur die geforderten Methoden bereitstellen (ähnlich Duck Typing: “wenn es aussieht wie eine Ente…”). Dadurch können generische Funktionen definiert werden, die mit beliebigen Objekten arbeiten, solange sie die erwartete Signatur besitzen.
Beispiel:
from typing import Protocol
class Flyer(Protocol):
def fly(self) -> None:
...
class Bird:
def fly(self) -> None:
print('Flap!')
def start(f: Flyer):
f.fly()
start(Bird())
Bird erfüllt das Protocol automatisch, ohne explizite Vererbung.
8.6 Zusammenfassung
| Konzept | Bedeutung |
|---|---|
TypeVar | generischer Typ |
Generic[T] | Klasse/Funktion ist generisch |
bound= | Typ auf Obergrenze beschränken |
constraints | Liste erlaubter Typen |
Protocol | strukturelle Typprüfung |
TypedDict | typisierte Dictionaries |
9 Warum Typannotationen verwenden?
- Lesbarkeit: Andere Entwickler verstehen schneller, was erwartet wird.
- Wartbarkeit: Änderungen im Code sind leichter nachzuvollziehen.
- Fehlervermeidung: Tools wie Mypy, Pyright/Pylance oder IDEs können Fehler frühzeitig erkennen.
- Dokumentation: Typen fungieren als explizite Dokumentation des Codes.
kein zwang zur typisierung
Python bleibt trotz Typannotationen dynamisch. Die Typangaben werden zur Laufzeit nicht erzwungen. Sie dienen lediglich als Hilfe für Entwickler und Werkzeuge.
def echo(text: str) -> str:
return text
# Funktioniert trotzdem, obwohl ein falscher Typ übergeben wird
print(echo(123)) # Ausgabe: 123
10 Typenvergleich
Typen vergleichen kann man mit den Funktionen type() und isinstance().
Beispiel:
from collections import namedtuple
Point = namedtuple('Punkt', ['x', 'y'])
p = Point(2, 3)
if type(p) == tuple:
print('P ist ein Tupel.') # Wird nicht ausgegeben!
if isinstance(p, tuple):
print('P ist ein Tupel.')
tip
Es ist in der Regel besser, isinstance() anstelle eines direkten Vergleichs mit type() zu verwenden, weil isinstance() auch Vererbungen berücksichtigt.
type()gibt die exakte Klasse des Objektspzurück, nämlichPoint. Daher wird in dem Beispiel der Vergleich falsch ausgewertet.isinstance()überprüft, obpeine Instanz vontupleoder einer Unterklasse davon ist. Danamedtupleeine Unterklasse vontupleist, gibt isinstance(p, tuple)Truezurück.
Siehe auch: type(), isinstance() und issubclass().
11 Static Type Checking Tools
Typannotationen allein werden von Python zur Laufzeit nicht überprüft. Um Typfehler bereits vor der Ausführung zu finden, nutzt man Static Type Checker wie mypy oder Pyright.
11.1 mypy – Der Standard Type Checker
mypy ist der offizielle und am weitesten verbreitete Type Checker für Python.
Installation:
pip install mypy
Grundlegende Verwendung:
# example.py
def greet(name: str) -> str:
return f"Hello, {name}"
result: int = greet("Alice") # Typfehler!
# Type Checking ausführen
mypy example.py
Ausgabe:
example.py:4: error: Incompatible types in assignment (expression has type "str", variable has type "int")
Found 1 error in 1 file (checked 1 source file)
11.2 Konfiguration mit mypy.ini oder pyproject.toml
mypy.ini:
[mypy]
python_version = 3.10
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_any_unimported = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
check_untyped_defs = True
# Pro Modul konfigurieren
[mypy-pandas.*]
ignore_missing_imports = True
[mypy-numpy.*]
ignore_missing_imports = True
pyproject.toml:
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.mypy.overrides](tool.mypy.overrides.md)
module = "pandas.*"
ignore_missing_imports = true
11.3 Wichtige mypy-Optionen
| Option | Bedeutung |
|---|---|
--strict | Aktiviert alle strengen Checks |
--ignore-missing-imports | Ignoriert fehlende Type Stubs von Drittbibliotheken |
--disallow-untyped-defs | Verlangt Typen für alle Funktionsdefinitionen |
--check-untyped-defs | Prüft auch Funktionen ohne Typannotationen |
--warn-return-any | Warnt bei Any als Rückgabetyp |
--show-error-codes | Zeigt Error-Codes (z.B. [assignment]) |
11.4 Type Stubs für Drittbibliotheken
Viele Bibliotheken haben keine eingebauten Typannotationen. Dafür gibt es separate Type Stubs.
# Type Stubs installieren
pip install types-requests
pip install types-PyYAML
pip install pandas-stubs
Typeshed: Zentrale Sammlung von Type Stubs für die Standardbibliothek und populäre Packages
- Wird automatisch mit mypy installiert
- Repository: https://github.com/python/typeshed
11.5 Inline Type Ignores
Manchmal ist man sich sicher, dass der Code korrekt ist, auch wenn mypy warnt:
from typing import Any
def process_data(data: Any) -> int:
# mypy würde hier warnen, aber wir wissen, dass data eine Zahl ist
return data + 1 # type: ignore[operator]
# Gesamte Zeile ignorieren
result = some_complex_function() # type: ignore
# Nur bestimmte Error-Codes ignorieren
value = int("123") # type: ignore[arg-type]
11.6 Pyright / Pylance – Microsoft’s Type Checker
Pyright ist ein schneller, moderner Type Checker von Microsoft, integriert in VS Code als Pylance.
Installation:
pip install pyright
# Oder als npm-Package (schneller)
npm install -g pyright
Verwendung:
pyright src/
Konfiguration (pyrightconfig.json):
{
"include": ["src"],
"exclude": ["**/node_modules", "**/__pycache__"],
"typeCheckingMode": "strict",
"pythonVersion": "3.10",
"reportMissingImports": true,
"reportMissingTypeStubs": false
}
In pyproject.toml:
[tool.pyright]
include = ["src"]
exclude = ["**/node_modules", "**/__pycache__"]
typeCheckingMode = "strict"
pythonVersion = "3.10"
11.7 mypy vs. Pyright
| Kriterium | mypy | Pyright |
|---|---|---|
| Performance | ⚠️ Langsamer | ✅ Sehr schnell |
| Standard-Konformität | ✅ Referenz-Implementation | ✅ Sehr gut |
| VS Code Integration | ⚠️ Extension nötig | ✅ Native (Pylance) |
| Konfiguration | ✅ Sehr flexibel | ✅ Gut |
| Community | ✅ Größer | ✅ Wachsend |
| Empfohlen für | CI/CD, Commandline | VS Code, IDE-Integration |
11.8 Integration in CI/CD
GitHub Actions:
name: Type Check
on: [push, pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install mypy types-requests
- name: Run mypy
run: mypy src/
Pre-commit Hook:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.5.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-PyYAML]
11.9 Graduelle Typisierung
Man muss nicht das gesamte Projekt auf einmal typisieren:
# Schritt 1: Keine Typen (Status Quo)
def calculate(x, y):
return x + y
# Schritt 2: Partielle Typisierung
def calculate(x: int, y: int):
return x + y
# Schritt 3: Vollständige Typisierung
def calculate(x: int, y: int) -> int:
return x + y
Strategie für große Projekte:
- Kritische/neue Module zuerst typisieren
# type: ignorefür Legacy-Code nutzen- Schrittweise strengere mypy-Optionen aktivieren
- Test-Code kann weniger streng sein
11.10 Häufige Type Checker Fehler
Fehler: Incompatible return value type
def get_name() -> str:
return None # Fehler!
# Lösung: Optional verwenden
from typing import Optional
def get_name() -> Optional[str]:
return None # OK
Fehler: Argument has incompatible type
def greet(name: str) -> None:
print(f"Hello, {name}")
greet(123) # Fehler!
# Lösung: Richtigen Typ übergeben
greet("Alice")
Fehler: Missing type parameters
from typing import List
def process(items: List): # Fehler! List[?]
pass
# Lösung: Typ-Parameter angeben
def process(items: List[int]) -> None:
pass
11.11 Best Practices
✅ DO:
- Type Checker in CI/CD Pipeline integrieren
- Neue Module vollständig typisieren
strictMode für neue Projekte aktivieren- Type Stubs für Dependencies installieren
- Pre-commit Hooks verwenden
❌ DON’T:
- Typen nur hinzufügen, um mypy zufriedenzustellen
- Überall
Anyverwenden (verliert Typsicherheit) - Type Checking bei Tests vernachlässigen
# type: ignoreohne Grund nutzen
11.12 Weitere Tools
Pytype (Google):
pip install pytype
pytype src/
- Inferiert Typen automatisch
- Weniger strikte als mypy
- Gut für Legacy-Code
Pyre (Meta):
pip install pyre-check
pyre check
- Fokus auf Performance
- Inkrementelles Type Checking
- Hauptsächlich für große Codebases
11.13 Zusammenfassung
| Tool | Verwendung |
|---|---|
| mypy | Standard Type Checker, CLI, CI/CD |
| Pyright | Schneller Checker, VS Code Integration |
| Type Stubs | Typen für Drittbibliotheken |
# type: ignore | Einzelne Warnungen unterdrücken |
Kernprinzip: Type Checking ist ein Werkzeug zur Verbesserung der Code-Qualität, kein Selbstzweck. Beginne mit lockeren Einstellungen und erhöhe die Strenge schrittweise.
12 Fazit
Typannotationen machen Python-Code robuster, verständlicher und besser wartbar, ohne die Flexibilität der Sprache einzuschränken. Es lohnt sich, sie konsequent zu verwenden, besonders bei größeren Projekten.
Speicherverhalten, Referenzen und Mutability
Die folgenden Konzepte sind zentral für das Verständnis von Speicherverhalten und Referenzen. Dieses ist wichtig, um Seiteneffekte und unerwartetes Verhalten zu vermeiden.
1 Speicherverwaltung und Garbage Collection
1.1 Speicherverwaltung
Python verwaltet den Speicher auf zwei Ebenen:
-
Private Heap
- Alles, was in Python anlegt wird (Variablen, Objekte, Listen usw.), wird im sogenannten private heap gespeichert.
- Man hat keinen direkten Zugriff auf diesen Heap, sondern nur über Python-Objekte.
-
Speicherallokatoren (Memory Manager)
- Python hat einen internen Mechanismus, der entscheidet, wann wie viel Speicher angefordert wird.
- Objekte kleiner als 512 Bytes werden z. B. in einem speziellen Arena-basierten System verwaltet (über das Modul
pymalloc).
1.2 Garbage Collection
Garbage Collection bedeutet, dass nicht mehr benötigte Objekte automatisch aus dem Speicher entfernt werden.
1.2.1 Referenzzählung
Das ist die primäre Methode der Speicherfreigabe:
import sys
x = [] # Liste wird erstellt
print(sys.getrefcount(x)) # 2
y = x # Jetzt gibt es 2 Referenzen
print(sys.getrefcount(x)) # 3
del x # Eine Referenz weniger
print(sys.getrefcount(y)) # 2
1.2.2 Zyklische Garbage Collection
Referenzzählung reicht nicht aus, wenn zwei Objekte sich gegenseitig referenzieren:
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b
b.ref = a # Zyklus
del a
del b # → Referenzzähler wird nicht null
Daher hat Python ein zusätzliches Garbage Collector-Modul, das Zyklen erkennen und auflösen kann:
import gc
gc.collect() # Startet manuelle Garbage Collection
1.3 Tools & Module
gc: Garbage-Collector-Modulsys.getrefcount(obj): Anzahl der Referenzen auf ein Objektid(obj): Gibt die Speicheradresse des Objekts zurückhex(id(obj)): Speicher Adresse als HEX
1.4 Zusammenfassung
| Feature | Beschreibung |
|---|---|
| Heap | Speicherort aller Objekte |
| Referenzzählung | Basis-Mechanismus zur Speicherfreigabe |
| Zyklische GC | Entfernt Objektzyklen, die Referenzzähler nicht abfängt |
gc-Modul | Ermöglicht Kontrolle über die Garbage Collection |
2 Variablen und Referenzen
In Python sind Variablen keine Container, sondern Namen, die auf Objekte im Speicher verweisen. Das Verständnis von Variablenbindung und Referenzen ist essenziell, um Seiteneffekte und unerwartetes Verhalten zu vermeiden.
Variablen binden Namen an Objekte:
Eine Variable in Python ist ein Name, der auf ein Objekt zeigt. Es wird keine Kopie erzeugt, sondern eine Referenz.
a = [1, 2, 3]
b = a # b referenziert dasselbe Objekt wie a
a.append(4)
print(b) # [1, 2, 3, 4] -> b wurde mit verändert
Referenzen und id():
Jedes Objekt hat eine eindeutige ID (Speicheradresse), die mit id() abgefragt werden kann.
x = 'hello'
y = x
print(id(x) == id(y)) # True -> beide zeigen auf dasselbe Objekt
x = x + ' world' # neues Objekt
print(id(x) == id(y)) # False -> x zeigt jetzt auf etwas anderes
Zuweisung erzeugt keine Kopie:
data = {'key': 'value'}
copy = data # keine Kopie, nur neue Referenz
copy['key'] = 'new value'
print(data) # {'key': 'new value'}
2.1 Referenzen in Funktionen
Wird ein Objekt an eine Funktion übergeben, wird nur eine Referenz übergeben – keine Kopie.
def change_list(lst):
lst.append(42)
numbers = [1, 2, 3]
change_list(numbers)
print(numbers) # [1, 2, 3, 42]
Bei unveränderlichen Objekten (z. B. int, str) wirkt sich das nicht aus:
def change_value(x):
x = x + 1 # neue Referenz innerhalb der Funktion
value = 10
change_value(value)
print(value) # 10
2.2 is vs. ==
- == prüft Gleichheit des Inhalts
isprüft, ob zwei Variablen auf dasselbe Objekt zeigen
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a == c) # True -> Inhalt gleich
print(a is c) # False -> unterschiedliche Objekte
print(a is b) # True -> gleiche Referenz
2.3 Objektidentität bei Mutability
Mutability beeinflusst das Verhalten bei Referenzen stark:
def reset_list(l):
l.clear() # verändert Originalobjekt
my_list = [9, 8, 7]
reset_list(my_list)
print(my_list) # []
Bei immutable Typen wie int passiert das nicht:
def reset_number(n):
n = 0 # neue Referenz, Original bleibt gleich
my_number = 123
reset_number(my_number)
print(my_number) # 123
2.4 Namen und Gültigkeit (Scope)
Variablen sind innerhalb des jeweiligen Gültigkeitsbereichs (Scope) sichtbar. Eine Zuweisung innerhalb einer Funktion überschreibt nicht die äußere Variable.
name = 'outer'
def test():
name = 'inner'
print(name) # 'inner'
test()
print(name) # 'outer'
2.5 Zusammenfassung
- Variablen sind Namen, keine Container.
- Zuweisung erzeugt Referenzen, keine Kopien.
- Mutable Objekte können über mehrere Referenzen verändert werden.
- Unveränderliche Objekte bleiben von außen unbeeinflusst.
isprüft Identität, == prüft Gleichheit.
3 Mutability von Datentypen
In Python unterscheidet man zwischen veränderlichen (mutable) und unveränderlichen (immutable) Datentypen. Dieses Konzept ist zentral für das Verständnis von Speicherverhalten, Referenzen und Seiteneffekten.
mutable vs immutable
Ein mutable Objekt kann nach seiner Erstellung verändert werden. Änderungen wirken sich auf alle Referenzen auf dieses Objekt aus.
Ein immutable Objekt kann nicht verändert werden. Jede Änderung erzeugt ein neues Objekt.
mutability prüfen mit `id()`
Die id()-Funktion zeigt die Speicheradresse eines Objekts. So erkennt man, ob eine Änderung ein neues Objekt erzeugt hat.
s = 'abc'
print(id(s)) # 4380616160
s += 'd'
print(id(s)) # 4934619072 (Neue ID)
3.1 Beispiele für immutable Typen
intfloatstrtuplefrozensetbool
x = 10
print(id(x)) # 4381074208
y = x
print(id(y)) # 4381074208
x = x + 1 # neues Objekt wird erstellt
print(id(x)) # 4381074240
print(x) # 11
print(y) # 10
a = 'hello'
print(id(a)) # 4843587312
b = a
print(id(b)) # 4843587312
a += ' world' # neuer String, neue Referenz
print(id(a)) # 4934764784
print(a) # 'hello world'
print(b) # 'hello'
3.2 Beispiele für mutable Typen
listdictsetbytearray- user-defined classes (standardmäßig)
items = [1, 2, 3]
copy = items
items.append(4) # verändert das Objekt selbst
print(items) # [1, 2, 3, 4]
print(copy) # [1, 2, 3, 4] -> auch verändert
settings = {'theme': 'dark'}
ref = settings
settings['theme'] = 'light' # Änderung wirkt auf beide Referenzen
print(ref) # {'theme': 'light'}
3.3 Typen in Klassenattributen
Bei eigenen Klassen ist das Verhalten abhängig vom verwendeten Datentyp:
class User:
def __init__(self, name):
self.name = name # str ist immutable
self.tags = [] # list ist mutable
u1 = User('Alice')
u2 = u1
u1.tags.append('admin') # wirkt auch auf u2
print(u2.tags) # ['admin']
3.4 Auswirkungen auf Funktionsparameter
Mutable Objekte können in Funktionen verändert werden, was außerhalb der Funktion sichtbar ist.
def modify_list(lst):
lst.append(99) # verändert die übergebene Liste
nums = [1, 2, 3]
modify_list(nums)
print(nums) # [1, 2, 3, 99]
Bei immutable Objekten passiert das nicht:
def modify_number(n):
n += 1 # neues Objekt innerhalb der Funktion
x = 10
modify_number(x)
print(x) # 10
3.5 Zusammenfassung
| Datentyp | Mutable |
|---|---|
int | Nein |
str | Nein |
bool | Nein |
tuple | Nein (aber Inhalt kann mutable sein) |
list | Ja |
dict | Ja |
set | Ja |
user class | Abhänigig von den verwendeten Datentypen |
Mutability beeinflusst, wie sich Daten in Speicher, Referenzen und Funktionen verhalten. Ein gutes Verständnis hilft, Bugs und unerwartetes Verhalten zu vermeiden.
4 In-Place Operations und Shallow bzw. Deep Copy
4.1 In-Place Operations
In-Place Operationen ändern ein Objekt direkt, ohne eine neue Kopie zu erstellen. Das spart Speicher, kann aber zu Seiteneffekten führen, wenn mehrere Referenzen auf das gleiche Objekt existieren.
Listen verändern:
numbers = [1, 2, 3]
numbers.append(4) # verändert das Objekt selbst
print(numbers) # [1, 2, 3, 4]
+= bei Listen:
data = [1, 2]
other = data
data += [3, 4] # in-place Änderung
print(data) # [1, 2, 3, 4]
print(other) # [1, 2, 3, 4] -> auch verändert
`+=` kann auch eine neue referenz erzeugen (bei immutable typen)
Bei immutable Typen wie int, str, tuple wird keine In-Place-Änderung durchgeführt.
Beispiel:
x = 10
y = x
x += 5 # erzeugt neues Objekt
print(x) # 15
print(y) # 10
4.2 Shallow Copy (flache Kopie)
Eine flache Kopie kopiert nur die äußere Struktur, nicht die enthaltenen Objekte.
Beispiel mit copy.copy():
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
shallow[0][0] = 99 # verändert auch 'original'
print(original) # [[99, 2], [3, 4]]
shallowundoriginalteilen sich die inneren Listen.- Nur die erste Ebene wird kopiert.
4.3 Deep Copy (tiefe Kopie)
Eine tiefe Kopie erstellt eine vollständige Kopie aller Ebenen, rekursiv.
Beispiel mit copy.deepcopy():
import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 99
print(original) # [[1, 2], [3, 4]]
deepist vollständig unabhängig vonoriginal.- Änderungen wirken sich nicht auf das Original aus.
Vergleich Shallow vs. Deep Copy:
import copy
data = [{'a': 1}, {'b': 2}]
shallow = copy.copy(data)
deep = copy.deepcopy(data)
shallow[0]['a'] = 99
print(data) # [{'a': 99}, {'b': 2}]
print(deep) # [{'a': 1}, {'b': 2}]
kopieren mit slicing oder konstruktor
Beim Kopieren durch Slicing oder die Verwendung eines Konstruktors werden nur flache Kopien erzeugt:
original = [1, 2, 3]
copy1 = original[:] # flache Kopie
copy2 = list(original) # ebenfalls flach
copy1.append(4)
print(original) # [1, 2, 3]
print(copy1) # [1, 2, 3, 4]
4.4 Wann welche Kopie verwenden?
| Situation | Empfehlung |
|---|---|
| Nur äußere Struktur kopieren | copy.copy() |
| Vollständig unabhängig kopieren | copy.deepcopy() |
| Performance wichtig, Inhalt flach | list(), Slicing ([:]) |
4.5 Zusammenfassung
- In-Place Operationen ändern das Objekt direkt.
- Shallow Copies duplizieren nur die oberste Ebene.
- Deep Copies duplizieren rekursiv die komplette Struktur.
- Vorsicht bei verschachtelten Strukturen – dort macht
deepcopyden Unterschied.
5 Memory Profiling mit tracemalloc
tracemalloc ist ein eingebautes Modul zum Überwachen und Analysieren des Speicherverbrauchs von Python-Programmen. Es hilft, Speicherlecks zu finden und den Speicherverbrauch zu optimieren.
5.1 Grundlegende Verwendung
import tracemalloc
# Tracking starten
tracemalloc.start()
# Code ausführen
data = [i for i in range(1000000)]
# Aktuellen Speicherverbrauch abrufen
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory: {peak / 1024 / 1024:.2f} MB")
# Tracking stoppen
tracemalloc.stop()
5.2 Snapshots und Vergleiche
Snapshots ermöglichen es, den Speicherverbrauch zu verschiedenen Zeitpunkten zu vergleichen.
import tracemalloc
tracemalloc.start()
# Erster Snapshot
snapshot1 = tracemalloc.take_snapshot()
# Code ausführen
data = [i * 2 for i in range(1000000)]
# Zweiter Snapshot
snapshot2 = tracemalloc.take_snapshot()
# Unterschiede anzeigen
stats = snapshot2.compare_to(snapshot1, 'lineno')
print("Top 10 Speicherzuwächse:")
for stat in stats[:10]:
print(stat)
5.3 Top-Statistiken anzeigen
import tracemalloc
tracemalloc.start()
# Speicherintensiver Code
large_list = [i for i in range(5000000)]
large_dict = {i: str(i) for i in range(100000)}
# Snapshot nehmen
snapshot = tracemalloc.take_snapshot()
# Top 10 nach Speicherverbrauch
top_stats = snapshot.statistics('lineno')
print("Top 10 Memory Consumers:")
for index, stat in enumerate(top_stats[:10], 1):
print(f"{index}. {stat}")
tracemalloc.stop()
Ausgabe-Beispiel:
Top 10 Memory Consumers:
1. memory_example.py:7: size=38.1 MiB, count=1, average=38.1 MiB
2. memory_example.py:8: size=12.3 MiB, count=100000, average=129 B
5.4 Speicherlecks finden
import tracemalloc
# Simuliertes Speicherleck
leaked_data = []
def leak_memory():
"""Funktion, die Speicher nicht freigibt"""
data = [i for i in range(100000)]
leaked_data.append(data) # Referenz bleibt bestehen
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
# Mehrfach aufrufen
for _ in range(10):
leak_memory()
snapshot2 = tracemalloc.take_snapshot()
# Differenz analysieren
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("Memory increases:")
for stat in top_stats[:5]:
print(stat)
tracemalloc.stop()
5.5 Filtern nach Dateien/Modulen
import tracemalloc
import fnmatch
tracemalloc.start()
# Code ausführen
data = list(range(1000000))
snapshot = tracemalloc.take_snapshot()
# Nur spezifische Dateien
filters = [
tracemalloc.Filter(True, "*/my_module/*"), # Include
tracemalloc.Filter(False, "<frozen*"), # Exclude frozen modules
tracemalloc.Filter(False, "<unknown>"), # Exclude unknown
]
snapshot = snapshot.filter_traces(filters)
top_stats = snapshot.statistics('filename')
for stat in top_stats[:10]:
print(stat)
tracemalloc.stop()
5.6 Context Manager für Profiling
from contextlib import contextmanager
import tracemalloc
@contextmanager
def memory_profiler(description="Code Block"):
"""Context Manager für Speicher-Profiling"""
tracemalloc.start()
snapshot_before = tracemalloc.take_snapshot()
try:
yield
finally:
snapshot_after = tracemalloc.take_snapshot()
top_stats = snapshot_after.compare_to(snapshot_before, 'lineno')
print(f"\n=== Memory Profile: {description} ===")
current, peak = tracemalloc.get_traced_memory()
print(f"Current: {current / 1024 / 1024:.2f} MB")
print(f"Peak: {peak / 1024 / 1024:.2f} MB")
print("\nTop 5 allocations:")
for stat in top_stats[:5]:
print(stat)
tracemalloc.stop()
# Verwendung
with memory_profiler("List Creation"):
large_list = [i ** 2 for i in range(1000000)]
5.7 Detaillierte Traceback-Information
import tracemalloc
import linecache
def display_top(snapshot, key_type='lineno', limit=10):
"""Zeigt Top-Speicherverbraucher mit Traceback"""
snapshot = snapshot.filter_traces((
tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
tracemalloc.Filter(False, "<unknown>"),
))
top_stats = snapshot.statistics(key_type)
print(f"Top {limit} lines")
for index, stat in enumerate(top_stats[:limit], 1):
frame = stat.traceback[0]
filename = frame.filename
lineno = frame.lineno
print(f"#{index}: {filename}:{lineno}: {stat.size / 1024:.1f} KiB")
# Zeile aus Quelldatei
line = linecache.getline(filename, lineno).strip()
if line:
print(f" {line}")
# Verwendung
tracemalloc.start()
# Speicherintensive Operationen
data1 = [i for i in range(500000)]
data2 = {i: str(i) for i in range(100000)}
snapshot = tracemalloc.take_snapshot()
display_top(snapshot, limit=5)
tracemalloc.stop()
5.8 Vergleich: Verschiedene Implementierungen
import tracemalloc
import time
def benchmark_memory(func, *args, **kwargs):
"""Misst Speicher und Zeit für Funktion"""
tracemalloc.start()
start_time = time.time()
result = func(*args, **kwargs)
current, peak = tracemalloc.get_traced_memory()
elapsed_time = time.time() - start_time
tracemalloc.stop()
return {
'result': result,
'current_mb': current / 1024 / 1024,
'peak_mb': peak / 1024 / 1024,
'time_s': elapsed_time
}
# Verschiedene Implementierungen testen
def create_list_comprehension(n):
return [i ** 2 for i in range(n)]
def create_generator(n):
return list(i ** 2 for i in range(n))
def create_map(n):
return list(map(lambda x: x ** 2, range(n)))
# Vergleichen
n = 1_000_000
implementations = [
('List Comprehension', create_list_comprehension),
('Generator Expression', create_generator),
('Map', create_map)
]
print(f"Creating {n:,} elements\n")
for name, func in implementations:
stats = benchmark_memory(func, n)
print(f"{name}:")
print(f" Peak Memory: {stats['peak_mb']:.2f} MB")
print(f" Time: {stats['time_s']:.4f} s\n")
5.9 Speicherlecks in Klassen finden
import tracemalloc
import weakref
class LeakyClass:
instances = [] # Klassenvariable - hält Referenzen
def __init__(self, data):
self.data = data
LeakyClass.instances.append(self) # Speicherleck!
class CleanClass:
instances = []
def __init__(self, data):
self.data = data
# Schwache Referenz verwenden
CleanClass.instances.append(weakref.ref(self))
def test_leak():
tracemalloc.start()
# Leaky Version
snapshot1 = tracemalloc.take_snapshot()
for _ in range(1000):
obj = LeakyClass([i for i in range(1000)])
# obj geht aus dem Scope, aber Referenz bleibt in instances
snapshot2 = tracemalloc.take_snapshot()
# Clean Version
for _ in range(1000):
obj = CleanClass([i for i in range(1000)])
snapshot3 = tracemalloc.take_snapshot()
# Vergleich
print("=== Leaky Class ===")
stats = snapshot2.compare_to(snapshot1, 'lineno')
for stat in stats[:3]:
print(stat)
print("\n=== Clean Class ===")
stats = snapshot3.compare_to(snapshot2, 'lineno')
for stat in stats[:3]:
print(stat)
tracemalloc.stop()
test_leak()
5.10 Integration in Unit Tests
import tracemalloc
import unittest
class MemoryTestCase(unittest.TestCase):
def setUp(self):
tracemalloc.start()
self.snapshot_before = tracemalloc.take_snapshot()
def tearDown(self):
snapshot_after = tracemalloc.take_snapshot()
top_stats = snapshot_after.compare_to(
self.snapshot_before, 'lineno'
)
# Warnung bei hohem Speicherverbrauch
for stat in top_stats[:5]:
if stat.size_diff > 10 * 1024 * 1024: # > 10 MB
print(f"\nWARNING: High memory increase: {stat}")
tracemalloc.stop()
def test_large_allocation(self):
"""Test mit Speicherüberwachung"""
data = [i for i in range(1000000)]
self.assertEqual(len(data), 1000000)
if __name__ == '__main__':
unittest.main()
5.11 Best Practices
✅ DO:
tracemallocfür Entwicklung und Debugging verwenden- Snapshots vor/nach kritischen Operationen
- Filter verwenden um Noise zu reduzieren
- Mit Context Managers arbeiten
- Peak Memory beachten, nicht nur Current
❌ DON’T:
tracemallocin Produktion laufen lassen (Performance-Overhead ~2-3x)- Zu häufig Snapshots nehmen (selbst speicherintensiv)
- Ohne Filter arbeiten bei großen Projekten
start()ohnestop()aufrufen
5.12 Alternative Tools
5.12.1 memory_profiler
pip install memory_profiler
from memory_profiler import profile
@profile
def my_func():
a = [1] * (10 ** 6)
b = [2] * (2 * 10 ** 7)
del b
return a
# Run with: python -m memory_profiler script.py
5.12.2 guppy3 / heapy
pip install guppy3
from guppy import hpy
h = hpy()
print(h.heap())
5.12.3 pympler
pip install pympler
from pympler import asizeof
data = [i for i in range(1000)]
print(f"Size: {asizeof.asizeof(data)} bytes")
5.13 Praktisches Beispiel: Memory Leak finden
import tracemalloc
class DataProcessor:
cache = {} # Klassenvariable - potenzielles Leak
def process(self, data):
# Cache wächst unbegrenzt
key = str(data)
if key not in self.cache:
self.cache[key] = [d * 2 for d in data]
return self.cache[key]
def find_leak():
tracemalloc.start()
processor = DataProcessor()
snapshots = []
# Mehrere Iterationen
for i in range(5):
# Viele verschiedene Daten verarbeiten
for _ in range(100):
data = list(range(1000 + i * 100))
processor.process(data)
snapshot = tracemalloc.take_snapshot()
snapshots.append(snapshot)
if i > 0:
stats = snapshot.compare_to(snapshots[i-1], 'lineno')
print(f"\n=== Iteration {i} ===")
for stat in stats[:3]:
print(stat)
# Cache-Größe
print(f"\nCache entries: {len(DataProcessor.cache)}")
tracemalloc.stop()
find_leak()
Lösung:
from functools import lru_cache
class DataProcessor:
@lru_cache(maxsize=100) # Begrenzter Cache
def process(self, data):
data_tuple = tuple(data) # Hashable machen
return [d * 2 for d in data_tuple]
5.14 Zusammenfassung
| Funktion | Zweck |
|---|---|
tracemalloc.start() | Tracking starten |
tracemalloc.stop() | Tracking stoppen |
take_snapshot() | Speicher-Snapshot erstellen |
get_traced_memory() | Current/Peak Memory abrufen |
snapshot.statistics() | Top-Verbraucher analysieren |
snapshot.compare_to() | Zwei Snapshots vergleichen |
Filter() | Traces filtern |
Kernprinzip: tracemalloc hilft, Speicherlecks zu finden und Speicherverbrauch zu optimieren. Es sollte während der Entwicklung verwendet werden, nicht in Produktion. Kombiniere es mit Snapshots und Filtern für präzise Analysen.
Kontext-Manager und contextlib
Kontextmanager gewährleisten eine saubere Ressourcenverwaltung durch automatisches Setup und Cleanup. Sie sind unverzichtbar beim Arbeiten mit Dateien, Datenbankverbindungen, Locks und anderen Ressourcen, die nach Gebrauch freigegeben werden müssen.
1 Das with-Statement
Das with-Statement stellt sicher, dass Ressourcen korrekt initialisiert und anschließend freigegeben werden – selbst wenn Fehler auftreten.
1.1 Dateien öffnen ohne with
# Klassischer Ansatz (fehleranfällig)
file = open('data.txt', 'r')
try:
content = file.read()
print(content)
finally:
file.close() # Manuelles Cleanup
1.2 Dateien öffnen mit with
# Moderner Ansatz (empfohlen)
with open('data.txt', 'r') as file:
content = file.read()
print(content)
# Datei wird automatisch geschlossen
Vorteile:
- Automatisches Schließen der Datei, auch bei Exceptions
- Keine
finally-Blöcke nötig - Kompakter und lesbarer Code
2 Eigene Kontextmanager mit __enter__ und __exit__
Ein Kontextmanager ist jede Klasse, die die Methoden __enter__() und __exit__() implementiert.
2.1 Einfacher Kontextmanager
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None
def __enter__(self):
print(f'Opening connection to {self.db_name}')
self.connection = f'Connection to {self.db_name}'
return self.connection # Wird an 'as'-Variable übergeben
def __exit__(self, exc_type, exc_value, traceback):
print(f'Closing connection to {self.db_name}')
if exc_type is not None:
print(f'Exception occurred: {exc_value}')
# False zurückgeben lässt Exception weiterpropagieren
# True unterdrückt die Exception
return False
# Verwendung
with DatabaseConnection('mydb') as conn:
print(f'Working with {conn}')
# raise ValueError('Test error') # Würde trotzdem cleanup ausführen
2.2 Parameter von __exit__
| Parameter | Beschreibung |
|---|---|
exc_type | Typ der Exception (z.B. ValueError) oder None |
exc_value | Exception-Objekt oder None |
traceback | Traceback-Objekt oder None |
| Rückgabewert | True unterdrückt Exception, False propagiert sie |
2.3 Timer-Kontextmanager
import time
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, *args):
self.end = time.time()
self.elapsed = self.end - self.start
print(f'Elapsed time: {self.elapsed:.4f} seconds')
return False
# Verwendung
with Timer():
# Zeitintensiver Code
sum(range(1_000_000))
3 Das contextlib-Modul
Das contextlib-Modul bietet Hilfsfunktionen zum Erstellen und Arbeiten mit Kontextmanagern.
3.1 @contextmanager Decorator
Der einfachste Weg, einen Kontextmanager zu erstellen, ist mit dem @contextmanager-Decorator.
from contextlib import contextmanager
@contextmanager
def file_manager(filename, mode):
print(f'Opening {filename}')
file = open(filename, mode)
try:
yield file # Alles vor yield = __enter__, danach = __exit__
finally:
print(f'Closing {filename}')
file.close()
# Verwendung
with file_manager('test.txt', 'w') as f:
f.write('Hello World')
Wichtig:
- Code vor
yieldentspricht__enter__ yieldgibt den Wert zurück (wiereturnin__enter__)- Code nach
yieldentspricht__exit__ finally-Block garantiert Cleanup auch bei Exceptions
3.2 Mehrere Ressourcen verwalten
from contextlib import contextmanager
@contextmanager
def multi_file_manager(*filenames):
files = []
try:
# Alle Dateien öffnen
for filename in filenames:
files.append(open(filename, 'r'))
yield files
finally:
# Alle Dateien schließen
for f in files:
f.close()
# Verwendung
with multi_file_manager('file1.txt', 'file2.txt') as (f1, f2):
content1 = f1.read()
content2 = f2.read()
4 Mehrere Kontextmanager kombinieren
4.1 Verschachtelt
with open('input.txt', 'r') as infile:
with open('output.txt', 'w') as outfile:
content = infile.read()
outfile.write(content.upper())
4.2 In einer Zeile (Python 3.1+)
with open('input.txt', 'r') as infile, open('output.txt', 'w') as outfile:
content = infile.read()
outfile.write(content.upper())
4.3 Mit ExitStack (flexibel)
from contextlib import ExitStack
filenames = ['file1.txt', 'file2.txt', 'file3.txt']
with ExitStack() as stack:
# Dynamisch Dateien öffnen
files = [stack.enter_context(open(fn, 'r')) for fn in filenames]
# Mit allen Dateien arbeiten
for f in files:
print(f.read())
# Alle werden automatisch geschlossen
Vorteile von ExitStack:
- Dynamische Anzahl von Kontextmanagern
- Manuelle Registrierung von Callbacks mit
stack.callback() - Besonders nützlich bei unbekannter Anzahl von Ressourcen
5 Nützliche Kontextmanager aus contextlib
5.1 suppress – Exceptions unterdrücken
from contextlib import suppress
# Fehler ignorieren statt try-except
with suppress(FileNotFoundError):
os.remove('nonexistent_file.txt')
# Kein Fehler, wenn Datei nicht existiert
5.2 redirect_stdout – Ausgabe umleiten
from contextlib import redirect_stdout
import io
# stdout in String-Buffer umleiten
output = io.StringIO()
with redirect_stdout(output):
print('This goes to the buffer')
print('And this too')
captured = output.getvalue()
print(f'Captured: {captured}')
5.3 redirect_stderr – Fehlerausgabe umleiten
from contextlib import redirect_stderr
import io
error_buffer = io.StringIO()
with redirect_stderr(error_buffer):
import sys
sys.stderr.write('Error message')
errors = error_buffer.getvalue()
5.4 closing – Objekte mit close() verwalten
from contextlib import closing
from urllib.request import urlopen
# Für Objekte, die close() haben, aber kein with-Statement
with closing(urlopen('http://example.com')) as page:
content = page.read()
# page.close() wird automatisch aufgerufen
5.5 nullcontext – Optionaler Kontextmanager
from contextlib import nullcontext
def process_file(filename, use_context=True):
cm = open(filename, 'r') if use_context else nullcontext()
with cm as f:
# f ist entweder File-Objekt oder None
if f:
return f.read()
return None
6 Kontextmanager für Threading und Locks
6.1 Thread-Lock
import threading
lock = threading.Lock()
# Ohne with (manuell)
lock.acquire()
try:
# Critical section
pass
finally:
lock.release()
# Mit with (automatisch)
with lock:
# Critical section
pass
# Lock wird automatisch freigegeben
6.2 Eigener Lock-Kontextmanager
@contextmanager
def acquire_lock(lock, timeout=10):
acquired = lock.acquire(timeout=timeout)
try:
if not acquired:
raise TimeoutError('Could not acquire lock')
yield acquired
finally:
if acquired:
lock.release()
# Verwendung
with acquire_lock(lock):
print('Lock acquired')
7 Asynchrone Kontextmanager
Bei asynchronem Code (async/await) gibt es spezielle Kontextmanager mit __aenter__ und __aexit__.
7.1 Async Kontextmanager definieren
import asyncio
class AsyncDatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
async def __aenter__(self):
print(f'Opening async connection to {self.db_name}')
await asyncio.sleep(0.1) # Simuliert async I/O
return self
async def __aexit__(self, exc_type, exc_value, traceback):
print(f'Closing async connection to {self.db_name}')
await asyncio.sleep(0.1)
return False
# Verwendung
async def main():
async with AsyncDatabaseConnection('asyncdb') as conn:
print('Working with async connection')
asyncio.run(main())
7.2 Mit @asynccontextmanager
from contextlib import asynccontextmanager
import asyncio
@asynccontextmanager
async def async_file_manager(filename):
print(f'Opening {filename} asynchronously')
await asyncio.sleep(0.1)
file = open(filename, 'r')
try:
yield file
finally:
print(f'Closing {filename} asynchronously')
file.close()
await asyncio.sleep(0.1)
# Verwendung
async def main():
async with async_file_manager('test.txt') as f:
content = f.read()
asyncio.run(main())
8 Best Practices
8.1 Wann eigene Kontextmanager erstellen?
Gute Anwendungsfälle:
- Ressourcen mit Setup/Cleanup (Dateien, Verbindungen, Locks)
- Temporäre Zustandsänderungen (Working Directory, Umgebungsvariablen)
- Zeitmessungen und Profiling
- Transaktionen (Begin/Commit/Rollback)
- Logging-Scopes
8.2 @contextmanager vs. Klasse?
| Kriterium | @contextmanager | Klasse mit __enter__/__exit__ |
|---|---|---|
| Einfache Anwendung | ✅ Bevorzugt | ❌ Mehr Boilerplate |
| Wiederverwendbarkeit | ✅ Gut | ✅ Sehr gut |
| State-Management | ⚠️ Begrenzt | ✅ Flexibel |
| Exception-Handling | ⚠️ Muss explizit sein | ✅ Klare Kontrolle |
| Code-Übersichtlichkeit | ✅ Kompakt | ⚠️ Mehr Code |
Faustregel: Für einfache Fälle @contextmanager, für komplexe Logik oder State-Management eine Klasse.
8.3 Typische Fehler vermeiden
# ❌ Falsch: yield vergessen
@contextmanager
def bad_context():
print('Enter')
# Fehlt: yield
print('Exit')
# ✅ Richtig: yield nicht vergessen
@contextmanager
def good_context():
print('Enter')
yield
print('Exit')
# ❌ Falsch: Keine Exception-Behandlung
@contextmanager
def unsafe_context():
resource = acquire_resource()
yield resource
release_resource(resource) # Wird bei Exception nicht ausgeführt!
# ✅ Richtig: finally verwenden
@contextmanager
def safe_context():
resource = acquire_resource()
try:
yield resource
finally:
release_resource(resource) # Wird immer ausgeführt
9 Praxisbeispiele
9.1 Temporäres Arbeitsverzeichnis
import os
from contextlib import contextmanager
@contextmanager
def temporary_directory(path):
original_dir = os.getcwd()
try:
os.chdir(path)
yield path
finally:
os.chdir(original_dir)
# Verwendung
with temporary_directory('/tmp'):
print(f'Current dir: {os.getcwd()}') # /tmp
print(f'Back to: {os.getcwd()}') # Original directory
9.2 Datenbank-Transaktion
@contextmanager
def transaction(connection):
"""Automatisches Commit/Rollback bei Datenbanktransaktionen"""
cursor = connection.cursor()
try:
yield cursor
connection.commit() # Erfolg → Commit
except Exception:
connection.rollback() # Fehler → Rollback
raise
finally:
cursor.close()
# Verwendung
# with transaction(db_connection) as cursor:
# cursor.execute('INSERT INTO ...')
# cursor.execute('UPDATE ...')
# Automatisches Commit bei Erfolg, Rollback bei Exception
9.3 Profiling-Context
import cProfile
from contextlib import contextmanager
@contextmanager
def profile_code(sort_by='cumulative'):
"""Profiliert Code-Block"""
profiler = cProfile.Profile()
profiler.enable()
try:
yield profiler
finally:
profiler.disable()
profiler.print_stats(sort=sort_by)
# Verwendung
with profile_code():
# Code der profiliert werden soll
result = sum(range(1_000_000))
9.4 Temporäre Environment Variables
import os
from contextlib import contextmanager
@contextmanager
def env_variable(key, value):
"""Setzt temporär eine Umgebungsvariable"""
old_value = os.environ.get(key)
os.environ[key] = value
try:
yield
finally:
if old_value is None:
del os.environ[key]
else:
os.environ[key] = old_value
# Verwendung
with env_variable('DEBUG', 'true'):
print(os.environ['DEBUG']) # 'true'
print(os.environ.get('DEBUG')) # None oder alter Wert
10 Zusammenfassung
| Konzept | Verwendung |
|---|---|
with-Statement | Automatisches Setup/Cleanup von Ressourcen |
__enter__ / __exit__ | Kontextmanager als Klasse implementieren |
@contextmanager | Generator-basierter Kontextmanager (einfacher) |
contextlib.suppress | Exceptions unterdrücken |
contextlib.redirect_stdout | Ausgaben umleiten |
contextlib.ExitStack | Dynamische Anzahl von Kontextmanagern |
async with | Asynchrone Kontextmanager mit __aenter__ / __aexit__ |
Kernprinzip: Kontextmanager garantieren, dass Cleanup-Code ausgeführt wird – egal ob der Code normal endet oder eine Exception auftritt. Sie sind der Standard-Ansatz für Ressourcenverwaltung in Python.
Debugging
Python bietet mehrere Möglichkeiten zum Debuggen: durch klassische Print-Ausgaben, durch das integrierte Debugging-Modul pdb oder mithilfe von Entwicklungsumgebungen wie Visual Studio Code (VS Code).
1 Debugging mit Print-Anweisungen
Dies ist die einfachste, aber oft sehr effektive Methode. Dabei werden Variablenwerte oder Kontrollflussinformationen mit print() ausgegeben.
def divide_numbers(a, b):
print('a:', a) # Wert von a ausgeben
print('b:', b) # Wert von b ausgeben
if b == 0:
print('Fehler: Division durch Null')
return None
return a / b
result = divide_numbers(10, 0)
print('Ergebnis:', result)
2 Debugging mit dem pdb-Modul
Das Modul pdb (Python Debugger) erlaubt schrittweises Durchlaufen des Codes mit interaktiver Steuerung.
2.1 Beispiel
import pdb
def greet_user(name):
pdb.set_trace() # Hier startet der Debugger
greeting = 'Hello ' + name
print(greeting)
greet_user('Anna')
2.2 Wichtige pdb-Kommandos
n: next – führt die nächste Zeile auss: step – springt in Funktionsaufrufe hineinc: continue – setzt das Programm bis zum nächsten Haltepunkt fortq: quit – verlässt den Debuggerp variable: gibt den Wert einer Variablen aus
3 Fehlerbehandlung mit try-except
Manche Fehler lassen sich nicht vermeiden, können aber abgefangen und behandelt werden.
def convert_to_int(value):
try:
return int(value)
except ValueError as e:
print('Ungültige Eingabe:', e)
return None
number = convert_to_int('abc')
print('Ergebnis:', number)
4 Verwendung von Loggern
4.1 Aus Standardbibliothek
4.1.1 Logging-Level
Wenn z. B. Log-Dateien erstellt werden sollen, in der nur Fehler oder kritische Ereignisse gespeichert werden sollen.
| Level | Beschreibung |
|---|---|
DEBUG | Detaillierte Debug-Informationen |
INFO | Nur zur Info: Dinge, die wie vorgesehen passiert sein |
WARNING | Wenn etwas Unerwartetes passiert ist, es hat jedoch nicht zu einem Absturz geführt hat |
ERROR | Schwerwiegender Fehler, der nicht zu einem Programmabsturz geführt hat (z. B. das Programm konnte eine bestimmte Funktion nicht ausführen) |
CRITICAL | Schwerwiegender, der zu einem Programmabsturz pgeführt hat |
Beispiel:
import logging
# Setup the logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Formatter (optional)
formatter = logging.Formatter(
'%(asctime)s:%(levelname)s:%(funcName)s:%(message)s',
)
# File handler
filepath = Path(__file__).parent.joinpath('log_standard.log')
file_handler = logging.FileHandler(filepath)
file_handler.setLevel(logging.INFO) # Info and higher will be logged
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
def divide_integers(a: int, b: int) -> float | None:
try:
logger.info(f'a={a}, b={b}')
result = a / b
return result
except ZeroDivisionError as e:
logger.exception(f'Exception was raised: {e}')
return None
def main() -> None:
for _ in range(3):
print(divide_integers(10, 0))
if __name__ == '__main__':
main()
Inhalt der erstellten Datei log_standard.log:
2025-03-22 12:05:07,716:INFO:divide_integers:a=10, b=0
2025-03-22 12:05:07,716:ERROR:divide_integers:Exception was raised: division by zero
Traceback (most recent call last):
File ".../logging_standard_lib--edit.py", line 28, in divide_integers
result = a / b
ZeroDivisionError: division by zero
[...]
4.2 Externer Logger
Ohne Boilerplate:
from loguru import logger
logger.debug("That's it, beautiful and simple logging!")
5 Debugging in Visual Studio Code (VS Code)
VS Code bietet eine sehr leistungsstarke Debugging-Umgebung mit grafischer Oberfläche.
5.1 Voraussetzungen
- Erweiterung “Python” von Microsoft muss installiert sein
- Eine Datei mit Python-Code geöffnet
5.2 Breakpoints setzen
- Links neben der Zeilennummer klicken, um einen roten Punkt (Haltepunkt) zu setzen
- Alternativ: F9 drücken bei ausgewählter Zeile
5.3 Debugging starten
- Run-Ansicht öffnen (links oder mit Strg+Shift+D)
- Auf ‘Start Debugging’ klicken oder drücke F5 drücken
- Man kann Variablen inspizieren, in den Code hineinschreiten (F11), darüber hinweg gehen (F10) oder zur nächsten Haltestelle springen (Shift+F5).
5.4 Variablen und Ausdrucksinspektion
- In der Debug-Seitenleiste unter “Variables” sieht man die aktuellen Werte.
- Im “Debug Console”-Fenster kann man Ausdrücke interaktiv testen.
5.5 Launch-Konfiguration anpassen
Erstelle eine Datei .vscode/launch.json, um z. B. Argumente zu übergeben:
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Datei starten",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}
6 Tipps für effektives Debugging
- Reproduzierbare Fehlerfälle schaffen
- Mit kleinen Datenmengen testen
- Unit Tests schreiben, um Fehler frühzeitig zu entdecken
- Kommentieren und Abschnitte temporär deaktivieren
- Code aufteilen in kleinere, testbare Funktionen
Unit Tests
1 Unit Tests mit pytest
Unit Tests prüfen einzelne Komponenten (Funktionen, Klassen) isoliert. Pytest ist das beliebteste Test-Framework für Python und bietet mächtige Features für produktiven Test-Code.
1.1 Grundlagen
1.1.1 Installation und Setup
pip install pytest pytest-cov
Projekt-Struktur:
myproject/
├── src/
│ ├── __init__.py
│ ├── calculator.py
│ └── user.py
├── tests/
│ ├── __init__.py
│ ├── test_calculator.py
│ └── test_user.py
└── pytest.ini
1.1.2 Einfacher Test
# src/calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# tests/test_calculator.py
from src.calculator import add, divide
import pytest
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_divide():
assert divide(10, 2) == 5
assert divide(9, 3) == 3
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
Tests ausführen:
# Alle Tests
pytest
# Verbose Output
pytest -v
# Spezifische Datei
pytest tests/test_calculator.py
# Spezifischer Test
pytest tests/test_calculator.py::test_add
# Mit Coverage
pytest --cov=src tests/
1.2 Assertions
1.2.1 Basis-Assertions
def test_assertions():
# Gleichheit
assert 1 + 1 == 2
assert "hello" == "hello"
# Ungleichheit
assert 5 != 3
# Boolean
assert True
assert not False
# Membership
assert 3 in [1, 2, 3]
assert "a" not in "xyz"
# Identität
x = [1, 2]
y = x
assert x is y
assert x is not [1, 2]
1.2.2 Erweiterte Assertions
def test_advanced_assertions():
# Approximation (Float-Vergleich)
assert 0.1 + 0.2 == pytest.approx(0.3)
assert 100 == pytest.approx(105, rel=0.1) # 10% Toleranz
# Listen/Sets
assert [1, 2, 3] == [1, 2, 3]
assert {1, 2} == {2, 1} # Set-Reihenfolge egal
# Dictionaries
assert {"a": 1, "b": 2} == {"b": 2, "a": 1}
# Teilmengen
assert {"a", "b"} <= {"a", "b", "c"}
1.2.3 Exception-Testing
def test_exceptions():
# Einfach
with pytest.raises(ValueError):
int("invalid")
# Mit Message-Check
with pytest.raises(ValueError, match="invalid literal"):
int("invalid")
# Exception-Objekt inspizieren
with pytest.raises(ValueError) as exc_info:
raise ValueError("Custom message")
assert "Custom" in str(exc_info.value)
assert exc_info.type is ValueError
1.3 Fixtures
Fixtures sind wiederverwendbare Setup-Funktionen für Tests.
1.3.1 Basis-Fixtures
import pytest
# Einfache Fixture
@pytest.fixture
def sample_data():
"""Gibt Test-Daten zurück"""
return [1, 2, 3, 4, 5]
def test_sum(sample_data):
assert sum(sample_data) == 15
def test_length(sample_data):
assert len(sample_data) == 5
1.3.2 Setup und Teardown
import pytest
from pathlib import Path
@pytest.fixture
def temp_file(tmp_path):
"""Erstellt temporäre Datei, löscht sie nach Test"""
# Setup
file_path = tmp_path / "test.txt"
file_path.write_text("Hello World")
# Fixture-Wert übergeben
yield file_path
# Teardown (wird nach Test ausgeführt)
if file_path.exists():
file_path.unlink()
print("Cleanup completed")
def test_file_content(temp_file):
content = temp_file.read_text()
assert content == "Hello World"
1.3.3 Fixture-Scopes
import pytest
# Function-Scope (Standard - für jeden Test neu)
@pytest.fixture(scope="function")
def function_fixture():
print("\nSetup function fixture")
return "data"
# Class-Scope (einmal pro Test-Klasse)
@pytest.fixture(scope="class")
def class_fixture():
print("\nSetup class fixture")
return "class data"
# Module-Scope (einmal pro Modul)
@pytest.fixture(scope="module")
def module_fixture():
print("\nSetup module fixture")
db = Database()
yield db
db.close()
# Session-Scope (einmal pro Test-Session)
@pytest.fixture(scope="session")
def session_fixture():
print("\nSetup session fixture")
return "session data"
1.3.4 Fixture-Dependencies
import pytest
@pytest.fixture
def database():
"""Simulierte Datenbank"""
db = {"users": []}
yield db
db.clear()
@pytest.fixture
def user(database):
"""Benötigt database-Fixture"""
user = {"id": 1, "name": "Alice"}
database["users"].append(user)
return user
def test_user_in_database(database, user):
assert user in database["users"]
assert len(database["users"]) == 1
1.3.5 Autouse-Fixtures
import pytest
@pytest.fixture(autouse=True)
def reset_state():
"""Wird automatisch vor jedem Test ausgeführt"""
global_state.clear()
yield
# Cleanup nach Test
1.3.6 Built-in Fixtures
def test_tmp_path(tmp_path):
"""tmp_path: Temporäres Verzeichnis (pathlib.Path)"""
file = tmp_path / "test.txt"
file.write_text("content")
assert file.read_text() == "content"
def test_tmp_path_factory(tmp_path_factory):
"""Erstellt mehrere temp Verzeichnisse"""
dir1 = tmp_path_factory.mktemp("data1")
dir2 = tmp_path_factory.mktemp("data2")
assert dir1 != dir2
def test_monkeypatch(monkeypatch):
"""monkeypatch: Temporär Code ändern"""
import os
monkeypatch.setenv("API_KEY", "test_key")
assert os.environ["API_KEY"] == "test_key"
# Nach Test wird original wiederhergestellt
def test_capsys(capsys):
"""capsys: stdout/stderr erfassen"""
print("Hello")
print("World", file=sys.stderr)
captured = capsys.readouterr()
assert captured.out == "Hello\n"
assert captured.err == "World\n"
1.4 Parametrized Tests
Tests mit mehreren Input/Output-Kombinationen.
1.4.1 Basis-Parametrize
import pytest
@pytest.mark.parametrize("input,expected", [
(2, 4),
(3, 9),
(4, 16),
(5, 25),
])
def test_square(input, expected):
assert input ** 2 == expected
# Output:
# test_square[2-4] PASSED
# test_square[3-9] PASSED
# test_square[4-16] PASSED
# test_square[5-25] PASSED
1.4.2 Mehrere Parameter
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(10, 5, 15),
(-1, 1, 0),
(0, 0, 0),
])
def test_add(a, b, expected):
assert a + b == expected
1.4.3 IDs für lesbare Test-Namen
@pytest.mark.parametrize("input,expected", [
(2, 4),
(3, 9),
(5, 25),
], ids=["two", "three", "five"])
def test_square(input, expected):
assert input ** 2 == expected
# Output:
# test_square[two] PASSED
# test_square[three] PASSED
# test_square[five] PASSED
1.4.4 Parametrize mit pytest.param
@pytest.mark.parametrize("input,expected", [
(2, 4),
pytest.param(0, 0, marks=pytest.mark.skip),
pytest.param(3, 9, marks=pytest.mark.xfail),
(4, 16),
])
def test_square(input, expected):
assert input ** 2 == expected
1.4.5 Verschachtelte Parametrize
@pytest.mark.parametrize("x", [1, 2, 3])
@pytest.mark.parametrize("y", [10, 20])
def test_multiply(x, y):
result = x * y
assert result == x * y
# Generiert 6 Tests: (1,10), (1,20), (2,10), (2,20), (3,10), (3,20)
1.4.6 Parametrize mit Fixtures
@pytest.fixture(params=[1, 2, 3])
def number(request):
return request.param
def test_positive(number):
assert number > 0
# Generiert 3 Tests mit number=1, 2, 3
1.5 Marks und Test-Organisation
1.5.1 Basis-Marks
import pytest
@pytest.mark.skip(reason="Not implemented yet")
def test_feature():
pass
@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
def test_unix_feature():
pass
@pytest.mark.xfail(reason="Known bug #123")
def test_buggy_feature():
assert False
@pytest.mark.slow
def test_expensive_operation():
# Langer Test
pass
Tests nach Marks ausführen:
# Nur slow Tests
pytest -m slow
# Alles außer slow
pytest -m "not slow"
# Kombinationen
pytest -m "slow and database"
pytest -m "slow or database"
1.5.2 Custom Marks
# pytest.ini
[pytest]
markers =
slow: marks tests as slow
integration: integration tests
unit: unit tests
api: API tests
# tests/test_app.py
@pytest.mark.unit
def test_function():
pass
@pytest.mark.integration
@pytest.mark.slow
def test_full_workflow():
pass
1.5.3 Test-Klassen
class TestCalculator:
@pytest.fixture(autouse=True)
def setup(self):
"""Wird vor jedem Test in der Klasse ausgeführt"""
self.calc = Calculator()
def test_add(self):
assert self.calc.add(2, 3) == 5
def test_subtract(self):
assert self.calc.subtract(5, 3) == 2
@pytest.mark.parametrize("a,b,expected", [
(10, 2, 5),
(20, 4, 5),
])
def test_divide(self, a, b, expected):
assert self.calc.divide(a, b) == expected
1.6 Mocking
Mocking ersetzt echte Objekte durch kontrollierte Fakes.
1.6.1 unittest.mock Basics
from unittest.mock import Mock, MagicMock, patch
def test_mock_basics():
# Mock erstellen
mock = Mock()
# Rückgabewert setzen
mock.return_value = 42
assert mock() == 42
# Aufrufe prüfen
mock()
mock.assert_called()
mock.assert_called_once()
# Mit Argumenten
mock(1, 2, key='value')
mock.assert_called_with(1, 2, key='value')
1.6.2 Funktionen patchen
from unittest.mock import patch
import requests
def get_user_data(user_id):
"""Holt Daten von API"""
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
def test_get_user_data():
# requests.get mocken
with patch('requests.get') as mock_get:
# Mock-Response konfigurieren
mock_get.return_value.json.return_value = {
'id': 1,
'name': 'Alice'
}
result = get_user_data(1)
assert result['name'] == 'Alice'
mock_get.assert_called_once_with('https://api.example.com/users/1')
1.6.3 Decorator-Style Patching
@patch('requests.get')
def test_api_call(mock_get):
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {'data': 'test'}
result = get_user_data(1)
assert result == {'data': 'test'}
1.6.4 Mehrere Patches
@patch('module.function_b')
@patch('module.function_a')
def test_multiple_patches(mock_a, mock_b):
# WICHTIG: Reihenfolge ist umgekehrt!
mock_a.return_value = 'A'
mock_b.return_value = 'B'
1.6.5 Side Effects
def test_side_effects():
mock = Mock()
# Verschiedene Rückgabewerte
mock.side_effect = [1, 2, 3]
assert mock() == 1
assert mock() == 2
assert mock() == 3
# Exception werfen
mock.side_effect = ValueError("Error")
with pytest.raises(ValueError):
mock()
# Custom Funktion
mock.side_effect = lambda x: x * 2
assert mock(5) == 10
1.6.6 Attribute und Methoden mocken
def test_mock_object():
# Mock-Objekt mit Attributen
mock_user = Mock()
mock_user.name = "Alice"
mock_user.age = 30
mock_user.get_email.return_value = "alice@example.com"
assert mock_user.name == "Alice"
assert mock_user.get_email() == "alice@example.com"
1.6.7 Pytest-mock Plugin
pip install pytest-mock
def test_with_mocker(mocker):
# Eleganter als unittest.mock
mock = mocker.patch('requests.get')
mock.return_value.json.return_value = {'data': 'test'}
result = get_user_data(1)
assert result == {'data': 'test'}
1.7 Test Coverage
Coverage misst, welcher Code von Tests abgedeckt wird.
1.7.1 Coverage ausführen
# Basic Coverage
pytest --cov=src tests/
# HTML-Report
pytest --cov=src --cov-report=html tests/
open htmlcov/index.html
# Terminal-Report mit fehlenden Zeilen
pytest --cov=src --cov-report=term-missing tests/
# Nur Coverage (ohne Tests)
pytest --cov=src --cov-report=term-missing --cov-fail-under=80 tests/
1.7.2 Coverage-Konfiguration
# .coveragerc oder pyproject.toml
[tool.coverage.run]
source = ["src"]
omit = [
"*/tests/*",
"*/test_*.py",
"*/__pycache__/*",
"*/site-packages/*"
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
1.7.3 Coverage ausschließen
def critical_function():
result = complex_operation()
if result: # pragma: no cover
# Nur in speziellen Fällen ausgeführt
handle_special_case()
return result
1.8 Praktische Patterns
1.8.1 Konfigurations-Fixture
# conftest.py (wird automatisch geladen)
import pytest
@pytest.fixture(scope="session")
def config():
"""Globale Konfiguration"""
return {
'api_url': 'http://localhost:8000',
'timeout': 30,
'debug': True
}
@pytest.fixture(scope="session")
def database_url():
"""Test-Datenbank URL"""
return "sqlite:///:memory:"
1.8.2 Datenbank-Tests
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope="module")
def engine():
"""Erstellt Test-Datenbank"""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
yield engine
Base.metadata.drop_all(engine)
@pytest.fixture
def db_session(engine):
"""Session mit Rollback nach Test"""
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.rollback()
session.close()
def test_create_user(db_session):
user = User(name="Alice", email="alice@example.com")
db_session.add(user)
db_session.commit()
assert user.id is not None
assert db_session.query(User).count() == 1
1.8.3 API-Testing
import pytest
import requests
@pytest.fixture
def api_client():
"""API Client mit Base URL"""
class APIClient:
base_url = "http://localhost:8000/api"
def get(self, endpoint):
return requests.get(f"{self.base_url}{endpoint}")
def post(self, endpoint, data):
return requests.post(f"{self.base_url}{endpoint}", json=data)
return APIClient()
def test_get_users(api_client, mocker):
# Mock HTTP-Call
mock_response = mocker.Mock()
mock_response.json.return_value = [{"id": 1, "name": "Alice"}]
mock_response.status_code = 200
mocker.patch('requests.get', return_value=mock_response)
response = api_client.get('/users')
assert response.status_code == 200
assert len(response.json()) == 1
1.9 Best Practices
✅ DO:
- Ein Assert pro Test (wenn möglich)
- Descriptive Test-Namen (
test_user_creation_with_invalid_email) - Fixtures für Setup/Teardown
- Parametrize für ähnliche Tests
- Mocking für externe Dependencies
- Coverage > 80% anstreben
❌ DON’T:
- Tests von anderen Tests abhängig machen
- Global State zwischen Tests teilen
- Zu komplexe Fixtures
- Echte Datenbanken/APIs in Unit Tests
- Tests ohne Assertions
1.10 pytest.ini Konfiguration
[pytest]
# Mindest-Python-Version
minversion = 6.0
# Wo Tests liegen
testpaths = tests
# Datei-Pattern
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
# Optionen für pytest
addopts =
-ra
--strict-markers
--strict-config
--showlocals
--cov=src
--cov-report=term-missing:skip-covered
--cov-report=html
# Custom Markers
markers =
slow: slow tests
integration: integration tests
unit: unit tests
# Filterwarnings
filterwarnings =
error
ignore::UserWarning
1.11 Zusammenfassung
| Feature | Verwendung |
|---|---|
| Fixtures | Setup/Teardown, Wiederverwendung |
| Parametrize | Mehrere Input/Output-Kombinationen |
| Marks | Tests kategorisieren/überspringen |
| Mocking | Externe Dependencies ersetzen |
| Coverage | Code-Abdeckung messen |
| conftest.py | Gemeinsame Fixtures |
Kernprinzip: Pytest macht Tests einfach und lesbar. Nutze Fixtures für Setup, Parametrize für Wiederholung, Mocking für Isolation, und Coverage für Vollständigkeit. Schreibe Tests, die schnell, isoliert und deterministisch sind.
Profiling und Timing
1 Dauer der Code-Ausführung messen
Die Funktion time.time() gibt die aktuelle Zeit als Unix-Timestamp in Sekunden zurück. Sie ist allerdings nicht ideal für präzise Zeitmessungen, weil sie durch folgende Faktoren beeinflusst wird:
- Geringe Präzision auf manchen Systemen
time.time()basiert auf der Systemuhr, die je nach Betriebssystem nicht hochauflösend ist.- Bei Windows hat
time.time()oft nur eine Genauigkeit von etwa 15,6 Millisekunden.
- Sprünge durch Systemzeit-Änderungen
- Wenn die Systemuhr manuell geändert oder durch NTP (Network Time Protocol) synchronisiert wird, kann
time.time()springen oder rückwärtslaufen.
- Wenn die Systemuhr manuell geändert oder durch NTP (Network Time Protocol) synchronisiert wird, kann
- Nicht speziell für Zeitmessungen gedacht
time.time()ist hauptsächlich für das Abrufen der aktuellen Uhrzeit gedacht, nicht für Hochpräzisionsmessungen.
warum ist `time.perf_counter()` besser?
Die Funktion time.perf_counter() ist speziell für hochpräzise Zeitmessungen gedacht:
- Hohe Auflösung
- Nutzt die genaueste verfügbare Uhr des Systems.
- Auf modernen Systemen meist Nanosekunden- oder Mikrosekunden-genau.
- Monoton steigend
- Kann nicht durch Systemzeitänderungen beeinflusst werden.
- Die Werte steigen immer an, niemals rückwärts.
- Optimiert für Laufzeitmessungen
- Ideal für Benchmarking und Laufzeitanalysen.
import time
# Ungenau:
start = time.time() # Nur geeignet um aktuelle Zeit zu erhalten
time.sleep(1)
end = time.time()
print(end - start)
# Genau:
start = time.perf_counter()
time.sleep(1)
end = time.perf_counter()
print(end - start)
2 Unit Tests in Python
2.1 Grundlagen
- Python verwendet das
unittest-Modul (im Standardpaket enthalten). - Alternativen:
pytest,nose2(externe Pakete).
2.2 Einfaches Beispiel mit unittest
import unittest
# Beispiel-Funktion: Addiert zwei Zahlen
def add_numbers(a, b):
return a + b
# Testklasse
class TestAddNumbers(unittest.TestCase):
def test_positive_numbers(self):
self.assertEqual(add_numbers(2, 3), 5)
def test_negative_numbers(self):
self.assertEqual(add_numbers(-1, -1), -2)
def test_zero(self):
self.assertEqual(add_numbers(0, 0), 0)
# Hauptprogramm für Unit-Tests
if __name__ == '__main__':
unittest.main()
2.3 Wichtige Methoden von unittest.TestCase
assertEqual(a, b)assertNotEqual(a, b)assertTrue(x)assertFalse(x)assertIsNone(x)assertRaises(Exception, func, args...)
3 Profiling in Python
3.1 Ziel
Performance-Engpässe (Bottlenecks) im Code finden.
3.2 Tools im Überblick
cProfile(Standardmodul, robust)timeit(für kleinere Snippets)line_profiler(extern, detailliert)py-spy,snakeviz,yappi(externe Visualisierung)
3.3 Beispiel: cProfile
import cProfile
# Rechenintensive Beispiel-Funktion
def compute_heavy_task():
total = 0
for i in range(100000):
total += i ** 2
return total
# Führe Profiling durch
cProfile.run('compute_heavy_task()')
3.4 Beispiel: timeit
import timeit
# Zeitmessung für einfachen Ausdruck
code = "sum([i*i for i in range(1000)])"
duration = timeit.timeit(stmt=code, number=1000)
print(f"Durchschnittszeit: {duration:.4f} Sekunden")
3.5 line_profiler (externe Installation nötig)
pip install line_profiler
# sample.py
# Mit @profile markierte Funktion
@profile
def compute():
total = 0
for i in range(10000):
total += i ** 2
return total
# Ausführen mit line_profiler
kernprof -l sample.py
python -m line_profiler sample.py.lprof
4 Tipps
4.1 Zuerst Tests schreiben, dann den Code (Test-Driven Development, TDD)
Was es bedeutet:
- TDD ist ein Entwicklungsansatz, bei dem man zuerst Tests schreibt, bevor man den eigentlichen Code implementiert.
- Das Ziel ist, nur so viel Code zu schreiben, wie nötig ist, um den Test bestehen zu lassen.
Ablauf:
- Einen Test schreiben, der fehlschlägt (weil es die Funktion noch nicht gibt).
- Den minimalen Code implementieren, der den Test bestehen lässt.
- Refaktorisiere, falls nötig, und sicherstellen, dass der Test weiterhin grün ist.
- Den Zyklus wiederholen für jede neue Funktionalität.
Vorteile:
- Klar definierte Anforderungen.
- Höhere Code-Qualität.
- Tests sind immer aktuell.
- Bessere Wartbarkeit.
4.2 pytest für eleganteres Testing (nutzt assert statt Methoden)
Was pytest auszeichnet:
-
Sehr beliebt wegen seiner einfachen Syntax.
-
Statt wie in
unittestMethoden wieassertEqual(a, b)zu verwenden, nutzt man einfach:assert a == b
Beispiel mit pytest:
# test_math.py
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
Warum es eleganter ist:
- Weniger Boilerplate-Code.
- Fehlermeldungen bei Fehlschlägen sind ausdrucksstärker (zeigt automatisch Werte an).
- Automatische Erkennung von Tests.
- Integration mit Plugins wie
pytest-cov,hypothesisuvm.
4.3 Profiling immer bei realistischen Datenmengen
Warum das wichtig ist:
- Kleine Testdaten täuschen oft über die Performance hinweg.
- Code, der bei 100 Einträgen schnell ist, kann bei 1 Million Einträgen ineffizient skalieren.
- Realitätsnahe Tests helfen, echte Engpässe zu erkennen.
Praxis-Tipp:
Wenn man z. B. ein Programm für Logfile-Analyse entwickelt:
- Profiling mit 10 Zeilen bringt kaum Erkenntnisse.
- Simuliere echte Logdateien (z. B. 100 MB oder mehr).
Tools wie cProfile, timeit, line_profiler sollten mit realistischen Inputs verwendet werden, um aussagekräftige Ergebnisse zu liefern.
4.4 Tests und Profiling kombinieren für Performance-Regressionstests
Was das bedeutet:
- Nicht nur funktionale Richtigkeit testen, sondern auch sicherstellen, dass neue Änderungen den Code nicht verlangsamen.
Wie man es macht:
- Einen Test schreiben, der nicht nur prüft, ob das Ergebnis korrekt ist, sondern auch, ob die Ausführung innerhalb einer Zeitgrenze bleibt.
Beispiel mit pytest:
import time
def test_performance():
start = time.time()
result = sum(i*i for i in range(100000))
duration = time.time() - start
assert duration < 0.5 # z. B. Maximal 0.5 Sekunden erlaubt
Vorteile:
- Du erkennst Leistungsregressionen sofort, z. B. wenn jemand unabsichtlich einen ineffizienten Algorithmus einbaut.
- Ideal für kritische Funktionen in Web-Backends, Datenverarbeitung, Simulationen etc.
5 Arbeiten in VS Code
5.1 Tests ausführen mit unittest oder pytest in VS Code
- Sicherstellen, dass der Python-Interpreter ausgewählt wurde (unten links oder
Ctrl+Shift+P→ “Python: Interpreter auswählen”). - Kommando-Palette öffnen:
Ctrl+Shift+P - Auswählen:
Python: Discover Tests unittest,pytestodernoseals Framework auswählen.- Danach:
Python: Run All TestsoderRun Testdirekt über dem Test mit dem kleinen „Play“-Icon.
[!INFO] VS Code erkennt
pytestautomatisch, wenn die Datei mittest_*.pybeginnt undassert-Statements enthält.
5.2 Profiling mit Erweiterung: “Python Profile”
- Optional: Erweiterung “Python Profiler” installieren.
- Datei öffnen
- Einen Breakpoint setzen oder über
Run → Start Debuggingausführen. - Man kann
cProfile-Ausgaben direkt im Terminal oder über Plugins visualisieren lassen.
5.3 Kurzbefehle
| Aktion | Shortcut |
|---|---|
| Testdatei ausführen | Ctrl+F5 oder ▶ oben |
| Terminal öffnen | `Ctrl+`` |
| Befehlspalette öffnen | Ctrl+Shift+P |
| Tests entdecken | Python: Discover Tests |
| Nur einen Test ausführen | Rechtsklick > Run Test |
Leistungsoptimierung
Python ist zwar sehr flexibel und einfach zu schreiben, jedoch nicht immer die schnellste Sprache. Für leistungskritische Anwendungen gibt es mehrere Möglichkeiten, Python-Code zu beschleunigen, oft durch die Integration von C/C++ oder durch JIT-Compiler.
1 Cython
Cython ist eine Erweiterung von Python, mit der man C-ähnlichen Code schreiben kann, der zu sehr schnellem C-Code kompiliert wird. Es erlaubt auch das Einbinden von C-Bibliotheken.
Die Verwendung lohnt sich nur, wenn man sehr viele mathematische Operationen durchführt.
1.1 Beispiel
# example.pyx
def compute_sum(int n):
cdef int i
cdef int total = 0
for i in range(n):
total += i
return total
Um diesen Code zu verwenden, muss er mit Cython kompiliert werden, z. B. über setup.py oder Jupyter mit %%cython.
2 CPython
CPython ist die Standard-Implementierung von Python, geschrieben in C. Bei der Optimierung auf CPython-Ebene kann man z. B. direkt in C Erweiterungsmodule schreiben.
2.1 Vorteile
- Maximale Performance durch nativen C-Code
- Direkter Zugriff auf Python-Interna
- Verwendung für systemnahe Programmierung
2.2 Nachteile
- Komplexität des C-Codes
- Manuelle Speicherverwaltung
3 Pybind11 (C++)
Pybind11 ist eine moderne Header-only-C++-Bibliothek zur Anbindung von C++-Code an Python. Sie erlaubt es, bestehende C++-Bibliotheken einfach in Python zu verwenden.
3.1 Beispiel
#include <pybind11/pybind11.h>
int add(int a, int b) {
return a + b;
}
PYBIND11_MODULE(my_module, m) {
m.def("add", &add);
}
Kompiliert wird dies zu einer .so-Datei, die in Python importiert werden kann:
import my_module
print(my_module.add(3, 4)) # Ausgabe: 7
was ist eine header-only-c++-bibliothek?
In C++ bestehen Bibliotheken typischerweise aus:
- Header-Dateien (
.hoder.hpp) – enthalten Deklarationen (z. B. Funktionen, Klassen). - Implementierungsdateien (
.cpp) – enthalten die eigentliche Logik.
Eine Header-only-Bibliothek verzichtet auf .cpp-Dateien.
Stattdessen ist die gesamte Implementierung direkt in den Header-Dateien enthalten. Das bedeutet:
- Man muss die Bibliothek nicht kompilieren – man bindet einfach nur die Header-Datei ein.
- Sie kann per
#includebenutzt werden. - Beispiel:
#include <pybind11/pybind11.h>
Vorteile:
- Einfacher zu integrieren, kein separater Build-Schritt nötig.
- Portabler, da es keine Abhängigkeit zu vorcompilierten Binaries gibt.
- Gut geeignet für Templates und generischen Code.
Nachteil:
- Der Compiler sieht bei jedem
#includedie komplette Implementierung → längere Compile-Zeiten.
4 Numba
Numba ist ein JIT-Compiler (Just-In-Time), der Funktionen zur Laufzeit in optimierten Maschinen-Code übersetzt, basierend auf LLVM.
4.1 Beispiel
from numba import jit
@jit(nopython=True)
def fast_sum(n):
total = 0
for i in range(n):
total += i
return total
print(fast_sum(1000000))
Numba funktioniert besonders gut bei numerischen Berechnungen und Schleifen.
was ist llvm?
LLVM steht für “Low-Level Virtual Machine”, ist aber heute viel mehr als das: LLVM ist eine moderne Compiler-Infrastruktur, die aus mehreren modularen Tools besteht. Viele moderne Programmiersprachen verwenden LLVM, um ihren Code in Maschinencode zu übersetzen.
Wichtigste Eigenschaften:
- Zwischensprache (IR): LLVM übersetzt Code zuerst in eine eigene “Intermediate Representation” (IR), die dann weiter optimiert wird.
- Optimierungen: Bietet sehr fortschrittliche Optimierungen auf niedriger Ebene.
- Backend: Erzeugt optimierten Maschinencode für viele Plattformen.
Beispiele für Tools/Projekte, die LLVM nutzen:
- Clang (C/C++-Compiler)
- Rust
- Swift
- Numba (s. o.)
- Julia
Warum wichtig für Python-Optimierung? Numba nutzt LLVM, um Python-Funktionen zur Laufzeit (JIT) in schnellen Maschinen-Code umzuwandeln.
5 Mypyc
Mypyc kompiliert typannotierten Python-Code zu C-Extensions und kann so erhebliche Geschwindigkeitsvorteile bringen. Es arbeitet zusammen mit mypi, dem statischen Typprüfer.
5.1 Beispiel
# example.py
def double(x: int) -> int:
return x * 2
Kompilieren mit mypyc:
mypyc example.py
Die resultierende .so-Datei kann dann wie ein normales Python-Modul importiert werden.
5.2 Vorteile
- Nahtlose Integration mit typisiertem Python-Code
- Einfache Anwendung über bestehende Typannotationen
6 C-Extensions & FFI (Foreign Function Interface)
Python kann mit C/C++-Code interagieren für maximale Performance oder Zugriff auf System-Bibliotheken. Es gibt mehrere Ansätze mit unterschiedlicher Komplexität.
6.1 ctypes – Einfacher FFI-Zugriff
ctypes ist eine Built-in Library zum Aufrufen von C-Funktionen aus Shared Libraries.
6.1.1 Grundlagen
import ctypes
# C-Bibliothek laden
# Linux
libc = ctypes.CDLL("libc.so.6")
# macOS
libc = ctypes.CDLL("libc.dylib")
# Windows
libc = ctypes.CDLL("msvcrt.dll")
# C-Funktion aufrufen
libc.printf(b"Hello from C! %d\n", 42)
6.1.2 Eigene C-Library einbinden
C-Code (mylib.c):
// mylib.c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
void greet(const char* name) {
printf("Hello, %s!\n", name);
}
Kompilieren:
# Shared Library erstellen
# Linux
gcc -shared -o libmylib.so -fPIC mylib.c
# macOS
gcc -shared -o libmylib.dylib -fPIC mylib.c
# Windows
gcc -shared -o mylib.dll mylib.c
Python-Code:
import ctypes
# Library laden
lib = ctypes.CDLL("./libmylib.so")
# add-Funktion aufrufen
result = lib.add(5, 3)
print(f"5 + 3 = {result}") # 8
# greet-Funktion mit String
lib.greet(b"Alice") # Hello, Alice!
6.1.3 Typen und Argumente
import ctypes
lib = ctypes.CDLL("./libmylib.so")
# Rückgabetyp deklarieren
lib.add.restype = ctypes.c_int
lib.add.argtypes = [ctypes.c_int, ctypes.c_int]
result = lib.add(10, 20)
print(result)
# String-Argumente
lib.greet.argtypes = [ctypes.c_char_p]
lib.greet(b"Bob")
6.1.4 Komplexe Datentypen
import ctypes
# Struct definieren
class Point(ctypes.Structure):
_fields_ = [
("x", ctypes.c_int),
("y", ctypes.c_int)
]
# C-Funktion: void print_point(Point* p)
lib.print_point.argtypes = [ctypes.POINTER(Point)]
p = Point(10, 20)
lib.print_point(ctypes.byref(p))
6.1.5 Arrays und Pointer
import ctypes
# Array erstellen
IntArray = ctypes.c_int * 5
arr = IntArray(1, 2, 3, 4, 5)
# Als Pointer übergeben
lib.sum_array.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
lib.sum_array.restype = ctypes.c_int
result = lib.sum_array(arr, 5)
print(f"Sum: {result}")
6.1.6 Callbacks (Python → C → Python)
import ctypes
# Python-Funktion als C-Callback
@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)
def callback(x):
print(f"Callback called with {x}")
return x * 2
# C-Funktion: int process(int (*func)(int), int value)
lib.process.argtypes = [ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int), ctypes.c_int]
lib.process.restype = ctypes.c_int
result = lib.process(callback, 5)
print(f"Result: {result}")
6.2 cffi – Modern FFI Interface
cffi (C Foreign Function Interface) ist moderner und sicherer als ctypes.
6.2.1 Installation und Grundlagen
pip install cffi
from cffi import FFI
ffi = FFI()
# C-Deklarationen
ffi.cdef("""
int add(int a, int b);
void greet(const char* name);
""")
# Library laden
lib = ffi.dlopen("./libmylib.so")
# Funktionen aufrufen
result = lib.add(5, 3)
print(result) # 8
lib.greet(b"Alice")
6.2.2 Out-of-Line Mode (Kompiliert)
# build_mylib.py
from cffi import FFI
ffibuilder = FFI()
ffibuilder.cdef("""
int add(int a, int b);
""")
ffibuilder.set_source("_mylib",
"""
int add(int a, int b) {
return a + b;
}
""")
if __name__ == "__main__":
ffibuilder.compile(verbose=True)
# Kompilieren
python build_mylib.py
# Verwendung
from _mylib import lib
print(lib.add(10, 20))
6.2.3 Structs mit cffi
from cffi import FFI
ffi = FFI()
ffi.cdef("""
typedef struct {
int x;
int y;
} Point;
void print_point(Point* p);
""")
lib = ffi.dlopen("./libmylib.so")
# Struct erstellen
p = ffi.new("Point *")
p.x = 10
p.y = 20
lib.print_point(p)
6.2.4 Callbacks mit cffi
from cffi import FFI
ffi = FFI()
ffi.cdef("""
typedef int (*callback_t)(int);
int process(callback_t func, int value);
""")
lib = ffi.dlopen("./libmylib.so")
# Python-Callback
@ffi.def_extern()
def my_callback(x):
return x * 2
callback = ffi.callback("int(int)", my_callback)
result = lib.process(callback, 5)
print(result)
6.3 Python C-API – Native Extensions
Direkte C-Extensions mit Python C-API für maximale Kontrolle.
6.3.1 Einfaches Modul
// mymodule.c
#include <Python.h>
static PyObject* add(PyObject* self, PyObject* args) {
int a, b;
if (!PyArg_ParseTuple(args, "ii", &a, &b))
return NULL;
return PyLong_FromLong(a + b);
}
static PyMethodDef ModuleMethods[] = {
{"add", add, METH_VARARGS, "Add two integers"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef mymodule = {
PyModuleDef_HEAD_INIT,
"mymodule",
"Example module",
-1,
ModuleMethods
};
PyMODINIT_FUNC PyInit_mymodule(void) {
return PyModule_Create(&mymodule);
}
setup.py:
from setuptools import setup, Extension
module = Extension('mymodule', sources=['mymodule.c'])
setup(
name='mymodule',
version='1.0',
ext_modules=[module]
)
# Kompilieren
python setup.py build_ext --inplace
# Verwenden
import mymodule
print(mymodule.add(5, 3))
6.4 Vergleich: ctypes vs. cffi vs. C-API
| Aspekt | ctypes | cffi | Python C-API |
|---|---|---|---|
| Komplexität | ✅ Einfach | ✅ Mittel | ❌ Komplex |
| Performance | ⚠️ Overhead | ✅ Schnell | ✅✅ Sehr schnell |
| Portabilität | ✅ Built-in | ⚠️ pip install | ✅ Standard |
| Typsicherheit | ❌ Runtime | ✅ Compile-Zeit | ✅ Compile-Zeit |
| Wartbarkeit | ✅ Gut | ✅ Gut | ⚠️ Aufwendig |
| PyPy | ⚠️ Langsam | ✅ Optimiert | ❌ Nicht verfügbar |
6.5 Praktische Beispiele
6.5.1 System-Calls mit ctypes
import ctypes
import platform
if platform.system() == 'Linux':
libc = ctypes.CDLL('libc.so.6')
# getpid() System-Call
pid = libc.getpid()
print(f"Process ID: {pid}")
# gethostname()
buf = ctypes.create_string_buffer(256)
libc.gethostname(buf, 256)
print(f"Hostname: {buf.value.decode()}")
6.5.2 Performance-kritischer Code mit cffi
# build_fast.py
from cffi import FFI
ffibuilder = FFI()
ffibuilder.cdef("""
void fast_sum(double* data, int size, double* result);
""")
ffibuilder.set_source("_fast",
"""
void fast_sum(double* data, int size, double* result) {
double sum = 0.0;
for (int i = 0; i < size; i++) {
sum += data[i];
}
*result = sum;
}
""")
ffibuilder.compile(verbose=True)
# usage.py
from _fast import ffi, lib
import time
# Große Daten
data = list(range(10_000_000))
# Python-Version
start = time.time()
py_sum = sum(data)
print(f"Python: {time.time() - start:.3f}s")
# C-Version
start = time.time()
c_data = ffi.new("double[]", data)
c_result = ffi.new("double*")
lib.fast_sum(c_data, len(data), c_result)
print(f"C: {time.time() - start:.3f}s")
6.5.3 GPU-Zugriff mit ctypes (CUDA)
import ctypes
import numpy as np
# CUDA Runtime laden
cuda = ctypes.CDLL('libcudart.so')
# Device Properties
prop = ctypes.c_int()
cuda.cudaGetDeviceCount(ctypes.byref(prop))
print(f"CUDA Devices: {prop.value}")
6.6 Best Practices
✅ Verwende ctypes wenn:
- Einfacher FFI-Zugriff nötig
- Prototyping
- Standard-Libraries (libc, etc.)
- Keine Kompilierung gewünscht
✅ Verwende cffi wenn:
- Performance wichtig
- PyPy-Kompatibilität
- Komplexere C-Interaktion
- Typsicherheit zur Compile-Zeit
✅ Verwende C-API wenn:
- Maximale Performance
- Volle Python-Kontrolle nötig
- Existierende C/C++-Codebasis
- NumPy-ähnliche Extensions
✅ Verwende Pybind11 wenn:
- C++ Code
- Modern C++ Features
- Header-only bevorzugt
6.7 Debugging C-Extensions
# Mit gdb debuggen
import sys
import ctypes
# Core Dumps aktivieren
import resource
resource.setrlimit(resource.RLIMIT_CORE,
(resource.RLIM_INFINITY, resource.RLIM_INFINITY))
# Valgrind für Memory-Leaks
# valgrind --leak-check=full python script.py
# Logging in C-Code
lib = ctypes.CDLL("./libmylib.so")
lib.set_debug(True)
7 Rust-Integration mit PyO3
PyO3 ermöglicht nahtlose Interoperabilität zwischen Rust und Python – Performance von Rust mit Einfachheit von Python.
Was ist PyO3?
Offizielle Dokumentation von PyO3
PyO3 ist eine Rust-Bibliothek (Crate), die es ermöglicht, Rust-Code mit Python zu verbinden. Mit PyO3 kann man:
- Rust-Module für Python schreiben (Rust-Code in Python importieren)
- Python-Code in Rust aufrufen (z. B. bestehende Python-Bibliotheken nutzen)
- Python-Objekte mit Rust interagieren lassen
PyO3 nutzt Rusts Foreign Function Interface (FFI), um eine nahtlose Interoperabilität mit Python zu ermöglichen.
Warum Rust + Python?
- ✅ Rust-Performance (~C++ Geschwindigkeit)
- ✅ Memory Safety (keine Segfaults)
- ✅ Moderne Sprache (Cargo, crates.io)
- ✅ Zunehmend populär (ruff, polars, pydantic-core)
PyO3 vs. Alternativen:
| Tool | Sprache | Performance | Memory Safety | Komplexität | |
|---|---|---|---|---|---|
| Pybind11 | C++ | ✅ Sehr hoch | ⚠️ Manuell | Mittel | |
| PyO3 | Rust | ✅ Sehr hoch | ✅ Garantiert | Mittel | |
| ctypes | C | ✅ Hoch | ❌ Unsicher | Niedrig | |
| Cython | Python | ✅ Hoch | ⚠️ Manuell | Niedrig |
7.1 Anwendungsfälle von PyO3
- Beschleunigung von Python-Code durch performanten Rust-Code
- Erstellung von Python-Erweiterungsmodulen
- Rust-Programme mit einer Python-API ausstatten
- Einbinden von Python-Bibliotheken in Rust
Installation
7.2 Beispiel für ein Rust-Modul für Python
7.2.1 Python-Umgebung auswählen und maturin installieren
conda activate py312
Falls maturin noch nicht installiert ist:
pip install maturin
Projekt-Struktur
string_sum/
├── Cargo.toml # Rust-Konfiguration
├── pyproject.toml # Python-Konfiguration (optional)
└── src/
└── lib.rs # Rust-Code
7.2.2 Leeren Ordner für die Rust-Bibliothek erstellen
mkdir string_sum
… und in den Ordner wechseln:
cd string_sum
7.2.3 maturin ausführen um das Projekt zu initialisieren
maturin init
pyo3 aus der Liste auswählen
7.2.4 cargo/config.tomlanpassen
[build]
target-dir = "/Users/cgroening/Downloads/cargo_target/notes"
7.2.5 cargo.toml anpassen
[package]
name = "string_sum"
version = "0.1.0"
edition = "2021"
[lib]
# The name of the native library. This is the name which will
# be used in Python to import the library (i.e. `import string_sum`).
# If you change this, you must also change the name of the
# `#[pymodule]` in `src/lib.rs`.
name = "string_sum"
# "cdylib" is necessary to produce a shared library for Python to import from.
# Downstream Rust code (including code in `bin/`, `examples/`, and `tests/`) will not be able to `use string_sum;` unless the "rlib" or
# "lib" crate type is also included, e.g.:
# crate-type = ["cdylib", "rlib"]
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.24.0", features = ["extension-module"] }
7.2.6 Code der Datei lib.rs
#![allow(unused)]
fn main() {
use pyo3::prelude::*;
/// Formats the sum of two numbers as string
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}
/// A Python module implemented in Rust. The name of this function must match the `lib.name` setting in the `Cargo.toml`, else Python will not be able to import the module.
#[pymodule]
fn string_sum(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
Ok(())
}
}
leistungstest mit vielen iterationen
Den Leistungsunterschied zwischen Rust und Python kann man vereinfacht testen, indem man eine Aktion durch eine Schleife viele Millionen Mal wiederholt. Rusts Compiler (rustc) ist jedoch extrem aggressiv in der Optimierung. Falls das Ergebnis nicht wirklich genutzt wird, erkennt der Compiler, dass die Berechnung nutzlos ist und eliminiert die gesamte Schleife (Dead Code Elimination).
Um diese Optimierung zu umgehen, kann man die Funktion black_box() aus std::hint nutzen, um sicherzustellen, dass der Wert wirklich berechnet wird.
Beispiel für black_box():
#![allow(unused)]
fn main() {
// ...
#use std::hint::black_box;
// ...
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
let mut sum = 0;
for _ in 0..100_000_000 {
sum += a + b;
}
black_box(sum); // Optimierung verhindern
Ok(format!("{}", sum))
}
// ...
}
7.2.7 Kompilieren der Bibliothek
maturin develop
Wenn Änderungen am Rust-Code gemacht werden, muss anschließend maturin develop erneut ausgeführt werden, damit sie wirksam sind.
Das Modul wird direkt in die ausgewählte Python-Umgebung installiert, sodass es wie folgt genutzt werden kann:
import string_sum
string_sum.sum_as_string(1, 2)
Vergleich der Leistung
Performance-Vergleich:
# Python-Version
def sum_as_string_py(a, b):
total = 0
for _ in range(100_000_000):
total += a + b
return str(total)
# Rust-Version via PyO3 (siehe oben)
Benchmark:
import time
import string_sum
# Rust
start = time.time()
result = string_sum.sum_as_string(5, 3)
print(f"Rust: {time.time() - start:.2f}s")
# Python
start = time.time()
result = sum_as_string_py(5, 3)
print(f"Python: {time.time() - start:.2f}s")
# Typisches Ergebnis:
# Rust: 0.15s
# Python: 4.8s
# → ~32x schneller!
8.7 Fortgeschrittene Features
8.7.1 Python-Klassen in Rust
#![allow(unused)]
fn main() {
use pyo3::prelude::*;
#[pyclass]
struct Counter {
count: i32,
}
#[pymethods]
impl Counter {
#[new]
fn new() -> Self {
Counter { count: 0 }
}
fn increment(&mut self) {
self.count += 1;
}
fn get(&self) -> i32 {
self.count
}
}
#[pymodule]
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<Counter>()?;
Ok(())
}
}
Python-Verwendung:
from my_module import Counter
c = Counter()
c.increment()
print(c.get()) # 1
8.7.2 NumPy-Integration
# Cargo.toml
[dependencies]
pyo3 = { version = "0.24.0", features = ["extension-module"] }
numpy = "0.24.0"
#![allow(unused)]
fn main() {
use numpy::PyArray1;
use pyo3::prelude::*;
#[pyfunction]
fn sum_array(arr: &PyArray1<f64>) -> PyResult<f64> {
let slice = unsafe { arr.as_slice()? };
Ok(slice.iter().sum())
}
}
8.7.3 Python aus Rust aufrufen
#![allow(unused)]
fn main() {
use pyo3::prelude::*;
fn call_python_function() -> PyResult<()> {
Python::with_gil(|py| {
// Python-Code ausführen
let code = "print('Hello from Python!')";
py.run(code, None, None)?;
// Python-Modul importieren
let json = py.import("json")?;
let dumps = json.getattr("dumps")?;
let result = dumps.call1(("{\"key\": \"value\"}",))?;
println!("JSON: {}", result);
Ok(())
})
}
}
8.8 Fehlerbehandlung
#![allow(unused)]
fn main() {
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
#[pyfunction]
fn divide(a: f64, b: f64) -> PyResult<f64> {
if b == 0.0 {
Err(PyValueError::new_err("Division by zero"))
} else {
Ok(a / b)
}
}
}
In Python:
try:
result = my_module.divide(10, 0)
except ValueError as e:
print(f"Error: {e}")
8.9 Distribution
8.9.1 Wheel bauen
# Für aktuelle Platform
maturin build --release
# Cross-compilation für mehrere Platforms
maturin build --release --target x86_64-unknown-linux-gnu
maturin build --release --target aarch64-apple-darwin
# Universal wheel (wenn möglich)
maturin build --release --universal2
8.9.2 PyPI veröffentlichen
# Testweise
maturin publish --repository testpypi
# Production
maturin publish
8.9.3 pyproject.toml
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
name = "string_sum"
version = "0.1.0"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
]
8.10 Bekannte PyO3-Projekte
Production Use Cases:
- ruff: Python Linter (1000x schneller als pylint)
- polars: DataFrame-Bibliothek (schneller als pandas)
- pydantic-core: Validierung für pydantic v2
- cryptography: Kryptografie-Primitiven
- tantivy-py: Full-text Search Engine
- tokenizers (Hugging Face): NLP Tokenization
8.11 Vergleich: PyO3 vs. Pybind11
| Aspekt | PyO3 (Rust) | Pybind11 (C++) |
|---|---|---|
| Sprache | Rust | C++ |
| Memory Safety | ✅ Garantiert | ⚠️ Manuell |
| Performance | ✅✅ Sehr hoch | ✅✅ Sehr hoch |
| Build-Tool | maturin, Cargo | CMake, setuptools |
| Learning Curve | ⚠️ Rust-Kenntnisse | ⚠️ C++-Kenntnisse |
| Ökosystem | crates.io | vcpkg, conan |
| Async Support | ✅ Tokio | ⚠️ Komplex |
8.12 Best Practices
✅ DO:
- Nutze
maturin develop --releasefür Benchmarks - Verwende
black_box()für realistische Performance-Tests - Nutze Rust’s Ownership für Memory-Safety
- Profile mit
cargo flamegraph - Verwende NumPy-Integration für Array-Operationen
- Teste auf mehreren Platforms (CI/CD)
❌ DON’T:
- Python GIL ignorieren (bei Multi-Threading)
- Zu kleine Funktionen (FFI-Overhead)
- Komplexe Python-Objekte ständig konvertieren
- Rust-Panics ungefangen lassen (werden zu Python-Exceptions)
8.13 Troubleshooting
Problem: “ImportError: DLL load failed”
# Windows: Visual C++ Redistributables fehlen
# Lösung: Installiere VC++ Runtime
# Linux: fehlende Shared Libraries
ldd target/release/libmy_module.so
Problem: GIL-Deadlocks
#![allow(unused)]
fn main() {
// ❌ Kann deadlocken
fn bad_example(py: Python) -> PyResult<()> {
let data = some_rust_work();
// Noch im GIL-Lock!
do_more_work(data);
Ok(())
}
// ✅ GIL freigeben wenn möglich
fn good_example(py: Python) -> PyResult<()> {
py.allow_threads(|| {
some_rust_work() // Ohne GIL!
});
Ok(())
}
}
Zusammenfassung
Kernprinzip: PyO3 kombiniert Rust’s Performance und Safety mit Python’s Einfachheit. Ideal für moderne, performance-kritische Python-Extensions.
Wann PyO3 verwenden:
- ✅ CPU-intensive Berechnungen
- ✅ Memory-Safety wichtig
- ✅ Moderne Codebase
- ✅ Rust-Team vorhanden
- ✅ Async/Concurrent Workloads
Wann Alternativen:
- Pybind11: Existierende C++-Codebase
- Cython: Python-Entwickler, graduelle Optimierung
- NumPy/Numba: Wissenschaftliches Computing
- ctypes: Einfache C-Library-Anbindung
Development Workflow:
# 1. Code schreiben (src/lib.rs)
# 2. Build & Install (Development)
maturin develop
# 3. In Python testen
python -c "import string_sum; print(string_sum.sum_as_string(1, 2))"
# 4. Release Build (optimiert)
maturin develop --release
# 5. Wheel für Distribution
maturin build --release
7.3 Vergleich
| Tool | Use Case | Complexity | Performance |
|---|---|---|---|
| ctypes | Quick FFI, System Libs | Low | Medium |
| cffi | Modern FFI, PyPy-compatible | Medium | High |
| Python C-API | Full control, NumPy-like | High | Very High |
| Pybind11 | C++ Integration | Medium | Very High |
| Cython | Python-like, gradual optimization | Low-Medium | High |
Kernprinzip: Wähle ctypes für einfache FFI-Calls, cffi für moderne Performance-kritische Anwendungen, und C-API/Pybind11 für maximale Kontrolle. Cython bleibt die einfachste Option für Python-Entwickler, die Performance brauchen.
8 Vergleich: Python Performance & Extension Tools
| Tool | Sprache | Anwendungsfall | Komplexität | Performance | Speichersicherheit | Build-Zeit | Lernkurve | PyPy-Support |
|---|---|---|---|---|---|---|---|---|
| ctypes | C | Schneller FFI, System-Libs | Niedrig | Mittel | ❌ Unsicher | Keine | ✅ Einfach | ⚠️ Langsam |
| cffi | C | Modern FFI, PyPy-kompatibel | Mittel | Hoch | ⚠️ Manuell | Schnell | ✅ Einfach | ✅ Schnell |
| Python C-API | C | Volle Kontrolle, NumPy-ähnlich | Hoch | Sehr hoch | ❌ Unsicher | Mittel | ❌ Schwer | ❌ Nein |
| Pybind11 | C++ | C++-Integration, modern | Mittel | Sehr hoch | ⚠️ Manuell | Langsam | ⚠️ Mittel | ❌ Nein |
| PyO3 | Rust | Modern, sicher, performant | Mittel | Sehr hoch | ✅ Sicher | Mittel | ⚠️ Mittel | ❌ Nein |
| Cython | Python+C | Python-ähnlich, graduelle Optimierung | Niedrig-Mittel | Hoch | ⚠️ Manuell | Mittel | ✅ Einfach | ❌ Nein |
| Numba | Python | JIT, NumPy-fokussiert | Niedrig | Sehr hoch | ✅ Sicher | Keine (JIT) | ✅ Einfach | ❌ Nein |
| Mypyc | Python | Typisiertes Python → C | Niedrig | Hoch | ✅ Sicher | Mittel | ✅ Einfach | ❌ Nein |
| PyPy | Python | JIT für reines Python | Keine | Sehr hoch | ✅ Sicher | Keine (JIT) | ✅ Keine | ✅ Nativ |
| Nuitka | Python | Python → C Compiler | Niedrig | Hoch | ✅ Sicher | Langsam | ✅ Einfach | ❌ Nein |
8.1 Legende
Komplexität:
- Niedrig: Einfach zu verwenden, wenig Boilerplate
- Mittel: Erfordert Setup und Verständnis
- Hoch: Tiefes Verständnis von Internals nötig
Performance:
- Mittel: 2-5x schneller als CPython
- Hoch: 5-20x schneller
- Sehr hoch: 20-100x+ schneller (bei geeigneten Workloads)
Speichersicherheit:
- ✅ Sicher: Compiler garantiert Speichersicherheit
- ⚠️ Manuell: Entwickler verantwortlich
- ❌ Unsicher: Leicht Fehler zu machen
Build-Zeit:
- Keine: Keine Kompilierung (JIT oder reines Python)
- Schnell: < 1 Sekunde
- Mittel: 1-10 Sekunden
- Langsam: 10+ Sekunden
Lernkurve:
- ✅ Einfach: Python-Kenntnisse ausreichend
- ⚠️ Mittel: Neue Sprache/Konzepte lernen
- ❌ Schwer: Tiefe C/C++-Kenntnisse + Python C-API
PyPy-Support:
- ✅ Schnell/Nativ: Optimiert für PyPy
- ⚠️ Langsam: Funktioniert, aber langsamer
- ❌ Nein: Nicht kompatibel
8.2 Erweiterte Vergleichskriterien
| Tool | Geeignet für | Vermeiden bei | Ökosystem | Async-Support |
|---|---|---|---|---|
| ctypes | System-Libs, Prototyping | Komplexe Interaktionen | Standard-Lib | N/A |
| cffi | Moderne C-Libs, PyPy | Einfache Aufgaben (übertrieben) | PyPI | N/A |
| Python C-API | NumPy-ähnliche Extensions | Einfache Optimierungen | Nur CPython | Komplex |
| Pybind11 | Moderner C++-Code | Reiner C-Code | Header-only | Komplex |
| PyO3 | Moderne Rust-Integration | Einfache Aufgaben | crates.io | ✅ Tokio |
| Cython | Graduelle Optimierung | Reine Python-Alternativen | PyPI | Eingeschränkt |
| Numba | NumPy-Arrays, Schleifen | String-Ops, komplexe Objekte | Conda/PyPI | Eingeschränkt |
| Mypyc | Typisiertes Python beschleunigen | Dynamisches Python | MyPy-Ökosystem | Eingeschränkt |
| PyPy | Lang laufendes reines Python | Kurze Scripts, C-Extensions | PyPI (begrenzt) | ✅ Nativ |
| Nuitka | Ganzes Programm optimieren | Entwicklung (langsame Kompilierung) | Standalone | Natives Python |
8.3 Entscheidungshilfe
Szenario: Numerische Berechnungen mit Arrays → Numba (einfachste Option) oder Cython (volle Kontrolle)
Szenario: Vorhandene C-Bibliothek einbinden → cffi (modern) oder ctypes (schnell & einfach)
Szenario: Vorhandene C++-Codebasis → Pybind11
Szenario: Moderne, sichere Extension → PyO3 (Rust) oder Pybind11 (C++)
Szenario: Python-Code beschleunigen ohne neue Sprache → PyPy (reines Python) oder Numba (NumPy-fokussiert)
Szenario: Typisiertes Python zu nativem Code → Mypyc
Szenario: Python + C Hybrid → Cython
Szenario: Maximale Performance, volle Kontrolle → Python C-API oder PyO3 (mit Speichersicherheit)
Szenario: Distribution als Binary → Nuitka (kompiliert) oder PyInstaller (bündelt)
Multithreading und Multiprocessing
1 Begriffsdefinitionen
i/o-bound performance
- Eine Aufgabe ist I/O-bound, wenn sie viel Zeit mit Input/Output verbringt, z. B.:
- Auf Netzwerkdaten warten
- Dateien lesen/schreiben
- Datenbankabfragen
- Die CPU ist dabei oft untätig → Warten auf externe Ressourcen.
Bei I/O-bound Tasks helfen Threads oder Asyncio besonders gut, weil sie während der Wartezeit andere Aufgaben erledigen können.
cpu-bound performance
- Eine Aufgabe ist CPU-bound, wenn sie hauptsächlich die Prozessorleistung beansprucht (z. B. große Berechnungen, Datenkompression).
- Je schneller der Prozessor, desto schneller die Aufgabe.
- Beispiel: Primzahlen berechnen, Matrixmultiplikation.
Bei CPU-bound Tasks ist der GIL ein Problem in Threads – daher ist Multiprocessing hier besser.
global interpreter lock (gil)
- GIL = Python-Sperre, die nur einen Thread zur Zeit Bytecode ausführen lässt (bei CPython).
- Begrenzt die Effizienz von Multithreading bei CPU-bound Tasks (z. B. große Berechnungen).
- Umgehbar durch Multiprocessing oder C-Extensions.
ipc (inter-process communication)
- Prozesse haben getrennte Speicherbereiche → sie teilen keine Variablen.
- Daher braucht man IPC, um Daten auszutauschen:
- z. B. Queue, Pipe, Shared Memory, Sockets.
Beispiel mit multiprocessing.Queue:
from multiprocessing import Process, Queue
def worker(q):
q.put("Hallo aus dem Prozess")
q = Queue()
p = Process(target=worker, args=(q,))
p.start()
print(q.get()) # Ausgabe: Hallo aus dem Prozess
p.join()
event loop
- Das Herzstück von Asyncio.
- Der Event Loop verwaltet alle Tasks, die gerade laufen oder darauf warten, weiterzumachen.
- Bei
awaitwird eine Task “geparkt”, und der Loop schaut, ob eine andere weiterlaufen kann.
Beispiel mit einem Loop:
import asyncio
async def task():
print("Task läuft")
await asyncio.sleep(1)
print("Task beendet")
loop = asyncio.get_event_loop()
loop.run_until_complete(task())
2 Multithreading (concurrently, gleichzeitig)
- Leichtgewichtig: Laufen im gleichen Prozessspeicherraum (alle Threads gehören zu einem Prozess).
- Schnell beim Starten (schneller, als einen neuen Prozess zu starten).
- Shared memory: Teilen sich Speicher (z. B. Variablen, Objekte).
- kann Vor- oder Nachteil sein; ist buganfälliger
- Ideal für I/O-bound Tasks (z. B. Netzwerk, Datei lesen).
- Werden durch den Global Interpreter Lock (GIL) begrenzt – nur ein Thread kann zur gleichen Zeit Python-Bytecode ausführen.
+--------------+
| Main Process |
+--------------+
|
+--------------+-------------+
| | |
v v v
+----------+ +----------+ +----------+
| Thread 1 | | Thread 2 | | Thread 3 |
+----------+ +----------+ +----------+
(teilen sich Speicher & Ressourcen)
Beispiel:
import threading
import time
def worker():
print("Thread startet")
time.sleep(2)
print("Thread endet")
t1 = threading.Thread(target=worker)
t2 = threading.Thread(target=worker)
t1.start()
t2.start()
t1.join()
t2.join()
3 Multiprocessing (parallel)
- Jeder Prozess hat eigenen Speicher, läuft unabhängig.
- Es neuen Prozess zu starten ist langsamer, als einen neuen Thread zu starten.
- Kein GIL-Problem → besser für CPU-bound Tasks.
- Kein gemeinsamer Speicher → Interprozess-Kommunikation (IPC) nötig.
- Kommunikation zwischen Prozessen ist aufwändiger (z. B. mit Queues, Pipes).
+-----------+ +-----------+ +-----------+
| Process 1 | | Process 2 | | Process 3 |
| | | | | |
| Eigener | | Eigener | | Eigener |
| Speicher | | Speicher | | Speicher |
+-----------+ +-----------+ +-----------+
| | |
+-------- IPC ------+-------- IPC ------+
(z. B. Queues, Pipes, Sockets)
Beispiel:
import multiprocessing
import time
def worker():
print("Prozess startet")
time.sleep(2)
print("Prozess endet")
p1 = multiprocessing.Process(target=worker)
p2 = multiprocessing.Process(target=worker)
p1.start()
p2.start()
p1.join()
p2.join()
4 Threads vs Processes
| Kriterium | Threads | Processes |
|---|---|---|
| Speicher | Gemeinsamer Speicher | Getrennter Speicher |
| Startzeit | Schnell | Etwas langsamer |
| CPU-bound Performance | Schlecht (wegen GIL) | Gut |
| I/O-bound Performance | Gut | Gut |
| Kommunikation | Einfach (gemeinsamer Speicher) | Komplexer (IPC nötig) |
| Nutzung des GIL | Ja | Nein |
5 Asyncio (Asynchrone Programmierung)
- Für gleichzeitige Tasks, ohne neue Threads/Prozesse.
- Ideal für viele I/O-bound Tasks (z. B. viele Netzwerkanfragen) unabhängig vom eigentlichen Programm; d. h. wenn man auf ein Event von einem externen Programm wartet.
- Die CPU kann andere Aufgaben erledigen, während auf das externe Event gewartet wird.
- Arbeitet mit dem Event Loop.
Beispiel:
import asyncio
async def say_hello():
print("Hello")
await asyncio.sleep(1)
print("World")
asyncio.run(say_hello())
6 asyncio.gather
- Führt mehrere coroutines gleichzeitig aus.
- Wartet auf alle coroutines, gibt ihre Ergebnisse als Liste zurück.
import asyncio
async def task(n):
print(f"Task {n} startet")
await asyncio.sleep(n)
print(f"Task {n} endet")
return n * 10
async def main():
results = await asyncio.gather(
task(1),
task(2),
task(3)
)
print("Ergebnisse:", results)
asyncio.run(main())
7 Vergleich aller Methoden
| Kriterium | 🧵 Threading | 💥 Multiprocessing | ⚡ Asyncio |
|---|---|---|---|
| Parallel? | Ja (pseudo) | Ja (echt) | Ja (kooperativ) |
| GIL betroffen? | ✅ Ja | ❌ Nein | ✅ Ja |
| Für CPU-bound? | 🚫 Nein | ✅ Ja | 🚫 Nein |
| Für I/O-bound? | ✅ Ja | 😐 Mäßig geeignet | ✅ Ja |
| Speicher | Gemeinsam | Getrennt | Gemeinsam (1 Thread) |
| Kommunikation | Einfach | Komplex (IPC nötig) | Intern (await/gather) |
| Nutzung von Tasks | Threads | Prozesse | Coroutines |
| Startzeit | Schnell | Langsamer | Sehr schnell |
| Schwierigkeit | 😊 Einfach | 😓 Schwer | 😌 Mittel |
Module und Pakete
1 Aufbau von Modulen und Paketen
1.1 Was ist ein Modul?
Ein Modul ist eine einzelne Python-Datei (.py), die Funktionen, Klassen oder Variablen enthält. Module helfen, Code logisch zu gliedern und wiederverwendbar zu machen.
Beispiel für ein Modul math_utils.py:
# math_utils.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
Dieses Modul kann in anderen Dateien importiert werden:
import math_utils
print(math_utils.add(2, 3)) # Ausgabe: 5
print(math_utils.multiply(4, 5)) # Ausgabe: 20
1.2 Was ist ein Paket?
Ein Paket ist ein Verzeichnis, das mehrere Module enthält und eine __init__.py-Datei besitzt. Diese Datei kennzeichnet das Verzeichnis als Paket.
Beispielstruktur:
my_package/
├── __init__.py
├── math_utils.py
└── string_utils.py
Inhalt von __init__.py (kann leer sein oder Exporte definieren):
from .math_utils import add, multiply
from .string_utils import capitalize
Verwendung des Pakets:
import my_package
print(my_package.add(2, 2))
2 Import-System und Paketsuche
Python’s Import-System bestimmt, wie Module und Pakete gefunden und geladen werden. Das Verständnis von sys.path und importlib ist wichtig für größere Projekte und Package-Entwicklung.
2.1.1 Wie Python Module findet
Beim import-Statement sucht Python in dieser Reihenfolge:
- Built-in Modules (in Python kompiliert)
- Aktuelles Verzeichnis (wo das Script liegt)
PYTHONPATHEnvironment Variable- Standard-Library-Pfade
- Site-Packages (installierte Pakete)
import sys
# Alle Suchpfade anzeigen
for path in sys.path:
print(path)
# Typische Ausgabe:
# /Users/user/project ← Aktuelles Verzeichnis
# /usr/local/lib/python3.10 ← Standard Library
# /usr/local/lib/python3.10/site-packages ← Installierte Pakete
2.1.2 sys.path manipulieren
sys.path ist eine Liste und kann zur Laufzeit modifiziert werden.
import sys
# Pfad hinzufügen (am Ende)
sys.path.append('/path/to/my/modules')
# Pfad hinzufügen (am Anfang - höhere Priorität)
sys.path.insert(0, '/path/to/my/modules')
# Jetzt können Module aus diesem Pfad importiert werden
import my_custom_module
Praktisches Beispiel – Projekt-Root finden:
import sys
from pathlib import Path
# Projekt-Root zum Path hinzufügen
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
# Jetzt können alle Projekt-Module importiert werden
from src.utils import helper
2.1.3 PYTHONPATH Environment Variable
Permanent Pfade hinzufügen ohne Code-Änderung:
# Linux/macOS
export PYTHONPATH="/path/to/modules:$PYTHONPATH"
# Windows
set PYTHONPATH=C:\path\to\modules;%PYTHONPATH%
# In .bashrc/.zshrc dauerhaft:
echo 'export PYTHONPATH="/path/to/modules:$PYTHONPATH"' >> ~/.bashrc
In Python-Scripts:
import os
# PYTHONPATH zur Laufzeit setzen (vor Import!)
os.environ['PYTHONPATH'] = '/path/to/modules'
2.1.4 Module dynamisch importieren mit importlib
importlib erlaubt programmatisches Importieren zur Laufzeit.
Modul aus String importieren:
import importlib
# Modul-Name als String
module_name = 'math'
math_module = importlib.import_module(module_name)
print(math_module.sqrt(16)) # 4.0
# Mit relativem Import (innerhalb eines Pakets)
# from ..utils import helper ← als Code
helper = importlib.import_module('..utils.helper', package='mypackage.submodule')
Modul neu laden:
import importlib
import my_module
# Modul während Entwicklung neu laden
importlib.reload(my_module)
Plugin-System mit dynamischen Imports:
import importlib
import os
def load_plugins(plugin_dir):
"""Lädt alle Python-Dateien aus plugin_dir als Module"""
plugins = []
for filename in os.listdir(plugin_dir):
if filename.endswith('.py') and not filename.startswith('_'):
module_name = filename[:-3] # .py entfernen
# Dynamisch importieren
spec = importlib.util.spec_from_file_location(
module_name,
os.path.join(plugin_dir, filename)
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
plugins.append(module)
return plugins
# Verwendung
plugins = load_plugins('./plugins')
for plugin in plugins:
if hasattr(plugin, 'initialize'):
plugin.initialize()
2.1.5 Import-Mechanismus im Detail
Was passiert bei import module:
- Python prüft
sys.modules(Cache) ob Modul bereits geladen - Falls nicht: Suche nach Modul in
sys.path - Modul-Datei wird gefunden und kompiliert zu Bytecode (
.pyc) - Code wird ausgeführt
- Modul-Objekt wird in
sys.modulesgecacht - Name wird im lokalen Namespace gebunden
import sys
# Modul-Cache anzeigen
print('math' in sys.modules) # False
import math
print('math' in sys.modules) # True
# Modul ist jetzt gecacht
import math # Lädt nicht neu, nutzt Cache
Cache manuell leeren:
import sys
# Modul aus Cache entfernen
if 'my_module' in sys.modules:
del sys.modules['my_module']
# Jetzt wird es beim nächsten Import neu geladen
import my_module
2.1.6 Import-Varianten
# 1. Ganzes Modul importieren
import math
print(math.sqrt(16))
# 2. Spezifische Funktionen importieren
from math import sqrt, pi
print(sqrt(16))
# 3. Alles importieren (nicht empfohlen!)
from math import *
# 4. Mit Alias
import numpy as np
from datetime import datetime as dt
# 5. Relativer Import (nur innerhalb von Paketen)
from . import sibling_module
from .. import parent_module
from ..sibling_package import module
2.1.7 __init__.py im Detail
Die __init__.py kontrolliert, was beim Import eines Pakets passiert.
Leere __init__.py:
# my_package/__init__.py
# (leer)
# Verwendung
import my_package.module # OK
from my_package import module # OK
import my_package # OK, aber nichts verfügbar
Mit Exporten:
# my_package/__init__.py
from .module_a import function_a
from .module_b import ClassB
from .subpackage import helper
__all__ = ['function_a', 'ClassB', 'helper']
# Verwendung
from my_package import function_a, ClassB
import my_package
my_package.function_a() # Funktioniert!
Mit __all__ für from package import *:
# my_package/__init__.py
from .module_a import function_a
from .module_b import function_b, function_c
# Nur diese werden bei "from package import *" exportiert
__all__ = ['function_a', 'function_b']
2.1.8 Namespace Packages (PEP 420)
Seit Python 3.3: Packages ohne __init__.py (für verteilte Packages).
namespace_package/
├── part1/
│ └── module_a.py
└── part2/
└── module_b.py
# Beide Teile werden als ein Namespace-Package behandelt
from namespace_package.part1 import module_a
from namespace_package.part2 import module_b
Wann verwenden:
- Große Packages über mehrere Repositories verteilt
- Plugin-Systeme
- Unternehmens-interne Package-Strukturen
2.1.9 Import-Hooks und Finder
Fortgeschritten: Eigene Import-Mechanismen implementieren.
import sys
import importlib.abc
import importlib.machinery
class CustomFinder(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path, target=None):
"""Custom Module Finder"""
if fullname.startswith('custom_'):
print(f"Custom finder handling: {fullname}")
# Eigene Logik zum Finden des Moduls
return None
# Finder registrieren
sys.meta_path.insert(0, CustomFinder())
2.1.10 Debugging von Import-Problemen
import sys
# 1. Prüfen, ob Modul im Path liegt
def find_module(module_name):
"""Zeigt, wo Python nach einem Modul suchen würde"""
import importlib.util
spec = importlib.util.find_spec(module_name)
if spec:
print(f"Found: {spec.origin}")
else:
print(f"Not found: {module_name}")
print("\nSearching in:")
for path in sys.path:
print(f" {path}")
find_module('numpy')
# 2. Import mit Debugging
import importlib
import logging
logging.basicConfig(level=logging.DEBUG)
# Zeigt detaillierte Import-Informationen
import importlib
importlib.import_module('my_module')
# 3. Verbose Imports beim Start
# python -v script.py
2.1.11 Best Practices
✅ DO:
# Absolute Imports bevorzugen
from my_package.module import function
# sys.path nur für temporäre Anpassungen
import sys
sys.path.insert(0, '/temp/path')
# importlib für dynamische Imports
import importlib
module = importlib.import_module('plugin_name')
# __all__ in __init__.py definieren
__all__ = ['public_function', 'PublicClass']
❌ DON’T:
# Vermeiden: from module import *
from math import * # Namespace pollution!
# Vermeiden: sys.path dauerhaft manipulieren in Libraries
sys.path.append('/hardcoded/path') # Nicht portabel!
# Vermeiden: Zirkuläre Imports
# module_a.py imports module_b
# module_b.py imports module_a
# Vermeiden: __init__.py mit viel Logik
# Imports sollten schnell sein
2.1.12 Häufige Import-Probleme
Problem 1: ModuleNotFoundError
# Fehler: ModuleNotFoundError: No module named 'my_module'
# Lösung 1: Pfad prüfen
import sys
print(sys.path)
# Lösung 2: Pfad hinzufügen
sys.path.insert(0, '/path/to/module')
# Lösung 3: PYTHONPATH setzen
# export PYTHONPATH="/path/to/module:$PYTHONPATH"
# Lösung 4: Package installieren
# pip install my_module
Problem 2: Zirkuläre Imports
# module_a.py
from module_b import function_b
# module_b.py
from module_a import function_a # ImportError!
# Lösung 1: Import in Funktion verschieben
# module_a.py
def my_function():
from module_b import function_b
function_b()
# Lösung 2: Gemeinsame Logik in drittes Modul auslagern
Problem 3: Name Conflicts
# ❌ Überschreibt builtin
from mymodule import open # Überschreibt open()!
# ✅ Alias verwenden
from mymodule import open as myopen
2.1.13 Praktisches Beispiel: Projekt-Setup
myproject/
├── src/
│ ├── __init__.py
│ ├── core/
│ │ ├── __init__.py
│ │ └── engine.py
│ └── utils/
│ ├── __init__.py
│ └── helpers.py
├── tests/
│ └── test_engine.py
└── setup.py
In Tests importieren:
# tests/test_engine.py
import sys
from pathlib import Path
# Projekt-Root zum Path hinzufügen
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
# Jetzt funktionieren Imports
from src.core.engine import Engine
from src.utils.helpers import helper_function
Bessere Lösung: Editable Install
# Im Projekt-Root
pip install -e .
# Jetzt funktioniert überall:
from src.core.engine import Engine
2.1.14 Zusammenfassung
| Konzept | Zweck |
|---|---|
sys.path | Liste der Suchpfade für Module |
sys.modules | Cache geladener Module |
importlib | Programmatisches Importieren |
PYTHONPATH | Environment Variable für Suchpfade |
__init__.py | Paket-Initialisierung und Exporte |
__all__ | Kontrolliert from package import * |
| Namespace Packages | Packages ohne __init__.py (PEP 420) |
Kernprinzip: Python’s Import-System ist flexibel und erweiterbar. Für normale Projekte reichen Standard-Imports, aber importlib und sys.path ermöglichen erweiterte Szenarien wie Plugin-Systeme und dynamisches Laden.
HTML-Dokumentation
9 Dokumentation erstellen
Gute Dokumentation ist essenziell für wartbaren Code. Python bietet mehrere Tools zur automatischen Generierung von Dokumentation aus Docstrings.
9.1 Docstrings – Best Practices
Docstrings dokumentieren Module, Klassen und Funktionen direkt im Code.
9.1.1 Drei Hauptstile
Google Style (empfohlen für Lesbarkeit):
def calculate_distance(x1: float, y1: float, x2: float, y2: float) -> float:
"""Berechnet die euklidische Distanz zwischen zwei Punkten.
Args:
x1: X-Koordinate des ersten Punkts
y1: Y-Koordinate des ersten Punkts
x2: X-Koordinate des zweiten Punkts
y2: Y-Koordinate des zweiten Punkts
Returns:
Die euklidische Distanz als float
Raises:
ValueError: Wenn Koordinaten nicht numerisch sind
Examples:
>>> calculate_distance(0, 0, 3, 4)
5.0
"""
return ((x2 - x1)**2 + (y2 - y1)**2)**0.5
NumPy Style (wissenschaftliche Pakete):
def calculate_distance(x1, y1, x2, y2):
"""
Berechnet die euklidische Distanz zwischen zwei Punkten.
Parameters
----------
x1 : float
X-Koordinate des ersten Punkts
y1 : float
Y-Koordinate des ersten Punkts
x2 : float
X-Koordinate des zweiten Punkts
y2 : float
Y-Koordinate des zweiten Punkts
Returns
-------
float
Die euklidische Distanz
Examples
--------
>>> calculate_distance(0, 0, 3, 4)
5.0
"""
return ((x2 - x1)**2 + (y2 - y1)**2)**0.5
Sphinx Style (klassisch):
def calculate_distance(x1, y1, x2, y2):
"""Berechnet die euklidische Distanz zwischen zwei Punkten.
:param x1: X-Koordinate des ersten Punkts
:type x1: float
:param y1: Y-Koordinate des ersten Punkts
:type y1: float
:param x2: X-Koordinate des zweiten Punkts
:type x2: float
:param y2: Y-Koordinate des zweiten Punkts
:type y2: float
:return: Die euklidische Distanz
:rtype: float
:raises ValueError: Wenn Koordinaten nicht numerisch sind
"""
return ((x2 - x1)**2 + (y2 - y1)**2)**0.5
9.1.2 Docstring-Konventionen
Modul-Docstring:
"""Geometrie-Utilities für 2D-Berechnungen.
Dieses Modul stellt Funktionen für geometrische Berechnungen bereit,
einschließlich Distanz, Fläche und Umfang.
Example:
>>> from geometry import calculate_distance
>>> calculate_distance(0, 0, 3, 4)
5.0
"""
import math
# ... Code ...
Klassen-Docstring:
class Circle:
"""Repräsentiert einen Kreis.
Attributes:
radius: Radius des Kreises in Metern
center: Tuple (x, y) für Mittelpunkt
Example:
>>> circle = Circle(5.0, (0, 0))
>>> circle.area()
78.54
"""
def __init__(self, radius: float, center: tuple = (0, 0)):
"""Initialisiert einen Kreis.
Args:
radius: Radius des Kreises
center: Mittelpunkt als (x, y) Tuple
"""
self.radius = radius
self.center = center
Best Practices:
# ✅ DO
def process_data(data: list[str]) -> dict:
"""Verarbeitet Eingabedaten zu strukturiertem Dictionary.
Args:
data: Liste von Rohdaten-Strings
Returns:
Dictionary mit verarbeiteten Daten
"""
pass
# ❌ DON'T
def process_data(data):
"""processes data""" # Zu kurz, nicht aussagekräftig
pass
9.2 Sphinx – Der Python-Standard
Sphinx ist das Standard-Tool für Python-Dokumentation (verwendet von Python selbst, Django, Flask, etc.).
9.2.1 Installation und Setup
pip install sphinx sphinx-rtd-theme
Projekt initialisieren:
# Im Projekt-Root
mkdir docs
cd docs
sphinx-quickstart
# Interaktive Fragen beantworten:
# > Separate source and build directories? [n]: y
# > Project name: My Project
# > Author name(s): Your Name
# > Project release []: 1.0.0
Struktur:
myproject/
├── docs/
│ ├── source/
│ │ ├── conf.py # Konfiguration
│ │ ├── index.rst # Hauptseite
│ │ └── _static/
│ └── build/
│ └── html/ # Generierte HTML-Dateien
├── src/
│ └── myproject/
└── README.md
9.2.2 Konfiguration (conf.py)
# docs/source/conf.py
import os
import sys
sys.path.insert(0, os.path.abspath('../../src'))
project = 'My Project'
copyright = '2024, Your Name'
author = 'Your Name'
release = '1.0.0'
# Extensions
extensions = [
'sphinx.ext.autodoc', # Automatische API-Docs
'sphinx.ext.napoleon', # Google/NumPy Docstrings
'sphinx.ext.viewcode', # Quellcode-Links
'sphinx.ext.intersphinx', # Links zu anderer Dokumentation
]
# Theme
html_theme = 'sphinx_rtd_theme'
# Napoleon settings (für Google/NumPy Style)
napoleon_google_docstring = True
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = True
9.2.3 Dokumentation schreiben
index.rst:
Willkommen zu My Project
========================
.. toctree::
:maxdepth: 2
:caption: Inhalt:
installation
quickstart
api
examples
Indices
=======
* :ref:`genindex`
* :ref:`modindex`
api.rst (automatische API-Dokumentation):
API Reference
=============
.. automodule:: myproject.module
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: myproject.MyClass
:members:
:special-members: __init__
9.2.4 Build und Vorschau
# HTML generieren
cd docs
make html
# Öffnen
open build/html/index.html
# Auto-Rebuild bei Änderungen
pip install sphinx-autobuild
sphinx-autobuild source build/html
# → http://127.0.0.1:8000
9.2.5 Read the Docs Integration
Kostenlos hosten auf readthedocs.org:
- Erstelle
.readthedocs.yaml:
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: docs/source/conf.py
python:
install:
- requirements: docs/requirements.txt
- method: pip
path: .
docs/requirements.txt:
sphinx>=5.0
sphinx-rtd-theme
- GitHub Repository → Read the Docs verbinden
- Automatischer Build bei jedem Push
9.3 MkDocs – Modernes Markdown
MkDocs ist einfacher als Sphinx und verwendet Markdown statt reStructuredText.
9.3.1 Installation und Setup
pip install mkdocs mkdocs-material
Projekt initialisieren:
mkdocs new my-project
cd my-project
Struktur:
my-project/
├── docs/
│ ├── index.md
│ ├── installation.md
│ └── api.md
└── mkdocs.yml
9.3.2 Konfiguration (mkdocs.yml)
site_name: My Project
site_description: Awesome Python project
site_author: Your Name
site_url: https://myproject.readthedocs.io
theme:
name: material
palette:
primary: indigo
accent: indigo
features:
- navigation.tabs
- navigation.sections
- toc.integrate
- search.suggest
nav:
- Home: index.md
- Installation: installation.md
- User Guide:
- Getting Started: guide/quickstart.md
- Advanced: guide/advanced.md
- API Reference: api.md
plugins:
- search
- mkdocstrings:
handlers:
python:
options:
show_source: true
markdown_extensions:
- admonition
- codehilite
- toc:
permalink: true
9.3.3 Automatische API-Docs mit mkdocstrings
pip install mkdocstrings[python]
docs/api.md:
# API Reference
## Module
::: myproject.module
options:
show_root_heading: true
show_source: true
## Classes
::: myproject.MyClass
options:
members:
- method1
- method2
9.3.4 Build und Deploy
# Lokaler Server
mkdocs serve
# → http://127.0.0.1:8000
# HTML generieren
mkdocs build
# GitHub Pages deployen
mkdocs gh-deploy
9.4 pdoc – Automatische API-Dokumentation
pdoc generiert automatisch Dokumentation aus Docstrings – kein Setup nötig.
9.4.1 Installation und Verwendung
pip install pdoc
Einfache Verwendung:
# HTML-Dokumentation generieren
pdoc myproject -o docs/
# Live-Server
pdoc myproject --http :8080
# → http://localhost:8080
9.4.2 Konfiguration
Eigenes Template (optional):
# Template anpassen
pdoc myproject -o docs/ --template-dir ./templates/
Ausgabe anpassen:
# myproject/__init__.py
"""My Project
Modulbeschreibung mit **Markdown** Support.
# Features
- Feature 1
- Feature 2
## Examples
```python
from myproject import func
func()
```
"""
__pdoc__ = {
'private_function': False, # Nicht dokumentieren
'MyClass.internal_method': False,
}
9.4.3 Vorteile von pdoc
- ✅ Kein Setup nötig
- ✅ Automatische Erkennung
- ✅ Markdown in Docstrings
- ✅ Live-Reload
- ✅ Single-Command
Wann verwenden:
- Schnelle API-Dokumentation
- Kleine bis mittlere Projekte
- Kein komplexes Layout nötig
9.5 Vergleich
| Tool | Komplexität | Features | Format | Use Case |
|---|---|---|---|---|
| Sphinx | Hoch | Sehr viele | reStructured | Große Projekte, Standard |
| MkDocs | Niedrig | Mittel | Markdown | Moderne Docs, einfach |
| pdoc | Sehr niedrig | Automatisch | Docstrings | Schnelle API-Docs |
9.6 Praktisches Beispiel: Vollständige Dokumentation
Projekt-Struktur:
myproject/
├── src/
│ └── myproject/
│ ├── __init__.py
│ ├── calculator.py
│ └── utils.py
├── docs/
│ ├── source/ # Sphinx
│ │ ├── conf.py
│ │ └── index.rst
│ └── mkdocs.yml # MkDocs (alternativ)
├── tests/
├── pyproject.toml
└── README.md
calculator.py mit vollständigen Docstrings:
"""Mathematische Berechnungen.
Dieses Modul stellt grundlegende mathematische Operationen bereit.
"""
class Calculator:
"""Einfacher Taschenrechner.
Attributes:
history: Liste der durchgeführten Operationen
Example:
>>> calc = Calculator()
>>> calc.add(2, 3)
5
>>> calc.history
['2 + 3 = 5']
"""
def __init__(self):
"""Initialisiert Calculator mit leerer History."""
self.history = []
def add(self, a: float, b: float) -> float:
"""Addiert zwei Zahlen.
Args:
a: Erste Zahl
b: Zweite Zahl
Returns:
Summe von a und b
Example:
>>> calc = Calculator()
>>> calc.add(10, 5)
15.0
"""
result = a + b
self.history.append(f"{a} + {b} = {result}")
return result
def divide(self, a: float, b: float) -> float:
"""Dividiert zwei Zahlen.
Args:
a: Dividend
b: Divisor
Returns:
Quotient von a durch b
Raises:
ValueError: Wenn b gleich 0 ist
Example:
>>> calc = Calculator()
>>> calc.divide(10, 2)
5.0
"""
if b == 0:
raise ValueError("Division durch Null nicht erlaubt")
result = a / b
self.history.append(f"{a} / {b} = {result}")
return result
9.7 CI/CD Integration
GitHub Actions für automatische Docs:
# .github/workflows/docs.yml
name: Documentation
on:
push:
branches: [main]
jobs:
deploy-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install sphinx sphinx-rtd-theme
- name: Build docs
run: |
cd docs
make html
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/build/html
9.8 Best Practices
✅ DO:
- Google-Style Docstrings für Lesbarkeit
- Docstrings für alle öffentlichen Funktionen/Klassen
- Examples in Docstrings (mit doctest-Syntax)
- Type Hints + Docstrings kombinieren
- Automatische API-Docs (Sphinx/pdoc)
- CI/CD für Docs-Build
❌ DON’T:
- Docstrings nur für komplexe Funktionen (alle dokumentieren!)
- Informationen in Docstrings und Code duplizieren
- Veraltete Docstrings (bei Code-Änderung aktualisieren!)
- Zu viele Details in Docstrings (Implementierung gehört in Kommentare)
9.9 Docstring-Linting
Tools zur Validierung:
# pydocstyle - Docstring-Konventionen prüfen
pip install pydocstyle
pydocstyle src/
# darglint - Docstrings gegen Signaturen prüfen
pip install darglint
darglint src/myproject/
# interrogate - Docstring-Coverage messen
pip install interrogate
interrogate -v src/
# Mit Ruff (bereits integriert)
ruff check --select D .
Pre-commit Hook:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pycqa/pydocstyle
rev: 6.3.0
hooks:
- id: pydocstyle
9.10 Zusammenfassung
| Aspekt | Empfehlung |
|---|---|
| Docstring-Stil | Google Style (lesbar, klar) |
| Große Projekte | Sphinx + Read the Docs |
| Einfache Docs | MkDocs + Material Theme |
| Schnelle API | pdoc (automatisch) |
| Hosting | Read the Docs / GitHub Pages |
| Validierung | pydocstyle / Ruff |
Kernprinzip: Gute Dokumentation ist Teil des Codes. Nutze Docstrings konsequent, automatische Generierung (Sphinx/MkDocs/pdoc), und CI/CD für stets aktuelle Dokumentation.
Entwicklungs-Tooling & Packaging
Modernes Python-Dependency-Management und Packaging haben sich in den letzten Jahren stark weiterentwickelt. Dieses Kapitel behandelt Virtual Environments, Dependency-Management-Tools und moderne Packaging-Standards.
1 Virtual Environments
Virtual Environments isolieren Python-Projekte voneinander und vermeiden Versionskonflikte zwischen Abhängigkeiten.
1.1 venv – Standard Virtual Environment
# Virtual Environment erstellen
python -m venv myenv
# Aktivieren (Linux/macOS)
source myenv/bin/activate
# Aktivieren (Windows)
myenv\Scripts\activate
# Deaktivieren
deactivate
# Löschen (einfach Ordner entfernen)
rm -rf myenv
Wichtige Punkte:
venvist seit Python 3.3 Teil der Standardbibliothek- Erstellt isolierte Python-Installation mit eigenem
site-packages - Sollte nicht ins Git-Repository committed werden
1.2 Wo liegt was?
myenv/
├── bin/ # Aktivierungsscripts und Python-Interpreter (Linux/macOS)
├── Scripts/ # Aktivierungsscripts und Python-Interpreter (Windows)
├── include/ # C-Header-Dateien für Extensions
├── lib/
│ └── python3.x/
│ └── site-packages/ # Installierte Pakete
└── pyvenv.cfg # Konfiguration des Virtual Environment
1.3 virtualenv – Alternative zu venv
# Installation
pip install virtualenv
# Virtual Environment erstellen
virtualenv myenv
# Mit spezifischer Python-Version
virtualenv -p python3.11 myenv
# Vorteile gegenüber venv:
# - Schnellere Erstellung
# - Mehr Konfigurationsoptionen
# - Kompatibel mit älteren Python-Versionen
1.4 .gitignore für Virtual Environments
# Virtual Environments
venv/
env/
ENV/
myenv/
.venv/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
2 Code Quality Tools (Linting & Type Checking)
Code Quality Tools wie Linter und Typer Checker analysieren den Quellcode. Sie weisen auf potenzielle Fehler, Stilprobleme oder Verstöße gegen Coding-Richtlinien hin – ohne den Code auszuführen. Linter und Typer Checker helfen dabei, fehlerfreien, konsistenten und wartbaren Code zu schreiben.
2.1 Was sind Linter und Type Checker?
Linter:
- Analysieren Code auf Fehler und Stil-Probleme
- Erzwingen Coding-Standards (PEP 8)
- Finden potenzielle Bugs
Type Checker:
- Überprüfen Typannotationen
- Finden Typ-Inkonsistenzen
- Verbessern Code-Dokumentation
2.2 Linter
2.2.1 Pylint (obsolet)
- Ein sehr strenger Linter, der Code-Qualität überprüft.
- Findet Fehler, schlechte Praktiken und Verstöße gegen Coding-Standards.
- Sehr langsam im Vergleich zu anderen Lintern, da in Python geschrieben.
2.2.2 Flake8 (obsolet)
- Ein leichtgewichtiger Linter für PEP8-konforme Code-Qualität.
- Unterstützt Plugins für zusätzliche Checks.
- Weniger strikt als Pylint.
2.3 Ruff – Moderner All-in-One Linter
Installation:
pip install ruff
Grundlegende Verwendung:
# Code prüfen
ruff check .
# Auto-Fix
ruff check --fix .
# Formatieren (ersetzt black)
ruff format .
# In CI/CD
ruff check --output-format=github .
Konfiguration (pyproject.toml):
[tool.ruff]
line-length = 88
target-version = "py311"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
]
ignore = ["E501"] # line too long
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
- Extrem schneller Linter (da in Rust programmiert!) mit Unterstützung für PEP8, Typing, Imports, usw.
- Enthält bereits die Funktionen von Flake8 + viele Plugins.
- Kann Pylint-Regeln emulieren.
- Sehr viel schneller als Flake8 oder Pylint.
- Experimenteller mypy-Support, aber ist nicht so mächtig wie mypy selbst
Ersetzt folgende Tools:
- ✅ Flake8 (Linting)
- ✅ isort (Import-Sortierung)
- ✅ black (Formatierung)
- ✅ pyupgrade (Syntax-Modernisierung)
- ⚠️ Pylint (teilweise, weniger strikt)
veraltete tools
Flake8, Pylint, isort (Sortierung von Importen) und black sind nicht mehr erforderlich, da Ruff deren Funktionen (und mehr) bereits integriert hat.
Ruff gibt auf ihrer Homepage an, dass es deutlich schneller als vergleichbare Tools ist, siehe docs.astral.sh/ruff/.
2.3.1 Links
- https://docs.astral.sh/ruff/
- https://docs.astral.sh/ruff/rules/
2.4 Type Checker
2.4.1 Mypy – Strenger Type Checker
- Überprüft, ob Typannotationen korrekt sind
- Umfassender als Pylance, besonders für große Projekte
Installation:
pip install mypy
Verwendung:
# Einzelne Datei
mypy script.py
# Ganzes Projekt
mypy src/
# Mit strikten Regeln
mypy --strict src/
Konfiguration (pyproject.toml):
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
# Per-Modul Konfiguration
[tool.mypy.overrides](tool.mypy.overrides.md)
module = "tests.*"
disallow_untyped_defs = false
Häufige Mypy-Patterns:
from typing import Optional, List, Dict
def process(data: List[str]) -> Optional[Dict[str, int]]:
if not data:
return None
return {item: len(item) for item in data}
# Type Ignores (sparsam verwenden!)
result = legacy_function() # type: ignore
2.4.2 Pyright/Pylance – Alternative zu Mypy
Pyright
Merkmale:
- In TypeScript geschrieben (schneller als Mypy)
- Basis für Pylance
- Strikte Type Checks
# Installation
npm install -g pyright
# Verwendung
pyright src/
Pylance
- Basiert auf Microsofts Pyright.
- Verbessert Autovervollständigung, Typinferenz und Fehleranalyse.
VS Code Einstellungen:
{
"python.analysis.typeCheckingMode": "basic",
"python.analysis.diagnosticMode": "workspace",
"python.analysis.autoImportCompletions": true
}
2.4.3 ty (Astral, Beta) – Schnellster Type Checker
ty ist ein extrem schneller Python Type Checker und Language Server von Astral (den Machern von Ruff und uv), geschrieben in Rust. ty ist als Alternative zu mypy, Pyright und Pylance konzipiert.
Status: Beta (seit Dezember 2025), Stable Release für 2026 geplant.
Stärken:
- 10–60× schneller als mypy/Pyright (ohne Cache)
- Inkrementelle Updates in ~5ms (80× schneller als Pyright, 500× schneller als Pyrefly)
- Eingebauter Language Server (LSP) für VS Code, Neovim, PyCharm u.a.
- First-class Intersection Types und fortgeschrittenes Type Narrowing
- Diagnostik-System inspiriert vom Rust-Compiler (Kontext aus mehreren Dateien)
- Geplante Integration mit Ruff (type-aware Linting, Dead Code Elimination)
Schwächen (Stand Februar 2026):
- Conformance mit der Python Typing Specification bei ~15% (Pyrefly ~70%, Pyright deutlich höher)
- Noch viele False Positives bei komplexen Codebasen
- Kein Plugin-System für Django/SQLAlchemy/Pydantic (noch)
Installation und Verwendung:
# Installation via uv
uv tool install ty@latest
# Einzelne Datei oder Projekt prüfen
ty check src/
# Watch-Modus (inkrementell)
ty check --watch
Konfiguration (pyproject.toml):
[tool.ty.rules]
# Regeln können individuell konfiguriert werden
# Siehe: https://docs.astral.sh/ty/reference/rules/
Empfehlung: Für experimentierfreudige Entwickler jetzt schon als schnelles Editor-Feedback nutzbar. Für CI/CD weiterhin Pyright oder mypy parallel einsetzen, bis die Genauigkeit steigt. Astrals Track Record mit Ruff und uv spricht dafür, dass ty mittelfristig zum Standard wird.
- https://docs.astral.sh/ty/
- https://github.com/astral-sh/ty
2.4.4 Pyrefly (Meta, Beta) – Nachfolger von Pyre
Pyrefly ist Metas neuer Type Checker und Language Server, in Rust geschrieben. Er ersetzt den älteren, OCaml-basierten Pyre Type Checker, der u.a. für Instagrams Codebase entwickelt wurde.
Status: Beta (seit November 2025).
Stärken:
- Sehr hohe Performance (1,85 Mio. Zeilen/Sekunde, PyTorch in 2,4s vs. Pyright 35,2s vs. mypy 48,1s)
- 70% Conformance mit Python Typing Specification (von 39% bei Alpha-Launch)
- Automatische Type Inference für Rückgabewerte und lokale Variablen
- Automatische Type Stubs für populäre Third-Party-Libraries
- IDE-Extensions für VS Code, Neovim, Zed, Emacs, Helix
- Vorläufiger Support für Django, Pydantic und Jupyter Notebooks
- MIT-Lizenz, aktive Community (Discord, GitHub)
Schwächen:
- Inkrementelle Updates deutlich langsamer als ty (~2,4s vs. 4,7ms bei PyTorch)
- Framework-Support (Django, SQLAlchemy) noch in Entwicklung
- Weniger verbreitet als Pyright oder mypy
Installation und Verwendung:
# Installation
pip install pyrefly
# Projekt initialisieren
pyrefly init
# Type Check
pyrefly check
# Mit Fehler-Zusammenfassung
pyrefly check --summarize-errors
Empfehlung: Für Projekte, die von Pyre migrieren oder einen schnellen Type Checker mit guter Conformance suchen. Für die meisten Anwender ist Pyright aktuell noch die sicherere Wahl.
- https://pyrefly.org/
- https://github.com/facebook/pyrefly
2.5 Vergleich
| Tool | Typ | Sprache | Geschwindigkeit | Conformance | LSP/IDE | Status |
|---|---|---|---|---|---|---|
| Ruff | Linter | Rust | ✅✅ Extrem schnell | Hoch | ⚠️ Kein Type LSP | ✅ Stabil |
| Linter | Python | ❌ Langsam | Mittel | ❌ Nein | ⛔ Obsolet | |
| Linter | Python | ❌ Sehr langsam | Sehr hoch | ❌ Nein | ⛔ Obsolet | |
| Pyright | Type Checker | TypeScript | ✅ Schnell | ✅✅ Sehr hoch | ✅ Pylance (VS Code) | ✅ Stabil |
| mypy | Type Checker | Python | ⚠️ Mittel | ✅ Hoch | ❌ Nein | ✅ Stabil |
| ty | Type Checker | Rust | ✅✅ Extrem schnell | ❌ ~15% | ✅ Eingebaut | ⚠️ Beta |
| Pyrefly | Type Checker | Rust | ✅ Sehr schnell | ⚠️ ~70% | ✅ Eingebaut | ⚠️ Beta |
| Zuban | Type Checker | Rust | ✅ Schnell | ✅ ~69% | ✅ Eingebaut | ⚠️ Beta |
| Type Checker | OCaml | ⚠️ Mittel | ⚠️ Mittel | ❌ Nein | ⛔ Obsolet |
2.6 Zusammenfassung
Modern Stack (2024+):
- Ruff: Linting + Formatting (Standard)
- Mypy: Type Checking
- Pyright/Pylance: Type Checking (zuverlässigste Option)
Aufstrebend (Beta, 2025/2026): -
- ty (Astral) – extrem schnell, noch geringe Conformance
- Pyrefly (Meta) – schnell, bessere Conformance als ty
- Zuban – mypy-kompatibel, höchste Conformance der neuen Tools
Legacy Stack (nicht mehr empfohlen):
Flake8→ Ruffblack→ Ruff formatisort→ RuffPylint→ Ruff (oder zusätzlich)Pyre→ Pyrefly
2.6.1 Veraltete/Abgelöste Tools
Die folgenden Tools sind funktional ersetzt und sollten bei neuen Projekten nicht mehr eingesetzt werden, weil Ruff (in Rust geschrieben) deren Funktionen in einem einzigen Tool mit 10–100× höherer Geschwindigkeit vereint, bessere Wartbarkeit und einfacherere Konfiguration über eine einzige pyproject.toml bietet:
- Flake8 – komplett durch Ruff ersetzt. Ruff implementiert alle relevanten Flake8-Regeln plus die meisten populären Plugins (isort, pyupgrade, eradicate, yesqa, …) und ist dabei 10–100× schneller.
- Black – Ruff enthält einen eigenen Formatter (
ruff format), der Black-kompatibel ist. Ein separates Tool ist nicht mehr nötig. - isort – ebenfalls in Ruff integriert (
ruff check --select I). - Pylint – wird zwar noch gepflegt, aber immer weniger genutzt. Ruff deckt den Großteil der relevanten Regeln ab und ist drastisch schneller. Für manche sehr spezifischen Pylint-Checks gibt es noch keinen Ruff-Ersatz, aber für die meisten Projekte reicht Ruff.
- Pyre (Meta) – wird durch Pyrefly abgelöst, Metas neuen Rust-basierten Type Checker.
2.6.2 Moderne Linter und Type Checker
Linting & Formatting: Ruff
Ruff ersetzt Flake8, Black, isort, pyupgrade und viele weitere Tools in einem einzigen, extrem schnellen Rust-basierten Tool. Es ist mittlerweile der De-facto-Standard mit über 100.000 GitHub Stars und wird von den meisten großen Open-Source-Projekten genutzt. Konfiguration läuft über ruff.toml oder pyproject.toml.
Type Checking: Drei Optionen
1. Pyright – aktuell der zuverlässigste Type Checker. Microsofts TypeScript-basiertes Tool, das auch Pylance in VS Code antreibt. Pyright implementiert neue Typing-Features oft vor anderen Tools und bietet starke IDE-Integration (Rob’s Blog). Für Produktionsprojekte Stand heute die sicherste Wahl.
2. mypy – der Klassiker, weiterhin weit verbreitet und stabil. Für bestehende Projekte absolut brauchbar, aber langsamer als die Alternativen und bei neuen Typing-Features manchmal hinterher.
3. ty (Astral, Beta) – das spannendste neue Tool. ty ist ein extrem schneller Python Type Checker und Language Server, in Rust geschrieben, entwickelt als Alternative zu mypy, Pyright und Pylance. Astral Performance liegt bei 10–60× schneller als mypy/Pyright, mit inkrementellen Updates in 4,7ms statt Sekunden (byteiota). Allerdings ist die Genauigkeit noch nicht auf dem Niveau von Pyright – ty besteht aktuell etwa 15% der Conformance-Tests, während Pyright und andere deutlich weiter sind (byteiota). Für “motivated users” empfohlen, Stable Release ist für 2026 geplant. Für Neovim-Nutzer ist der eingebaute LSP besonders interessant sein.
Weitere Newcomer: Pyrefly (Meta, Rust-basiert) und Zuban (mypy-kompatibel, Rust-basiert) sind ebenfalls 2025 erschienen, aber weniger ausgereift als ty.
tip
Für ein umfassende Absicherung der Code-Qualität reichen Ruff (Linting und Formatting) und Pyright/Pylance (Type Checking) aus. Zukünftig könnte ty Pyright/Mypy ablösen.
In der pyproject.toml sieht ein modernes Setup dann minimal so aus:
[tool.ruff]
line-length = 88
target-version = "py313"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "standard"
3 Klassisches Dependency Management mit pip
3.1 Pakete installieren
# Einzelnes Paket
pip install requests
# Spezifische Version
pip install requests==2.31.0
# Mindestversion
pip install requests>=2.30.0
# Versionsbereiche
pip install "requests>=2.30.0,<3.0.0"
# Aus requirements.txt
pip install -r requirements.txt
# Paket aktualisieren
pip install --upgrade requests
# Paket deinstallieren
pip uninstall requests
3.2 requirements.txt
# requirements.txt - Produktions-Dependencies
requests==2.31.0
pandas>=2.0.0,<3.0.0
numpy==1.24.3
python-dotenv==1.0.0
# Optional: Kommentare
matplotlib==3.7.1 # Für Visualisierungen
# Requirements generieren (nicht empfohlen für Entwicklung)
pip freeze > requirements.txt
# Installieren
pip install -r requirements.txt
3.3 Mehrere Requirements-Dateien
# Projektstruktur
requirements/
├── base.txt # Basis-Dependencies
├── dev.txt # Entwicklungs-Tools
└── production.txt # Produktions-spezifisch
# requirements/base.txt
requests==2.31.0
pandas==2.0.3
# requirements/dev.txt
-r base.txt # Basis-Dependencies inkludieren
pytest==7.4.0
black==23.7.0
mypy==1.5.0
# requirements/production.txt
-r base.txt
gunicorn==21.2.0
# Entwicklungsumgebung
pip install -r requirements/dev.txt
# Produktion
pip install -r requirements/production.txt
3.4 Probleme mit pip freeze
# ❌ Problematisch: pip freeze
pip freeze > requirements.txt
Nachteile:
- Listet alle installierten Pakete auf (auch transitive Dependencies)
- Keine Trennung zwischen direkten und indirekten Abhängigkeiten
- Versionsnummern sind exakt → keine Flexibilität bei Updates
- Schwer wartbar bei vielen Paketen
4 Paketinstallation aus eigenem Ordner mit Python-Dateien
Im Folgenden wird erklärt, wie man einen Ordner mit .py-Dateien zu einem Paket macht und ihn in einer Umgebung lokal installiert.
4.1 Erstinstallation
4.1.1 Ordnerstruktur vorbereiten
mein_paket/
├── mein_paket/
│ ├── __init__.py
│ ├── modul1.py
│ ├── modul2.py
│ └── ...
└── setup.py
Die Datei __init__.py muss existieren, da es den Ordner zu einem Python-Paket macht - sie kann auch leer sein, s. [[#4 Initialisierungs-Datei]].
setup-datei
Es muss eine pyproject.toml (empfohlen bei neuen Projekten) oder setup.py im Hauptverzeichnis exisieren!
setup.py:
from setuptools import setup, find_packages
setup(
name='pylightlib',
version='0.1.0',
packages=find_packages(include=['io', 'qt', 'tk', 'msc', 'math', 'mech']),
install_requires=[],
)
4.1.2 Installation
- Im Terminal zum Ordner des Pakets wechseln (
cd ...) - Richtige Umgebung auswählen (
conda activate ...) - Paket installieren mit
pip install .
Alternative (Entwicklungsmodus/editable mode: Änderungen werden ohne Neuinstallation wirksam):
pip install -e .
Installation prüfen mit:
python -c "from setuptools import find_packages; print(find_packages())"
Folgendes der .vscode/settings.json (im Root-Ordner des Projekts, welches das Modul einbinden soll) oder der settings.json des Benutzers hinzufügen:
{
"python.analysis.extraPaths": [
"/Users/cgroening/Documents/Code/Python/libs/pylightlib"
]
}
warning
Wenn der Pfad eines Pakets geändert wird, muss es wie folgt neuinstalliert werden:
- Paket deinstallieren:
pip uninstall <paketname> -y
.egg-Datei löschen !!
Diese Datei löschen:
/opt/anaconda3/envs/<envname>/lib/python<version>/site-packages/<paketname>.egg-link
- Neuinstallation mit
pip install -e .
4.1.3 Nutzung des Pakets
from mein_paket import modul1
4.2 Alternative
Statt das Paket in eine Umgebung zu installieren, kann man stattdessen nur den Pfad lokal verfügbar machen, in dem man ihn zum PYTHONPATH hinzufügt oder dies direkt im Code macht:
import sys
sys.path.append('/pfad/zum/ordner')
import modul1
Dies ist jedoch eher ein Workaround, keine saubere Installation.
4.3 Aktualisierung
Wenn normal installiert wurde, muss nach jeder Änderung der Installationsbefehl erneut ausgeführt werden, damit diese übernommen werden:
pip install .
Wenn im Entwicklungsmodus installiert wurde (pip install -e .), ist das Paket verlinkt, d. h. Änderungen am Code wirken sofort - ohne Neuinstallation.
4.4 Initialisierungs-Datei
inhalt von `__init__.py`
Was man in die Initialisierungs-Datei schreibt, entscheidet, wie “sauber” oder bequem das Paket von außen nutzbar ist. Man kann die Datei auch leer lassen, was besonders bei kleinen Paketen häufig geschieht.
4.4.1 Importverkürzung
Beispiel: Die folgende Struktur liegt vor:
mein_paket/
├── __init__.py
└── tools.py # enthält eine Funktion namens "berechne_xyz"
In __init__.py:
from .tools import berechne_xyz
So kann man in seinem Projekt den folgenden Import vornehmen:
from mein_paket import berechne_xyz
Anstatt:
from mein_paket.tools import berechne_xyz
4.4.2 Globale Variablen
Ein weiterer Verwendungszweck für die __init__.py ist das Speichern von globalen Variablen:
__version__ = "0.1.0"
4.4.3 Initialisierungscode
Wenn beim Import deines Pakets direkt etwas passieren soll (z. B. Logging einrichten oder ein globaler Status):
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("mein_paket wurde importiert.")
5 pip-tools – Besseres Dependency Management
pip-tools löst viele Probleme von pip freeze durch Trennung von abstrakten und konkreten Dependencies.
5.1 Installation
pip install pip-tools
5.2 Workflow mit pip-compile
# requirements.in - Was wir WOLLEN (abstrakt)
requests>=2.30.0
pandas
pytest>=7.0
# Konkrete Versionen generieren
pip-compile requirements.in
# Erzeugt requirements.txt mit allen transitiven Dependencies
# requirements.txt - Was INSTALLIERT wird (konkret)
# Generated by pip-compile
requests==2.31.0
# via -r requirements.in
certifi==2023.7.22
# via requests
charset-normalizer==3.2.0
# via requests
idna==3.4
# via requests
urllib3==2.0.4
# via requests
pandas==2.0.3
# via -r requirements.in
numpy==1.24.3
# via pandas
...
5.3 pip-sync – Exakte Environment-Replikation
# Virtual Environment exakt auf requirements.txt synchronisieren
pip-sync requirements.txt
# Entfernt Pakete, die nicht in requirements.txt sind
# Installiert fehlende Pakete
# Aktualisiert auf exakte Versionen
5.4 Dependency-Updates
# Alle Dependencies aktualisieren
pip-compile --upgrade requirements.in
# Nur ein spezifisches Paket aktualisieren
pip-compile --upgrade-package requests requirements.in
# Mit Hashes für zusätzliche Sicherheit
pip-compile --generate-hashes requirements.in
5.5 Mehrere Environments mit pip-tools
# requirements.in
requests
pandas
# requirements-dev.in
-c requirements.txt # Constraint-File: nutze dieselben Versionen
pytest
black
mypy
# Workflow
pip-compile requirements.in
pip-compile requirements-dev.in
# Installation
pip-sync requirements-dev.txt # Installiert dev + base
6 Poetry – All-in-One Lösung
Poetry kombiniert Dependency Management, Packaging und Veröffentlichung in einem Tool.
6.1 Installation
# Empfohlene Installation (Linux/macOS)
curl -sSL https://install.python-poetry.org | python3 -
# Alternative: pipx (isoliert)
pipx install poetry
# Nach Installation
poetry --version
6.2 Neues Projekt initialisieren
# Interaktive Projekt-Erstellung
poetry new myproject
# Generierte Struktur:
myproject/
├── pyproject.toml
├── README.md
├── myproject/
│ └── __init__.py
└── tests/
└── __init__.py
# In bestehendem Projekt
poetry init
6.3 pyproject.toml – Poetry-Konfiguration
[tool.poetry]
name = "myproject"
version = "0.1.0"
description = "A sample Python project"
authors = ["Your Name <you@example.com>"]
readme = "README.md"
license = "MIT"
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.31.0"
pandas = "^2.0.0"
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
black = "^23.7.0"
mypy = "^1.5.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
6.4 Dependencies verwalten
# Paket hinzufügen
poetry add requests
# Dev-Dependency hinzufügen
poetry add --group dev pytest
# Mit Version Constraints
poetry add "requests>=2.30.0,<3.0.0"
# Paket entfernen
poetry remove requests
# Alle Dependencies installieren
poetry install
# Ohne dev-dependencies (Produktion)
poetry install --without dev
# Dependencies aktualisieren
poetry update
# Nur ein Paket aktualisieren
poetry update requests
6.5 Virtual Environment mit Poetry
# Poetry erstellt automatisch ein Virtual Environment
poetry install
# In der Poetry-Shell arbeiten
poetry shell
# Befehl im Poetry-Environment ausführen (ohne shell)
poetry run python script.py
poetry run pytest
# Environment-Info anzeigen
poetry env info
# Environment-Pfad
poetry env info --path
# Environment löschen
poetry env remove python3.10
6.6 Lock-File
Poetry erstellt automatisch poetry.lock:
# poetry.lock - Exakte Versionen aller Dependencies
# Wird automatisch bei 'poetry add' / 'poetry update' aktualisiert
# SOLLTE ins Git committed werden
# Lock-File neu generieren ohne Installation
poetry lock
# Lock-File prüfen
poetry check
6.7 Scripts definieren
[tool.poetry.scripts]
start = "myproject.main:main"
test = "pytest"
# Scripts ausführen
poetry run start
poetry run test
6.8 Packaging und Veröffentlichung
# Paket bauen
poetry build
# Erstellt dist/myproject-0.1.0.tar.gz und .whl
# Auf PyPI veröffentlichen
poetry publish
# Beides zusammen
poetry publish --build
# Test-PyPI
poetry config repositories.testpypi https://test.pypi.org/legacy/
poetry publish -r testpypi
7 pyproject.toml – PEP 621 Standard
pyproject.toml ist der moderne Standard für Python-Projektkonfiguration (definiert in PEP 621).
7.1 Grundstruktur ohne Poetry
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mypackage"
version = "0.1.0"
description = "A sample Python package"
readme = "README.md"
authors = [
{name = "Your Name", email = "you@example.com"}
]
license = {text = "MIT"}
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]
keywords = ["example", "package"]
dependencies = [
"requests>=2.30.0",
"pandas>=2.0.0,<3.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"black>=23.7.0",
"mypy>=1.5.0",
]
docs = [
"sphinx>=7.0.0",
]
[project.urls]
Homepage = "https://github.com/username/mypackage"
Documentation = "https://mypackage.readthedocs.io"
Repository = "https://github.com/username/mypackage"
7.2 Installation mit pip
# Paket installieren (im Entwicklungsmodus)
pip install -e .
# Mit optional dependencies
pip install -e ".[dev]"
pip install -e ".[dev,docs]"
7.3 Dynamische Versionen
[project]
name = "mypackage"
dynamic = ["version"] # Version aus Code lesen
[tool.setuptools.dynamic]
version = {attr = "mypackage.__version__"}
# mypackage/__init__.py
__version__ = "0.1.0"
7.4 Entry Points / Console Scripts
[project.scripts]
myapp = "mypackage.cli:main"
mytool = "mypackage.tools:run"
# mypackage/cli.py
def main():
print("Hello from myapp!")
if __name__ == "__main__":
main()
# Nach Installation verfügbar:
myapp # Ruft mypackage.cli:main() auf
8 Verschiedene Build-Backends
8.1 setuptools (Standard)
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
8.2 Poetry
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
8.3 Flit (minimalistisch)
[build-system]
requires = ["flit_core>=3.2"]
build-backend = "flit_core.buildapi"
[project]
name = "mypackage"
authors = [{name = "Your Name"}]
dynamic = ["version", "description"]
8.4 Hatch (modern)
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
9 Tool-Konfiguration in pyproject.toml
9.1 Black (Formatter)
[tool.black]
line-length = 88
target-version = ['py310', 'py311']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.venv
| build
| dist
)/
'''
9.2 MyPy (Type Checker)
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.mypy.overrides](tool.mypy.overrides.md)
module = "pandas.*"
ignore_missing_imports = true
9.3 Pytest
[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-ra -q --strict-markers"
testpaths = [
"tests",
]
markers = [
"slow: marks tests as slow",
"integration: integration tests",
]
9.4 Ruff (Linter & Formatter)
[tool.ruff]
line-length = 88
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]
ignore = ["E501"]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
10 Vergleich der Tools
10.1 Feature-Matrix
| Feature | pip + venv | pip-tools | Poetry | Hatch |
|---|---|---|---|---|
| Dependency Resolution | ✅ Basis | ✅ Gut | ✅ Sehr gut | ✅ Sehr gut |
| Lock-Files | ❌ | ✅ | ✅ | ✅ |
| Virtual Env Management | ⚠️ Manuell | ⚠️ Manuell | ✅ Automatisch | ✅ Automatisch |
| Packaging | ⚠️ Separat | ⚠️ Separat | ✅ Integriert | ✅ Integriert |
| Publishing | ⚠️ Separat | ⚠️ Separat | ✅ Integriert | ✅ Integriert |
| pyproject.toml | ✅ | ✅ | ✅ | ✅ |
| Lernkurve | ✅ Niedrig | ✅ Niedrig | ⚠️ Mittel | ⚠️ Mittel |
| Performance | ✅ | ✅ | ⚠️ Langsamer | ✅ |
| Ökosystem-Integration | ✅ Maximal | ✅ Gut | ⚠️ Eigenes Ökosystem | ✅ Gut |
10.2 Wann was verwenden?
pip + venv:
- Einfache Projekte ohne komplexe Dependencies
- CI/CD-Pipelines (minimale Abhängigkeiten)
- Lern-/Tutorial-Projekte
- Schnelle Prototypen
pip-tools:
- Mittlere bis große Projekte
- Wenn exakte Reproduzierbarkeit wichtig ist
- Wenn man bei pip-Ökosystem bleiben will
- CI/CD mit deterministischen Builds
Poetry:
- Neue Projekte von Grund auf
- Library-Entwicklung mit Veröffentlichung
- Wenn All-in-One-Lösung gewünscht
- Team-Projekte mit standardisiertem Workflow
- Wenn Virtual Env automatisch verwaltet werden soll
Hatch:
- Moderne Alternative zu Poetry
- Schnellere Builds
- Mehrere Python-Versionen testen
- Wenn man mehr Kontrolle als Poetry will
11 Best Practices
11.1 Allgemeine Empfehlungen
# ✅ DO: Virtual Environments nutzen
python -m venv venv
source venv/bin/activate
# ✅ DO: pyproject.toml statt setup.py
# Modern und standardisiert
# ✅ DO: Lock-Files ins Git committen
# poetry.lock, requirements.txt (von pip-compile)
# ❌ DON'T: Virtual Environment committen
# Gehört in .gitignore
# ❌ DON'T: pip freeze für Dependency-Management
# Nutze pip-tools oder Poetry
# ✅ DO: Versions-Constraints spezifizieren
requests = "^2.31.0" # Poetry
requests>=2.31.0,<3.0.0 # pip
11.2 Entwicklungs-Workflow (Poetry)
# 1. Projekt initialisieren
poetry new myproject
cd myproject
# 2. Dependencies hinzufügen
poetry add requests pandas
poetry add --group dev pytest black mypy
# 3. Installation
poetry install
# 4. Entwicklung
poetry shell
# oder
poetry run python main.py
poetry run pytest
# 5. Code formatieren/testen
poetry run black .
poetry run mypy .
poetry run pytest
# 6. Vor Commit: Lock-File aktualisieren falls nötig
poetry lock --no-update
# 7. Packaging
poetry build
poetry publish
11.3 CI/CD Integration
GitHub Actions mit Poetry:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: 1.6.0
- name: Install dependencies
run: poetry install
- name: Run tests
run: poetry run pytest
Mit pip-tools:
- name: Install dependencies
run: |
pip install pip-tools
pip-sync requirements.txt
- name: Run tests
run: pytest
12 Migration zwischen Tools
12.1 Von requirements.txt zu Poetry
# 1. Poetry initialisieren
poetry init
# 2. Dependencies aus requirements.txt importieren
# (manuell in pyproject.toml übertragen)
# 3. Oder mit poetry add
cat requirements.txt | grep -v "#" | xargs poetry add
# 4. Lock-File generieren
poetry lock
12.2 Von Poetry zu pip-tools
# 1. Aus pyproject.toml extrahieren
poetry export -f requirements.txt --output requirements.txt
# 2. requirements.in erstellen (manuell vereinfachen)
# Nur direkte Dependencies ohne Versionen/Hashes
# 3. Neu kompilieren
pip-compile requirements.in
13 PyInstaller: Standalone Executables – Erstellung einer ausführbaren Datei
Python-Anwendungen als ausführbare Dateien (.exe, .app) verteilen – ohne Python-Installation beim Endnutzer.
13.1 Konzept
PyInstaller bündelt Python-Code + Interpreter + Dependencies in eine ausführbare Datei.
Vorteile:
- ✅ Keine Python-Installation nötig
- ✅ Einfache Distribution
- ✅ Verschleierung des Source-Codes (minimal)
Nachteile:
- ❌ Große Dateigröße (50-200 MB)
- ❌ Langsamer Start
- ❌ Plattform-spezifisch (.exe muss unter Windows erstellt werden)
13.2 Windows: auto-py-to-exe
Es wird auto-py-to-exe verwendet. Es handelt sich um eine GUI für PyInstaller und ein paar Extras (siehe stackoverflow.com). Weitere Infos zum Paket unter pypi.org
- Installation
pip install auto-py-to-exe
- Ausführen:
auto-py-to-exe
- Konfigurationsdatei
auto-py-to-exe_settings.jsonaus Projekt laden - Konvertieren beginnen
warning
Zum Schluss muss der Ordner data, der sich im Ordner _internal befindet, eine Ebene höher geschoben werden, sodass er sich im gleichen Ordner wie die .exe befindet.
13.3 macOS: PyInstaller CLI
- Installation
pip install pyinstaller
- App-Bundle erstellen
pyinstaller --workpath /Users/cgroening/temp/PyInstaller/build \
--distpath /Users/cgroening/temp/PyInstaller/dist DoneZilla.spec
Zum Debuggen App über Terminal ausführen (um Fehlermeldungen sehen zu können):
open '/Users/cgroening/temp/PyInstaller/dist/DoneZilla.app'
- Richtiges Verhalten prüfen: In der
DoneZilla.specist eingestellt, dass die Qt-Dateien NICHT im App-Bundle enthalten sind. Für LGPL-Konformität werden die Qt-Dateien aus dem Ordnerexternal_libsgeladen. Das heißt, sollte der Ordner nicht existieren oder anders heißen, darf die App nicht starten. - Ordner
external_libsmit den UnterordnernPySide6undshiboken6in den gleichen Ordner wie das App-Bundle platzieren - Ordner
external_libsaufräumen, sodass er nur benötigte Dateien enthält, um die Größe zu reduzieren
13.4 .spec-Datei Konfiguration
# myapp.spec
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[('config.json', '.'), ('images/', 'images')],
hiddenimports=['pkg_resources'],
hookspath=[],
runtime_hooks=[],
excludes=['tkinter'],
)
pyz = PYZ(a.pure, a.zipped_data)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='MyApp',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False, # Für GUI-Apps
icon='app.ico'
)
13.5 Datendateien inkludieren
# Einzelne Datei
pyinstaller --add-data "config.json:." script.py
# Ordner (Linux/Mac)
pyinstaller --add-data "images:images" script.py
# Ordner (Windows)
pyinstaller --add-data "images;images" script.py
13.6 Häufige Probleme
Problem: “ModuleNotFoundError” nach Build
# Hidden Imports manuell angeben
pyinstaller --hidden-import=pkg_resources script.py
Problem: Zu große .exe
# UPX Kompression (optional)
pyinstaller --upx-dir=/path/to/upx script.py
# Module ausschließen
pyinstaller --exclude-module tkinter script.py
Problem: Antivirus False Positive
- Signiere die .exe digital (Windows)
- Reiche bei Antivirus-Herstellern als False Positive ein
13.7 Alternativen
| Tool | Platform | Besonderheit |
|---|---|---|
| PyInstaller | All | Standard, feature-reich |
| auto-py-to-exe | Windows | GUI für PyInstaller |
| cx_Freeze | All | Alternative zu PyInstaller |
| py2app | macOS | macOS-spezifisch |
| py2exe | Windows | Windows-only, veraltet |
| Nuitka | All | Kompiliert zu C (schneller) |
| PyOxidizer | All | Rust-basiert, modern |
13.8 Best Practices
✅ DO:
- Teste die .exe auf sauberem System
- Verwende .spec-Datei für komplexe Builds
- Versioniere die .spec-Datei
- Erstelle auf Target-Platform (Windows .exe auf Windows)
- Verwende Virtual Environment für saubere Dependencies
❌ DON’T:
- Cross-compile nicht (Windows → Linux funktioniert nicht)
- Zu viele Dependencies (Dateigröße)
- Sensitive Daten im Binary (leicht extrahierbar)
14 Zusammenfassung
| Tool | Zweck | Empfohlen für |
|---|---|---|
venv | Virtual Environments | Alle Projekte |
pip | Paket-Installation | Basis-Tool |
pip-tools | Dependency-Pinning | Deterministische Builds |
| Poetry | All-in-One Management | Neue Projekte, Libraries |
| Hatch | Alternative zu Poetry | Moderne Projekte, Multi-Python |
pyproject.toml | Standard-Konfiguration | Alle modernen Projekte |
Moderne Empfehlung:
- Einfache Projekte:
venv+pip+pyproject.toml - Professionelle Projekte:
pip-toolsoder Poetry - Library-Entwicklung: Poetry oder Hatch
- Legacy-Migration: Schrittweise zu
pyproject.tomlwechseln
Kernprinzip: Nutze Lock-Files für reproduzierbare Builds, trenne abstrakte von konkreten Dependencies, und setze auf standardisiertes pyproject.toml statt proprietärer Konfigurationen.
Disassembler, Syntax Tree und Flow Graph
Python bietet mächtige Werkzeuge zur Analyse und Transformation von Code, insbesondere auf niedriger Ebene. In dieser Referenz betrachten wir drei wichtige Konzepte: den Disassembler, den Syntaxbaum (AST) und den Kontrollflussgraphen.
1 Disassembler mit dis
Das Modul dis zeigt die Bytecode-Instruktionen, die vom Python-Interpreter ausgeführt werden. Es ist nützlich zur Fehleranalyse, Optimierung und beim Verständnis, wie Python intern arbeitet.
1.1 Beispiel: Disassemblieren einer Funktion
import dis
def greet(name):
return 'Hello ' + name
# Ausgabe des Bytecodes
dis.dis(greet)
Erklärung:
dis.dis()zeigt die Bytecode-Instruktionen.- Man sieht z. B.
LOAD_CONST,LOAD_FAST,BINARY_ADD,RETURN_VALUE.
2 Syntax Tree (AST – Abstract Syntax Tree)
Das ast-Modul ermöglicht es, Python-Code als abstrakten Syntaxbaum zu analysieren und zu manipulieren. So kann man z. B. Programme transformieren oder statisch untersuchen.
2.1 Beispiel: AST eines Ausdrucks erzeugen
import ast
source_code = 'x + 2'
tree = ast.parse(source_code, mode='eval')
# AST-Struktur anzeigen
print(ast.dump(tree, indent=4))
Erklärung:
ast.parse()erzeugt den Syntaxbaum.ast.dump()gibt ihn als lesbare Struktur aus.- Ideal zur statischen Codeanalyse oder Code-Transformation.
2.2 Beispiel: AST eines Moduls mit Funktion
code = '''
def square(x):
return x * x
'''
parsed = ast.parse(code)
print(ast.dump(parsed, indent=4))
3 AST Traversierung mit NodeVisitor
Man kann mit ast.NodeVisitor eigene Besucher-Klassen schreiben, um gezielt über den Baum zu laufen.
class FunctionLister(ast.NodeVisitor):
def visit_FunctionDef(self, node):
print(f'Found function: {node.name}')
self.generic_visit(node)
tree = ast.parse('''
def hello(): pass
def goodbye(): pass
''')
FunctionLister().visit(tree)
Erklärung:
visit_FunctionDefwird bei jeder Funktionsdefinition aufgerufen.generic_visit()sorgt für rekursive Durchläufe.
4 AST-Modifikation mit NodeTransformer
Mit NodeTransformer kann man den AST direkt verändern:
class ConstFolder(ast.NodeTransformer):
def visit_BinOp(self, node):
self.generic_visit(node)
if (isinstance(node.left, ast.Constant) and
isinstance(node.right, ast.Constant)):
return ast.Constant(value=eval(compile(ast.Expression(node), '', 'eval')))
return node
tree = ast.parse('x = 2 + 3')
new_tree = ConstFolder().visit(tree)
print(ast.dump(new_tree, indent=4))
Erklärung:
- Diese Transformation ersetzt
2 + 3durch5. - Praktisch für Optimierungen.
5 Flow Graph / Kontrollflussgraph
Python bietet keine Standardbibliothek für Flow-Graphs, aber man kann z. B. bytecode, cfg, oder externe Tools wie pycfg oder astmonkey verwenden.
Ein einfacher Kontrollflussgraph kann durch manuelle Analyse des AST oder Bytecodes entstehen.
5.1 Beispiel: Kontrolle über Sprungbefehle im Bytecode
import dis
def conditional(x):
if x > 0:
return 'positive'
else:
return 'non-positive'
dis.dis(conditional)
Erklärung:
- Man erkennt
POP_JUMP_IF_FALSEund entsprechende Sprungziele. - Diese lassen sich zu einem Flow Graph zusammensetzen.
5.2 Tools für CFG-Visualisierung
Diese Tools erzeugen z. B. DOT-Dateien, aus denen mit Graphviz Diagramme generiert werden können.
6 Fazit
| Thema | Zweck |
|---|---|
dis | Analyse des Bytecodes |
ast | Strukturierte Analyse und Manipulation von Code |
| Flow Graph | Analyse von Kontrollstrukturen und Ablaufpfaden |
Diese Werkzeuge sind besonders nützlich für Debugging, Optimierung, Sicherheitsanalysen oder eigene Python-Compiler/Interpreter-Projekte.
Netzwerk und APIs
HTTP-Requests sind das Rückgrat moderner Web-APIs. Dieses Kapitel behandelt synchrone Requests mit requests und asynchrone Requests mit aiohttp sowie Best Practices für API-Kommunikation.
1 Die requests-Bibliothek
requests ist die Standard-Bibliothek für HTTP-Requests in Python – einfach, elegant und mächtig.
1.1 Installation
pip install requests
1.2 Grundlegende GET-Requests
import requests
# Einfacher GET-Request
response = requests.get('https://api.github.com')
# Response-Attribute
print(response.status_code) # 200
print(response.text) # Response-Body als String
print(response.content) # Response-Body als Bytes
print(response.json()) # JSON automatisch parsen
print(response.headers) # Response-Headers
print(response.url) # Finale URL (nach Redirects)
print(response.encoding) # Encoding (z.B. 'utf-8')
1.3 Status-Code prüfen
response = requests.get('https://api.github.com/user')
# Manuell prüfen
if response.status_code == 200:
print('Success!')
elif response.status_code == 404:
print('Not Found')
# Automatisch Exception werfen bei Fehler (empfohlen)
response.raise_for_status() # Wirft HTTPError bei 4xx/5xx
# Mit try-except
try:
response = requests.get('https://api.example.com/data')
response.raise_for_status()
data = response.json()
except requests.exceptions.HTTPError as e:
print(f'HTTP Error: {e}')
except requests.exceptions.RequestException as e:
print(f'Request failed: {e}')
2 HTTP-Methoden
2.1 GET – Daten abrufen
# Mit Query-Parametern
params = {'q': 'python', 'sort': 'stars'}
response = requests.get('https://api.github.com/search/repositories',
params=params)
# URL: https://api.github.com/search/repositories?q=python&sort=stars
print(response.json()['total_count'])
2.2 POST – Daten senden
# JSON-Daten senden
data = {'title': 'foo', 'body': 'bar', 'userId': 1}
response = requests.post('https://jsonplaceholder.typicode.com/posts',
json=data)
print(response.status_code) # 201 Created
print(response.json())
# Formulardaten senden (application/x-www-form-urlencoded)
form_data = {'username': 'john', 'password': 'secret'}
response = requests.post('https://example.com/login', data=form_data)
2.3 PUT – Daten aktualisieren
# Gesamtes Objekt ersetzen
data = {'title': 'Updated Title', 'body': 'Updated Body', 'userId': 1}
response = requests.put('https://jsonplaceholder.typicode.com/posts/1',
json=data)
2.4 PATCH – Teilaktualisierung
# Nur einzelne Felder aktualisieren
data = {'title': 'New Title'}
response = requests.patch('https://jsonplaceholder.typicode.com/posts/1',
json=data)
2.5 DELETE – Ressource löschen
response = requests.delete('https://jsonplaceholder.typicode.com/posts/1')
print(response.status_code) # 200 oder 204 No Content
3 Headers und Authentication
3.1 Custom Headers
headers = {
'User-Agent': 'MyApp/1.0',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.get('https://api.example.com/data', headers=headers)
3.2 Basic Authentication
from requests.auth import HTTPBasicAuth
# Variante 1: Mit auth-Parameter
response = requests.get('https://api.example.com/user',
auth=('username', 'password'))
# Variante 2: Explizit mit HTTPBasicAuth
auth = HTTPBasicAuth('username', 'password')
response = requests.get('https://api.example.com/user', auth=auth)
3.3 Bearer Token (API Keys)
# Typisch für moderne APIs (OAuth, JWT)
token = 'your_api_token_here'
headers = {'Authorization': f'Bearer {token}'}
response = requests.get('https://api.github.com/user', headers=headers)
3.4 API Key in Query-Parameter
# Manche APIs nutzen Query-Parameter für Keys
params = {'api_key': 'your_api_key'}
response = requests.get('https://api.example.com/data', params=params)
4 Sessions – Persistente Verbindungen
Sessions ermöglichen wiederverwendbare Konfiguration und Connection-Pooling.
4.1 Session-Grundlagen
# Ohne Session (neue Verbindung pro Request)
response1 = requests.get('https://api.example.com/data')
response2 = requests.get('https://api.example.com/data')
# Mit Session (wiederverwendete Verbindung)
with requests.Session() as session:
# Headers gelten für alle Requests in der Session
session.headers.update({'Authorization': 'Bearer token123'})
response1 = session.get('https://api.example.com/data')
response2 = session.get('https://api.example.com/users')
# Beide Requests nutzen denselben Header
4.2 Session mit Cookies
session = requests.Session()
# Login (setzt Cookies)
login_data = {'username': 'user', 'password': 'pass'}
session.post('https://example.com/login', data=login_data)
# Weitere Requests nutzen automatisch die Cookies
response = session.get('https://example.com/dashboard')
# Cookies manuell setzen
session.cookies.set('session_id', 'abc123', domain='example.com')
4.3 Session-Konfiguration
session = requests.Session()
# Default-Parameter für alle Requests
session.headers.update({'User-Agent': 'MyApp/1.0'})
session.params = {'api_key': 'key123'} # Query-Parameter
session.verify = True # SSL-Zertifikate prüfen (Standard)
session.timeout = 10 # Timeout in Sekunden
# Alle Requests in Session nutzen diese Einstellungen
response = session.get('https://api.example.com/data')
5 Timeouts und Retries
5.1 Timeouts
# Timeout in Sekunden
try:
response = requests.get('https://api.example.com/slow', timeout=5)
except requests.exceptions.Timeout:
print('Request timed out')
# Verschiedene Timeouts für Connect und Read
response = requests.get('https://api.example.com/data',
timeout=(3.05, 10)) # (connect, read)
# Kein Timeout (nicht empfohlen!)
response = requests.get('https://api.example.com/data', timeout=None)
5.2 Automatische Retries
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Retry-Strategie konfigurieren
retry_strategy = Retry(
total=3, # Maximale Anzahl Retries
backoff_factor=1, # Wartezeit: 1s, 2s, 4s, ...
status_forcelist=[429, 500, 502, 503, 504], # Bei diesen Status-Codes
allowed_methods=["GET", "POST"] # Nur für diese Methoden
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.mount("http://", adapter)
session.mount("https://", adapter)
# Request mit automatischen Retries
response = session.get('https://api.example.com/data')
6 Datei-Uploads und Downloads
6.1 Datei hochladen
# Einfacher Upload
with open('document.pdf', 'rb') as f:
files = {'file': f}
response = requests.post('https://api.example.com/upload', files=files)
# Mit Dateinamen und MIME-Type
with open('image.jpg', 'rb') as f:
files = {
'file': ('custom_name.jpg', f, 'image/jpeg')
}
response = requests.post('https://api.example.com/upload', files=files)
# Mehrere Dateien
files = {
'file1': open('doc1.pdf', 'rb'),
'file2': open('doc2.pdf', 'rb')
}
response = requests.post('https://api.example.com/upload', files=files)
6.2 Datei herunterladen
# Kleine Dateien (komplette Response im RAM)
response = requests.get('https://example.com/image.jpg')
with open('downloaded_image.jpg', 'wb') as f:
f.write(response.content)
# Große Dateien (streaming)
response = requests.get('https://example.com/largefile.zip', stream=True)
with open('largefile.zip', 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
# Mit Progress Bar (benötigt tqdm)
from tqdm import tqdm
response = requests.get('https://example.com/largefile.zip', stream=True)
total_size = int(response.headers.get('content-length', 0))
with open('largefile.zip', 'wb') as f, tqdm(
total=total_size, unit='B', unit_scale=True
) as pbar:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
pbar.update(len(chunk))
7 JSON-Handling
7.1 JSON empfangen
response = requests.get('https://api.github.com/users/torvalds')
# JSON automatisch parsen
data = response.json()
print(data['name']) # Linus Torvalds
print(data['location']) # Portland
# Fehlerbehandlung
try:
data = response.json()
except requests.exceptions.JSONDecodeError:
print('Response is not valid JSON')
7.2 JSON senden
# json-Parameter setzt automatisch Content-Type: application/json
payload = {
'name': 'John Doe',
'email': 'john@example.com',
'age': 30
}
response = requests.post('https://api.example.com/users', json=payload)
# Äquivalent zu:
import json
headers = {'Content-Type': 'application/json'}
response = requests.post('https://api.example.com/users',
data=json.dumps(payload),
headers=headers)
8 Error Handling
8.1 Exception-Hierarchie
from requests.exceptions import (
RequestException, # Basis-Exception
ConnectionError, # Verbindungsfehler
Timeout, # Timeout
HTTPError, # 4xx/5xx Status-Codes
TooManyRedirects, # Zu viele Redirects
URLRequired, # URL fehlt
)
def safe_request(url):
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
except Timeout:
print('Request timed out')
except ConnectionError:
print('Failed to connect')
except HTTPError as e:
print(f'HTTP Error: {e.response.status_code}')
except RequestException as e:
print(f'Request failed: {e}')
return None
8.2 Status-Code Handling
response = requests.get('https://api.example.com/data')
# Kategorien prüfen
if response.ok: # 200-299
print('Success')
elif 400 <= response.status_code < 500:
print('Client error')
elif 500 <= response.status_code < 600:
print('Server error')
# Spezifische Codes
status_handlers = {
200: lambda: print('OK'),
201: lambda: print('Created'),
400: lambda: print('Bad Request'),
401: lambda: print('Unauthorized'),
404: lambda: print('Not Found'),
500: lambda: print('Server Error')
}
handler = status_handlers.get(response.status_code)
if handler:
handler()
9 Asynchrone Requests mit aiohttp
Für viele parallele Requests ist aiohttp deutlich schneller als requests.
9.1 Installation
pip install aiohttp
9.2 Einfacher GET-Request
import aiohttp
import asyncio
async def fetch_data(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
print(f'Status: {response.status}')
data = await response.json()
return data
# Ausführen
asyncio.run(fetch_data('https://api.github.com'))
9.3 Mehrere parallele Requests
import aiohttp
import asyncio
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.json()
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
return results
# Ausführen
urls = [
'https://api.github.com/users/torvalds',
'https://api.github.com/users/gvanrossum',
'https://api.github.com/users/kennethreitz',
]
results = asyncio.run(fetch_all(urls))
for result in results:
print(result['name'])
9.4 POST mit aiohttp
async def post_data(url, payload):
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload) as response:
return await response.json()
payload = {'title': 'Test', 'body': 'Content'}
result = asyncio.run(post_data('https://jsonplaceholder.typicode.com/posts',
payload))
9.5 Headers und Authentication
async def fetch_with_auth(url, token):
headers = {'Authorization': f'Bearer {token}'}
async with aiohttp.ClientSession(headers=headers) as session:
async with session.get(url) as response:
return await response.json()
# Timeout
timeout = aiohttp.ClientTimeout(total=10)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url) as response:
data = await response.json()
9.6 Session wiederverwenden
async def fetch_multiple(urls):
# Session nur einmal erstellen (effizienter)
async with aiohttp.ClientSession() as session:
tasks = []
for url in urls:
task = asyncio.create_task(fetch_url(session, url))
tasks.append(task)
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
async def fetch_url(session, url):
try:
async with session.get(url) as response:
return await response.json()
except Exception as e:
return {'error': str(e)}
9.7 Rate Limiting mit Semaphore
import asyncio
import aiohttp
async def fetch_with_limit(session, url, semaphore):
async with semaphore: # Maximale parallele Requests begrenzen
async with session.get(url) as response:
return await response.json()
async def fetch_all_limited(urls, max_concurrent=5):
semaphore = asyncio.Semaphore(max_concurrent)
async with aiohttp.ClientSession() as session:
tasks = [fetch_with_limit(session, url, semaphore) for url in urls]
results = await asyncio.gather(*tasks)
return results
# Maximal 5 parallele Requests
urls = [f'https://api.example.com/item/{i}' for i in range(100)]
results = asyncio.run(fetch_all_limited(urls, max_concurrent=5))
10 Best Practices
10.1 Immer Timeouts setzen
# ❌ Schlecht: Kein Timeout (kann ewig hängen)
response = requests.get('https://api.example.com/data')
# ✅ Gut: Timeout definieren
response = requests.get('https://api.example.com/data', timeout=10)
# ✅ Besser: Verschiedene Timeouts
response = requests.get('https://api.example.com/data',
timeout=(3, 10)) # connect, read
10.2 Sessions für mehrere Requests
# ❌ Schlecht: Neue Verbindung pro Request
for i in range(100):
response = requests.get(f'https://api.example.com/item/{i}')
# ✅ Gut: Session wiederverwendet Verbindung
with requests.Session() as session:
for i in range(100):
response = session.get(f'https://api.example.com/item/{i}')
10.3 Error Handling nicht vergessen
# ✅ Immer raise_for_status() verwenden
try:
response = requests.get('https://api.example.com/data')
response.raise_for_status() # Exception bei 4xx/5xx
data = response.json()
except requests.exceptions.RequestException as e:
logger.error(f'API request failed: {e}')
10.4 User-Agent setzen
# Viele APIs blockieren Requests ohne User-Agent
headers = {'User-Agent': 'MyApp/1.0 (contact@example.com)'}
response = requests.get('https://api.example.com/data', headers=headers)
10.5 Secrets nicht im Code
# ❌ Schlecht: API-Key im Code
API_KEY = 'sk_live_abc123xyz'
# ✅ Gut: Aus Umgebungsvariablen
import os
API_KEY = os.getenv('API_KEY')
# ✅ Oder aus .env-Datei (mit python-dotenv)
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv('API_KEY')
10.6 Rate Limiting respektieren
import time
from datetime import datetime, timedelta
class RateLimiter:
def __init__(self, max_calls, period):
self.max_calls = max_calls
self.period = period # in seconds
self.calls = []
def wait_if_needed(self):
now = datetime.now()
# Alte Calls entfernen
self.calls = [call for call in self.calls
if now - call < timedelta(seconds=self.period)]
if len(self.calls) >= self.max_calls:
sleep_time = (self.calls[0] + timedelta(seconds=self.period) - now).total_seconds()
time.sleep(sleep_time)
self.calls = []
self.calls.append(now)
# Verwendung: Max 10 Requests pro Minute
limiter = RateLimiter(max_calls=10, period=60)
for i in range(100):
limiter.wait_if_needed()
response = requests.get(f'https://api.example.com/item/{i}')
11 Praxisbeispiele
11.1 GitHub API Wrapper
class GitHubAPI:
BASE_URL = 'https://api.github.com'
def __init__(self, token=None):
self.session = requests.Session()
if token:
self.session.headers.update({
'Authorization': f'Bearer {token}',
'Accept': 'application/vnd.github.v3+json'
})
self.session.headers.update({'User-Agent': 'MyGitHubApp/1.0'})
def get_user(self, username):
response = self.session.get(f'{self.BASE_URL}/users/{username}')
response.raise_for_status()
return response.json()
def get_repos(self, username):
response = self.session.get(f'{self.BASE_URL}/users/{username}/repos')
response.raise_for_status()
return response.json()
def close(self):
self.session.close()
# Verwendung
api = GitHubAPI(token='ghp_xxxxx')
user = api.get_user('torvalds')
repos = api.get_repos('torvalds')
api.close()
11.2 REST API mit Pagination
def fetch_all_pages(base_url, params=None):
"""Holt alle Seiten einer paginierten API"""
all_data = []
page = 1
with requests.Session() as session:
while True:
params_with_page = {**(params or {}), 'page': page}
response = session.get(base_url, params=params_with_page)
response.raise_for_status()
data = response.json()
if not data: # Keine weiteren Daten
break
all_data.extend(data)
page += 1
return all_data
# Verwendung
all_users = fetch_all_pages('https://api.example.com/users')
11.3 Async Batch Processing
import aiohttp
import asyncio
from typing import List, Dict, Any
async def process_batch(items: List[str],
batch_size: int = 10) -> List[Dict[Any, Any]]:
"""Verarbeitet Items in Batches asynchron"""
results = []
async with aiohttp.ClientSession() as session:
for i in range(0, len(items), batch_size):
batch = items[i:i + batch_size]
tasks = [fetch_item(session, item) for item in batch]
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
results.extend(batch_results)
return results
async def fetch_item(session, item_id):
url = f'https://api.example.com/items/{item_id}'
try:
async with session.get(url, timeout=5) as response:
return await response.json()
except Exception as e:
return {'error': str(e), 'item_id': item_id}
# Verwendung
item_ids = list(range(1, 101))
results = asyncio.run(process_batch(item_ids, batch_size=10))
11.4 Retry mit exponential backoff
import time
import random
def retry_with_backoff(func, max_retries=3, base_delay=1):
"""Führt Funktion mit exponential backoff retry aus"""
for attempt in range(max_retries):
try:
return func()
except requests.exceptions.RequestException as e:
if attempt == max_retries - 1: # Letzter Versuch
raise
# Exponential backoff mit Jitter
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
print(f'Retry {attempt + 1}/{max_retries} after {delay:.2f}s')
time.sleep(delay)
# Verwendung
def make_request():
response = requests.get('https://api.example.com/data', timeout=5)
response.raise_for_status()
return response.json()
data = retry_with_backoff(make_request, max_retries=3)
12 Vergleich: requests vs. aiohttp
| Kriterium | requests | aiohttp |
|---|---|---|
| Synchron/Async | Synchron (blocking) | Asynchron (non-blocking) |
| Performance (single) | ✅ Ausreichend | ⚠️ Etwas Overhead |
| Performance (parallel) | ❌ Langsam (sequenziell) | ✅ Sehr schnell |
| Einfachheit | ✅ Sehr einfach | ⚠️ Async-Kenntnisse nötig |
| Use Cases | Normale Scripts, CLI-Tools | Web Scraping, viele APIs |
| HTTP/2 Support | ❌ | ✅ (mit aioh2) |
| Ecosystem | ✅ Riesig | ✅ Wachsend |
Faustregel:
requests: Für normale Scripts, wenige (<10) Requests, Einfachheitaiohttp: Für viele parallele Requests (>50), Web Scraping, Performance-kritisch
13 Zusammenfassung
| Thema | Verwendung |
|---|---|
requests.get() | Daten von API abrufen |
requests.post() | Daten an API senden |
response.json() | JSON automatisch parsen |
response.raise_for_status() | Exception bei Fehler-Status werfen |
Session() | Verbindungen wiederverwenden |
timeout | Maximale Wartezeit definieren |
headers | Custom Headers, Authentication |
aiohttp | Asynchrone Requests für hohe Parallelität |
Kernprinzipien:
- Immer Timeouts setzen
- Sessions für mehrere Requests nutzen
- Error Handling nicht vergessen (
raise_for_status()) - Rate Limits respektieren
- API-Keys aus Umgebungsvariablen laden
- Für viele parallele Requests
aiohttpverwenden
Datenbanken
Datenbanken sind zentral für die meisten Anwendungen. Dieses Kapitel behandelt SQLite für einfache Datenbankoperationen und SQLAlchemy als mächtiges ORM (Object-Relational Mapping) für komplexere Szenarien.
1 SQLite – Eingebaute Datenbank
SQLite ist eine leichtgewichtige, dateibasierte Datenbank, die Teil der Python-Standardbibliothek ist.
1.1 Verbindung herstellen
import sqlite3
# Datenbank erstellen/öffnen (Datei wird automatisch erstellt)
conn = sqlite3.connect('mydatabase.db')
# In-Memory-Datenbank (nur im RAM, für Tests)
conn = sqlite3.connect(':memory:')
# Cursor erstellen (führt SQL-Befehle aus)
cursor = conn.cursor()
# Verbindung schließen
conn.close()
1.2 Mit Context Manager (empfohlen)
# Automatisches Commit und Close
with sqlite3.connect('mydatabase.db') as conn:
cursor = conn.cursor()
# Datenbankoperationen
cursor.execute('SELECT * FROM users')
# conn.close() wird automatisch aufgerufen
1.3 Tabelle erstellen
import sqlite3
conn = sqlite3.connect('mydatabase.db')
cursor = conn.cursor()
# Tabelle erstellen
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
1.4 Daten einfügen
# Einzelner Datensatz
cursor.execute(
'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
('Alice', 'alice@example.com', 30)
)
# Mehrere Datensätze
users_data = [
('Bob', 'bob@example.com', 25),
('Charlie', 'charlie@example.com', 35),
('Diana', 'diana@example.com', 28)
]
cursor.executemany(
'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
users_data
)
conn.commit()
Wichtig: Verwende immer ? Platzhalter (nie String-Formatierung) um SQL-Injection zu vermeiden!
# ❌ NIEMALS SO (SQL-Injection-Risiko!)
name = "Alice'; DROP TABLE users; --"
cursor.execute(f"INSERT INTO users (name) VALUES ('{name}')")
# ✅ Immer Platzhalter verwenden
cursor.execute('INSERT INTO users (name) VALUES (?)', (name,))
1.5 Daten abfragen
# Alle Zeilen
cursor.execute('SELECT * FROM users')
all_users = cursor.fetchall()
for user in all_users:
print(user) # Tuple: (id, name, email, age, created_at)
# Einzelne Zeile
cursor.execute('SELECT * FROM users WHERE id = ?', (1,))
user = cursor.fetchone()
print(user)
# Bestimmte Anzahl
cursor.execute('SELECT * FROM users')
first_five = cursor.fetchmany(5)
# Mit WHERE-Bedingung
cursor.execute('SELECT name, email FROM users WHERE age > ?', (25,))
results = cursor.fetchall()
1.6 Row Factory – Zugriff als Dictionary
import sqlite3
conn = sqlite3.connect('mydatabase.db')
# Row Factory aktivieren
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE id = ?', (1,))
user = cursor.fetchone()
# Zugriff per Spaltenname (wie Dictionary)
print(user['name'])
print(user['email'])
# Oder als Dictionary konvertieren
user_dict = dict(user)
print(user_dict)
1.7 Daten aktualisieren
# UPDATE
cursor.execute(
'UPDATE users SET age = ? WHERE name = ?',
(31, 'Alice')
)
# Anzahl betroffener Zeilen
print(f'Updated {cursor.rowcount} rows')
conn.commit()
1.8 Daten löschen
# DELETE
cursor.execute('DELETE FROM users WHERE age < ?', (25,))
print(f'Deleted {cursor.rowcount} rows')
conn.commit()
# Alle Daten löschen
cursor.execute('DELETE FROM users')
conn.commit()
# Tabelle löschen
cursor.execute('DROP TABLE IF EXISTS users')
conn.commit()
1.9 Transaktionen
conn = sqlite3.connect('mydatabase.db')
cursor = conn.cursor()
try:
# Mehrere Operationen als Transaktion
cursor.execute('INSERT INTO users (name, email) VALUES (?, ?)',
('Eve', 'eve@example.com'))
cursor.execute('UPDATE users SET age = ? WHERE name = ?',
(40, 'Alice'))
# Alles erfolgreich → Commit
conn.commit()
except sqlite3.Error as e:
# Fehler → Rollback
conn.rollback()
print(f'Transaction failed: {e}')
finally:
conn.close()
2 SQLite mit Context Manager Pattern
import sqlite3
from contextlib import contextmanager
@contextmanager
def get_db_connection(db_path):
"""Context Manager für Datenbankverbindungen"""
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
# Verwendung
with get_db_connection('mydatabase.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM users')
users = cursor.fetchall()
# Automatisches Commit/Rollback/Close
3 SQLAlchemy – ORM und mehr
SQLAlchemy ist das mächtigste Datenbank-Toolkit für Python mit zwei Hauptkomponenten:
- Core: Low-level SQL-Abstraction
- ORM: High-level Object-Relational Mapping
3.1 Installation
pip install sqlalchemy
3.2 Engine erstellen
from sqlalchemy import create_engine
# SQLite
engine = create_engine('sqlite:///mydatabase.db', echo=True)
# PostgreSQL
engine = create_engine('postgresql://user:password@localhost/dbname')
# MySQL
engine = create_engine('mysql+pymysql://user:password@localhost/dbname')
# In-Memory SQLite
engine = create_engine('sqlite:///:memory:')
3.3 Deklarative Basis und Models
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import declarative_base
from datetime import datetime
# Basis-Klasse für alle Models
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
email = Column(String(100), unique=True, nullable=False)
age = Column(Integer)
created_at = Column(DateTime, default=datetime.utcnow)
def __repr__(self):
return f"<User(name='{self.name}', email='{self.email}')>"
# Tabellen erstellen
Base.metadata.create_all(engine)
3.4 Session erstellen
from sqlalchemy.orm import sessionmaker
# Session-Factory erstellen
Session = sessionmaker(bind=engine)
# Session-Instanz
session = Session()
# Mit Context Manager (empfohlen)
from contextlib import contextmanager
@contextmanager
def get_session():
session = Session()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
# Verwendung
with get_session() as session:
user = User(name='Alice', email='alice@example.com', age=30)
session.add(user)
# Automatisches Commit/Rollback/Close
3.5 CRUD-Operationen
3.5.1 Create (Einfügen)
from sqlalchemy.orm import Session
# Einzelner Datensatz
with get_session() as session:
user = User(name='Bob', email='bob@example.com', age=25)
session.add(user)
# Commit erfolgt automatisch bei __exit__
# Mehrere Datensätze
with get_session() as session:
users = [
User(name='Charlie', email='charlie@example.com', age=35),
User(name='Diana', email='diana@example.com', age=28)
]
session.add_all(users)
3.5.2 Read (Abfragen)
with get_session() as session:
# Alle Datensätze
all_users = session.query(User).all()
# Erster Datensatz
first_user = session.query(User).first()
# Nach Primary Key
user = session.query(User).get(1) # Deprecated in SQLAlchemy 2.0
user = session.get(User, 1) # Neue Syntax
# Mit Filter
alice = session.query(User).filter_by(name='Alice').first()
# Komplexere Filter
from sqlalchemy import and_, or_
young_users = session.query(User).filter(User.age < 30).all()
results = session.query(User).filter(
and_(User.age > 25, User.age < 35)
).all()
# ORDER BY
users_sorted = session.query(User).order_by(User.age.desc()).all()
# LIMIT
top_5 = session.query(User).limit(5).all()
# COUNT
user_count = session.query(User).count()
3.5.3 Update (Aktualisieren)
with get_session() as session:
# Objekt laden und ändern
user = session.query(User).filter_by(name='Alice').first()
user.age = 31
# Änderung wird beim Commit gespeichert
# Bulk-Update
with get_session() as session:
session.query(User).filter(User.age < 25).update({'age': 25})
3.5.4 Delete (Löschen)
with get_session() as session:
# Objekt laden und löschen
user = session.query(User).filter_by(name='Bob').first()
session.delete(user)
# Bulk-Delete
with get_session() as session:
session.query(User).filter(User.age < 20).delete()
4 Relationships (Beziehungen)
4.1 One-to-Many (Eins-zu-Viele)
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(100))
# Relationship: Ein User hat viele Posts
posts = relationship('Post', back_populates='author', cascade='all, delete-orphan')
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String(200))
content = Column(String)
user_id = Column(Integer, ForeignKey('users.id'))
# Relationship: Ein Post gehört zu einem User
author = relationship('User', back_populates='posts')
Base.metadata.create_all(engine)
# Verwendung
with get_session() as session:
user = User(name='Alice')
post1 = Post(title='First Post', content='Hello World')
post2 = Post(title='Second Post', content='Another one')
user.posts.append(post1)
user.posts.append(post2)
session.add(user)
# Posts werden automatisch mit gespeichert
# Abfragen
with get_session() as session:
user = session.query(User).filter_by(name='Alice').first()
print(f'{user.name} has {len(user.posts)} posts')
for post in user.posts:
print(f' - {post.title}')
4.2 Many-to-Many (Viele-zu-Viele)
from sqlalchemy import Table
# Zwischentabelle (Association Table)
user_role_association = Table(
'user_roles',
Base.metadata,
Column('user_id', Integer, ForeignKey('users.id')),
Column('role_id', Integer, ForeignKey('roles.id'))
)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(100))
# Many-to-Many Relationship
roles = relationship('Role', secondary=user_role_association, back_populates='users')
class Role(Base):
__tablename__ = 'roles'
id = Column(Integer, primary_key=True)
name = Column(String(50))
users = relationship('User', secondary=user_role_association, back_populates='roles')
Base.metadata.create_all(engine)
# Verwendung
with get_session() as session:
admin_role = Role(name='admin')
user_role = Role(name='user')
alice = User(name='Alice')
alice.roles.extend([admin_role, user_role])
bob = User(name='Bob')
bob.roles.append(user_role)
session.add_all([alice, bob])
# Abfragen
with get_session() as session:
user = session.query(User).filter_by(name='Alice').first()
print(f'{user.name} has roles: {[r.name for r in user.roles]}')
4.3 One-to-One (Eins-zu-Eins)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(100))
# One-to-One: uselist=False
profile = relationship('UserProfile', back_populates='user', uselist=False)
class UserProfile(Base):
__tablename__ = 'user_profiles'
id = Column(Integer, primary_key=True)
bio = Column(String)
avatar_url = Column(String)
user_id = Column(Integer, ForeignKey('users.id'), unique=True)
user = relationship('User', back_populates='profile')
# Verwendung
with get_session() as session:
user = User(name='Alice')
profile = UserProfile(bio='Software Developer', avatar_url='/avatars/alice.jpg')
user.profile = profile
session.add(user)
5 Erweiterte Queries
5.1 Joins
from sqlalchemy import select
with get_session() as session:
# Implicit Join
results = session.query(User, Post).filter(User.id == Post.user_id).all()
# Explicit Join
results = session.query(User).join(Post).filter(Post.title.like('%Python%')).all()
# Left Outer Join
results = session.query(User).outerjoin(Post).all()
5.2 Aggregationen
from sqlalchemy import func
with get_session() as session:
# COUNT
user_count = session.query(func.count(User.id)).scalar()
# AVG, MIN, MAX, SUM
avg_age = session.query(func.avg(User.age)).scalar()
min_age = session.query(func.min(User.age)).scalar()
max_age = session.query(func.max(User.age)).scalar()
# GROUP BY
post_counts = session.query(
User.name,
func.count(Post.id).label('post_count')
).join(Post).group_by(User.name).all()
for name, count in post_counts:
print(f'{name}: {count} posts')
5.3 Subqueries
from sqlalchemy import select
with get_session() as session:
# Subquery: Users mit mehr als 5 Posts
subquery = (
session.query(Post.user_id, func.count(Post.id).label('post_count'))
.group_by(Post.user_id)
.having(func.count(Post.id) > 5)
.subquery()
)
active_users = session.query(User).join(
subquery, User.id == subquery.c.user_id
).all()
5.4 Eager Loading (N+1 Problem vermeiden)
from sqlalchemy.orm import joinedload, selectinload
with get_session() as session:
# ❌ N+1 Problem: Ein Query pro User + Posts
users = session.query(User).all()
for user in users:
print(user.posts) # Neuer Query für jeden User!
# ✅ Joined Load: Ein Query mit JOIN
users = session.query(User).options(joinedload(User.posts)).all()
for user in users:
print(user.posts) # Keine zusätzlichen Queries
# ✅ Select In Load: Zwei Queries (besser bei Many-to-Many)
users = session.query(User).options(selectinload(User.roles)).all()
6 Datenbank-Migrationen mit Alembic
Alembic ist das Standard-Migrationstool für SQLAlchemy.
6.1 Installation und Setup
pip install alembic
# Alembic initialisieren
alembic init alembic
6.2 Konfiguration
# alembic/env.py
from myapp.models import Base # Deine Models
from myapp.database import engine # Deine Engine
target_metadata = Base.metadata
# In alembic.ini: Connection String eintragen
# sqlalchemy.url = sqlite:///mydatabase.db
6.3 Migration erstellen
# Automatische Migration aus Model-Änderungen
alembic revision --autogenerate -m "Add user table"
# Manuelle Migration
alembic revision -m "Manual migration"
6.4 Migration ausführen
# Neueste Migration anwenden
alembic upgrade head
# Spezifische Version
alembic upgrade +1 # Eine Version vorwärts
alembic downgrade -1 # Eine Version zurück
# Migration-History
alembic history
alembic current
6.5 Beispiel-Migration
# alembic/versions/xxx_add_user_table.py
"""Add user table
Revision ID: abc123
"""
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'users',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('name', sa.String(100), nullable=False),
sa.Column('email', sa.String(100), unique=True)
)
def downgrade():
op.drop_table('users')
7 Best Practices
7.1 Session-Management
# ✅ Immer Context Manager verwenden
with get_session() as session:
# Datenbankoperationen
pass
# ❌ Session nicht manuell schließen vergessen
session = Session()
# Operationen
session.close() # Leicht zu vergessen!
7.2 Connection Pooling
# Connection Pool konfigurieren
from sqlalchemy.pool import QueuePool
engine = create_engine(
'postgresql://user:password@localhost/db',
poolclass=QueuePool,
pool_size=10, # Anzahl permanenter Verbindungen
max_overflow=20, # Zusätzliche Verbindungen bei Bedarf
pool_timeout=30, # Timeout in Sekunden
pool_recycle=3600 # Verbindungen nach 1h recyceln
)
7.3 Bulk-Operationen
# ✅ Effizient: Bulk Insert
with get_session() as session:
users = [User(name=f'User{i}', email=f'user{i}@example.com')
for i in range(1000)]
session.bulk_save_objects(users)
# ❌ Ineffizient: Einzeln einfügen
with get_session() as session:
for i in range(1000):
user = User(name=f'User{i}', email=f'user{i}@example.com')
session.add(user)
7.4 Raw SQL wenn nötig
# Manchmal ist Raw SQL performanter
with get_session() as session:
result = session.execute(
'SELECT name, COUNT(*) as count FROM users GROUP BY name'
)
for row in result:
print(row.name, row.count)
7.5 Environment-spezifische Konfiguration
import os
from sqlalchemy import create_engine
def get_engine():
env = os.getenv('ENVIRONMENT', 'development')
if env == 'production':
return create_engine(
os.getenv('DATABASE_URL'),
echo=False,
pool_size=20
)
elif env == 'testing':
return create_engine('sqlite:///:memory:', echo=False)
else: # development
return create_engine('sqlite:///dev.db', echo=True)
engine = get_engine()
8 Praktisches Beispiel: Blog-System
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
from datetime import datetime
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
email = Column(String(100), unique=True, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
posts = relationship('Post', back_populates='author', cascade='all, delete-orphan')
comments = relationship('Comment', back_populates='author')
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
content = Column(Text, nullable=False)
published = Column(DateTime, default=datetime.utcnow)
author_id = Column(Integer, ForeignKey('users.id'), nullable=False)
author = relationship('User', back_populates='posts')
comments = relationship('Comment', back_populates='post', cascade='all, delete-orphan')
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
content = Column(Text, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
post_id = Column(Integer, ForeignKey('posts.id'), nullable=False)
author_id = Column(Integer, ForeignKey('users.id'), nullable=False)
post = relationship('Post', back_populates='comments')
author = relationship('User', back_populates='comments')
# Setup
engine = create_engine('sqlite:///blog.db')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
# Beispiel-Nutzung
def create_sample_data():
with Session() as session:
# User erstellen
alice = User(username='alice', email='alice@example.com')
# Post erstellen
post = Post(
title='My First Post',
content='This is my first blog post!',
author=alice
)
# Kommentar erstellen
comment = Comment(
content='Great post!',
post=post,
author=alice
)
session.add_all([alice, post, comment])
session.commit()
def get_user_posts(username):
with Session() as session:
user = session.query(User).filter_by(username=username).first()
if user:
return [(post.title, len(post.comments)) for post in user.posts]
return []
# Ausführen
create_sample_data()
posts = get_user_posts('alice')
print(posts)
9 Zusammenfassung
| Tool/Konzept | Verwendung |
|---|---|
sqlite3 | Einfache, dateibasierte Datenbank |
cursor.execute() | SQL-Befehle ausführen |
conn.commit() | Änderungen speichern |
| SQLAlchemy Engine | Datenbankverbindung |
| SQLAlchemy ORM | Python-Objekte ↔ Datenbank |
Base | Basis-Klasse für Models |
relationship() | Beziehungen zwischen Tabellen |
session.query() | Daten abfragen |
joinedload() | Eager Loading (N+1 Problem vermeiden) |
| Alembic | Datenbank-Migrationen |
Kernprinzipien:
- Verwende
?Platzhalter bei SQLite (SQL-Injection vermeiden) - Nutze Context Manager für Verbindungen und Sessions
- SQLAlchemy ORM für komplexe Anwendungen, Raw SQL für Performance-kritische Queries
- Eager Loading bei Relationships verwenden
- Migrationen mit Alembic für Schema-Änderungen
- Connection Pooling in Produktion konfigurieren
PyScript / Python im Browser
PyScript ermöglicht es, Python-Code direkt im Browser auszuführen – ohne Server-Backend. Es basiert auf Pyodide (CPython kompiliert zu WebAssembly).
1 Was ist PyScript?
PyScript ist ein Framework von Anaconda, das Python im Browser lauffähig macht:
- Kein Server nötig: Python läuft komplett client-seitig
- WebAssembly-basiert: Nutzt Pyodide (CPython → WASM)
- HTML-Integration: Python-Code direkt in HTML einbetten
- Pakete verfügbar: NumPy, Pandas, Matplotlib, Scikit-learn, etc.
Offizielle Website: https://pyscript.net
2 Erste Schritte
2.1 Minimales Beispiel
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Demo</title>
<!-- PyScript einbinden -->
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<h1>Hello from PyScript!</h1>
<!-- Python-Code ausführen -->
<py-script>
print("Hello, World!")
print(f"2 + 2 = {2 + 2}")
</py-script>
</body>
</html>
2.2 Output anzeigen
<body>
<h1>Calculator</h1>
<div id="output"></div>
<py-script>
from pyscript import display
result = 10 * 5
display(f"10 × 5 = {result}", target="output")
</py-script>
</body>
3 Python-Pakete verwenden
3.1 Pakete deklarieren
<head>
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
<!-- Pakete konfigurieren -->
<py-config>
packages = ["numpy", "pandas", "matplotlib"]
</py-config>
</head>
<body>
<py-script>
import numpy as np
import pandas as pd
# NumPy Array
arr = np.array([1, 2, 3, 4, 5])
print(f"Mean: {np.mean(arr)}")
# Pandas DataFrame
df = pd.DataFrame({
'A': [1, 2, 3],
'B': [4, 5, 6]
})
display(df)
</py-script>
</body>
3.2 Verfügbare Pakete
PyScript/Pyodide unterstützt viele populäre Pakete:
Wissenschaftlich:
- NumPy, SciPy, Pandas
- Matplotlib, Plotly
- Scikit-learn
- SymPy
Web/Utility:
- Requests (pyodide-http)
- BeautifulSoup
- Pillow
- PyYAML
Vollständige Liste: https://pyodide.org/en/stable/usage/packages-in-pyodide.html
4 DOM-Manipulation
4.1 JavaScript-Interoperabilität
<body>
<button id="myButton">Click Me!</button>
<div id="result"></div>
<py-script>
from pyscript import document
def button_click(event):
result_div = document.querySelector("#result")
result_div.innerText = "Button was clicked!"
# Event Listener
button = document.querySelector("#myButton")
button.addEventListener("click", button_click)
</py-script>
</body>
4.2 HTML-Elemente erstellen
<body>
<div id="container"></div>
<py-script>
from pyscript import document
container = document.querySelector("#container")
# Neues Element erstellen
new_div = document.createElement("div")
new_div.innerText = "Created with Python!"
new_div.style.color = "blue"
container.appendChild(new_div)
</py-script>
</body>
5 Interaktive Anwendungen
5.1 Eingabefelder verarbeiten
<body>
<input type="text" id="nameInput" placeholder="Enter your name">
<button id="greetBtn">Greet</button>
<div id="greeting"></div>
<py-script>
from pyscript import document
def greet(event):
name_input = document.querySelector("#nameInput")
greeting_div = document.querySelector("#greeting")
name = name_input.value
greeting_div.innerText = f"Hello, {name}!"
btn = document.querySelector("#greetBtn")
btn.addEventListener("click", greet)
</py-script>
</body>
5.2 Formular mit Validierung
<body>
<form id="myForm">
<input type="number" id="num1" placeholder="Number 1">
<input type="number" id="num2" placeholder="Number 2">
<button type="submit">Calculate</button>
</form>
<div id="result"></div>
<py-script>
from pyscript import document
def calculate(event):
event.preventDefault()
num1 = float(document.querySelector("#num1").value or 0)
num2 = float(document.querySelector("#num2").value or 0)
result = num1 + num2
result_div = document.querySelector("#result")
result_div.innerText = f"{num1} + {num2} = {result}"
form = document.querySelector("#myForm")
form.addEventListener("submit", calculate)
</py-script>
</body>
6 Datenvisualisierung
6.1 Matplotlib im Browser
<head>
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-config>
packages = ["matplotlib", "numpy"]
</py-config>
</head>
<body>
<h1>Matplotlib Chart</h1>
<div id="plot"></div>
<py-script>
import matplotlib.pyplot as plt
import numpy as np
from pyscript import display
# Daten erstellen
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
# Plot erstellen
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title("Sine Wave")
ax.set_xlabel("x")
ax.set_ylabel("sin(x)")
# Im Browser anzeigen
display(fig, target="plot")
</py-script>
</body>
6.2 Interaktive Plots
<body>
<h1>Interactive Plot</h1>
<label>Frequency: <input type="range" id="freq" min="1" max="10" value="1"></label>
<div id="plot"></div>
<py-script>
import matplotlib.pyplot as plt
import numpy as np
from pyscript import document, display
def update_plot(event):
freq = int(document.querySelector("#freq").value)
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(freq * x)
plt.clf()
plt.plot(x, y)
plt.title(f"sin({freq}x)")
display(plt.gcf(), target="plot", append=False)
# Initial plot
update_plot(None)
# Event Listener
slider = document.querySelector("#freq")
slider.addEventListener("input", update_plot)
</py-script>
</body>
7 Dateien und Storage
7.1 Dateien hochladen
<body>
<input type="file" id="fileInput" accept=".csv">
<div id="preview"></div>
<py-script>
from pyscript import document, display
import pandas as pd
from io import StringIO
async def handle_file(event):
file = event.target.files.item(0)
# Datei lesen
text = await file.text()
# Als Pandas DataFrame
df = pd.read_csv(StringIO(text))
display(df, target="preview")
file_input = document.querySelector("#fileInput")
file_input.addEventListener("change", handle_file)
</py-script>
</body>
7.2 localStorage verwenden
<body>
<input type="text" id="dataInput" placeholder="Enter data">
<button id="saveBtn">Save</button>
<button id="loadBtn">Load</button>
<div id="output"></div>
<py-script>
from pyscript import document, window
def save_data(event):
data = document.querySelector("#dataInput").value
window.localStorage.setItem("myData", data)
document.querySelector("#output").innerText = "Saved!"
def load_data(event):
data = window.localStorage.getItem("myData")
document.querySelector("#output").innerText = f"Loaded: {data}"
document.querySelector("#saveBtn").addEventListener("click", save_data)
document.querySelector("#loadBtn").addEventListener("click", load_data)
</py-script>
</body>
8 HTTP-Requests
<body>
<button id="fetchBtn">Fetch Data</button>
<div id="data"></div>
<py-script>
from pyscript import document
import json
from pyodide.http import pyfetch
async def fetch_data(event):
response = await pyfetch("https://api.github.com/users/python")
data = await response.json()
output = f"Name: {data['name']}\n"
output += f"Followers: {data['followers']}"
document.querySelector("#data").innerText = output
btn = document.querySelector("#fetchBtn")
btn.addEventListener("click", fetch_data)
</py-script>
</body>
9 Externe Python-Dateien
9.1 Dateien importieren
<!-- index.html -->
<head>
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-config>
[fetch](fetch.md)
files = ["utils.py"]
</py-config>
</head>
<body>
<py-script>
from utils import greet, calculate
print(greet("Alice"))
print(calculate(10, 5))
</py-script>
</body>
# utils.py
def greet(name):
return f"Hello, {name}!"
def calculate(a, b):
return a + b
10 Vor- und Nachteile
10.1 Vorteile
✅ Kein Server nötig
- Statisches Hosting (GitHub Pages, Netlify)
- Keine Backend-Infrastruktur
- Kostenlos hostbar
✅ Python-Ökosystem
- NumPy, Pandas, Matplotlib
- Wissenschaftliche Bibliotheken
- Bekannte Syntax
✅ Offline-fähig
- Nach initialem Laden
- Progressive Web Apps möglich
✅ Sicherheit
- Browser-Sandbox
- Kein direkter Dateisystem-Zugriff
10.2 Nachteile
❌ Ladezeit
- Initial: 5-10 Sekunden (Pyodide + Pakete)
- ~6 MB Download
- Langsam auf mobilen Geräten
❌ Performance
- Langsamer als natives JavaScript
- WebAssembly-Overhead
- Nicht für alle Use Cases geeignet
❌ Eingeschränkte Pakete
- Nicht alle Python-Pakete verfügbar
- C-Extensions müssen kompiliert sein
- Keine native System-Calls
❌ Browser-Support
- Moderne Browser nötig
- WebAssembly erforderlich
- IE nicht unterstützt
11 Use Cases
✅ Gut geeignet für:
- Datenvisualisierung
- Wissenschaftliche Demos
- Interaktive Tutorials
- Prototyping
- Bildungs-Tools
- Data Science Dashboards
- Statische Webseiten mit Python-Logik
❌ Nicht geeignet für:
- Performance-kritische Apps
- Große Enterprise-Anwendungen
- Real-time Anwendungen
- Mobile-First Apps
- SEO-kritische Seiten (initial render)
12 Alternativen
12.1 Pyodide (direkt)
PyScript basiert auf Pyodide. Kann auch direkt verwendet werden:
<script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
<script type="text/javascript">
async function main() {
let pyodide = await loadPyodide();
await pyodide.loadPackage("numpy");
pyodide.runPython(`
import numpy as np
print(np.array([1, 2, 3]))
`);
}
main();
</script>
Vorteile:
- Mehr Kontrolle
- Kleinerer Overhead
- Direkter JavaScript-Zugriff
Nachteile:
- Mehr Boilerplate
- Weniger Python-freundlich
12.2 Brython
Browser-Python-Implementierung (kein WebAssembly):
<script src="https://cdn.jsdelivr.net/npm/brython@3/brython.min.js"></script>
<body onload="brython()">
<script type="text/python">
from browser import document, alert
def hello(event):
alert("Hello from Brython!")
document["myButton"].bind("click", hello)
</script>
<button id="myButton">Click</button>
</body>
Vorteile:
- Schnellerer Start
- Leichtgewichtiger
Nachteile:
- Weniger Pakete
- Nicht vollständig Python-kompatibel
- Langsamer bei Berechnungen
12.3 Skulpt
Weitere Browser-Python-Alternative:
<script src="http://skulpt.org/js/skulpt.min.js"></script>
<script src="http://skulpt.org/js/skulpt-stdlib.js"></script>
<script>
function runit() {
Sk.configure({output: console.log});
Sk.importMainWithBody("<stdin>", false, `
print("Hello from Skulpt!")
`);
}
</script>
13 Vergleich
| Framework | Basis | Pakete | Performance | Ladezeit | Python-Version |
|---|---|---|---|---|---|
| PyScript | Pyodide/WASM | ✅✅ Viele | ⚠️ Mittel | ⏱️ Lang | 3.11 |
| Pyodide | WASM | ✅✅ Viele | ⚠️ Mittel | ⏱️ Lang | 3.11 |
| Brython | JS | ⚠️ Wenige | ✅ Schneller | ✅ Kurz | 3.10 |
| Skulpt | JS | ❌ Minimal | ✅ Schneller | ✅ Kurz | 2.x |
14 Praktisches Beispiel: Todo-App
<!DOCTYPE html>
<html>
<head>
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
<style>
.todo-item { padding: 10px; margin: 5px; border: 1px solid #ccc; }
.done { text-decoration: line-through; opacity: 0.5; }
</style>
</head>
<body>
<h1>PyScript Todo App</h1>
<input type="text" id="todoInput" placeholder="New todo...">
<button id="addBtn">Add</button>
<div id="todoList"></div>
<py-script>
from pyscript import document
todos = []
def render_todos():
todo_list = document.querySelector("#todoList")
todo_list.innerHTML = ""
for i, todo in enumerate(todos):
item = document.createElement("div")
item.className = "todo-item"
if todo['done']:
item.classList.add("done")
item.innerText = todo['text']
item.addEventListener("click", lambda e, idx=i: toggle_todo(idx))
todo_list.appendChild(item)
def add_todo(event):
input_elem = document.querySelector("#todoInput")
text = input_elem.value.strip()
if text:
todos.append({'text': text, 'done': False})
input_elem.value = ""
render_todos()
def toggle_todo(index):
todos[index]['done'] = not todos[index]['done']
render_todos()
# Event Listeners
add_btn = document.querySelector("#addBtn")
add_btn.addEventListener("click", add_todo)
input_elem = document.querySelector("#todoInput")
input_elem.addEventListener("keypress",
lambda e: add_todo(e) if e.key == "Enter" else None)
</py-script>
</body>
</html>
15 Deployment
15.1 GitHub Pages
# 1. Erstelle Repository
git init
git add .
git commit -m "Initial commit"
# 2. Push zu GitHub
git remote add origin https://github.com/username/pyscript-app.git
git push -u origin main
# 3. GitHub Pages aktivieren (Settings → Pages)
# Source: main branch
# 4. App verfügbar unter:
# https://username.github.io/pyscript-app/
15.2 Netlify
# 1. netlify.toml erstellen
[build]
publish = "."
# 2. Deploy
netlify deploy --prod
16 Best Practices
✅ DO:
- Loading-Indicator während Pyodide lädt
- Pakete nur wenn nötig laden
- Code in externe .py-Dateien auslagern
- Caching nutzen (Service Worker)
- Progressive Enhancement (JS Fallback)
❌ DON’T:
- Zu viele Pakete auf einmal laden
- Große Berechnungen synchron
- PyScript für SEO-kritische Seiten
- Sensitive Daten client-seitig verarbeiten
- Alte Browser erwarten
17 Zukunft von PyScript
Entwicklungen:
- Performance-Verbesserungen
- Kleinere Bundle-Größen
- Mehr Pakete
- Bessere Tooling
- Framework-Integration (React, Vue)
Versionen:
- 2023: PyScript 1.0 (stabil)
- 2024: Verbesserte Performance, kleinere Bundles
- Zukunft: Native WebAssembly-Integration
18 Zusammenfassung
| Aspekt | Bewertung |
|---|---|
| Einstieg | ✅ Einfach (HTML + Python) |
| Performance | ⚠️ Langsamer als JS, aber akzeptabel |
| Use Cases | Visualisierung, Demos, Prototyping |
| Produktion | ⚠️ Mit Vorsicht (Ladezeit beachten) |
| Zukunft | ✅ Aktive Entwicklung |
Kernprinzip: PyScript ermöglicht Python im Browser für Visualisierung und interaktive Demos. Ideal für statische Seiten mit wissenschaftlichen Inhalten, aber nicht als vollständiger JavaScript-Ersatz. Beachte Ladezeiten und Browser-Kompatibilität.
Ressourcen:
- Offizielle Docs: https://docs.pyscript.net/
- Examples: https://pyscript.net/examples/
- Pyodide: https://pyodide.org/
- Dicord: https://discord.gg/pyscript
Einführung
1 Begriffsdefinitionen
1.1 Data Science (Datenwissenschaft)
Data Science ist ein interdisziplinäres Feld, das sich mit dem Extrahieren von Wissen und Erkenntnissen aus Daten beschäftigt. Es kombiniert:
- Statistik
- Informatik
- Mathematik
- und oft auch Fachwissen aus einem bestimmten Bereich
Ziel ist es, aus großen Datenmengen Muster zu erkennen, Vorhersagen zu treffen oder Entscheidungen zu unterstützen.
Beispiele: Analyse von Kundenverhalten, Betrugserkennung, Optimierung von Geschäftsprozessen.
Alltagsbeispiel: Ein Online-Shop analysiert die Kaufhistorie seiner Kunden, um herauszufinden, welche Produkte oft zusammen gekauft werden. Daraus entstehen dann personalisierte Empfehlungen.
➡️ Amazon schlägt vor: “Kunden, die das gekauft haben, kauften auch…”
1.2 Machine Learning (Maschinelles Lernen)
Machine Learning ist ein Teilbereich der Künstlichen Intelligenz, bei dem Computer aus Daten lernen, ohne explizit programmiert zu werden. Es gibt dem Computer die Fähigkeit, aus Beispielen zu lernen und bei neuen Daten selbstständig Ergebnisse vorherzusagen.
Beispiel: Ein E-Mail-Dienst filtert deine Nachrichten automatisch in “Posteingang” oder “Spam”, basierend auf erkannten Mustern und Erfahrungen mit früheren E-Mails.
➡️ Gmail erkennt, dass „Gratis-Gewinn!“ meist Spam ist – auch wenn man das nie gesagt hat.
1.3 Deep Learning (Tiefes Lernen)
Deep Learning ist eine spezielle Methode im Machine Learning, die mit künstlichen neuronalen Netzwerken arbeitet, die aus vielen Schichten (“deep”) bestehen. Diese Methode ist besonders gut geeignet für:
- Bild- und Spracherkennung
- Übersetzungen
- autonomes Fahren
Beispiel: Das Smartphone kann Gesichter auf Fotos automatisch erkennen und sogar sortieren nach Personen.
➡️ Das iPhone gruppiert alle Bilder deiner Freunde – auch wenn sie unterschiedlich aussehen.
1.4 Generative Neuronal Networks (Generative Netzwerke / GANs)
Diese Netzwerke gehören zur Deep-Learning-Welt. Generative Adversarial Networks (GANs) bestehen aus zwei neuronalen Netzwerken, die gegeneinander arbeiten:
- Ein Generator erzeugt neue Daten (z. B. Bilder).
- Ein Diskriminator bewertet, ob die Daten echt oder künstlich sind.
Ziel: Der Generator lernt, so realistisch wie möglich neue Inhalte zu erzeugen – z. B. real aussehende Gesichter, die es aber gar nicht gibt.
Beispiel: Ein Tool wie DALL·E oder Midjourney kann auf Basis eines Textes realistische Bilder erzeugen, obwohl sie nie existiert haben.
➡️ Man gibt ein: „Ein Panda, der Gitarre spielt im Schnee“ – und bekommst ein Bild davon.
1.5 Artificial Intelligence (Künstliche Intelligenz / KI)
Künstliche Intelligenz ist ein Oberbegriff für alle Methoden, bei denen Maschinen so programmiert sind, dass sie intelligentes Verhalten zeigen – also Probleme lösen, Entscheidungen treffen, lernen oder Sprache verstehen, ähnlich wie Menschen.
Beispiel: Ein Sprachassistent versteht deine Sprache, beantwortet Fragen, spielt Musik oder steuert dein Smart Home – fast wie ein echter Gesprächspartner.
➡️ „Siri, wie wird das Wetter morgen?“ – und sie antwortet.
2 Häufig verwendete Bibliotheken
- Numpy: fundamentale mathematische Operationen auf Vektoren und Matrizen
- Pandas: Microsoft Excel auf Steroide
- Matplotlib: Erstellung von grafischen Darstellungen mit wenig Code
- Scipy: Implementiert alles, was Numpy nicht bietet
NumPy
1 Einleitung
NumPy ist DIE Standardbibliothek für wissenschaftliche Berechnungen. Sie stellt ein mehrdimensionales Array-Objekt, verschiedene abgeleitete Objekte und eine Vielzahl von Funktionen für schnelle Operationen auf Arrays bereit.
1.1 Beispiel für die Leistungssteigerung durch Numpy
Durch die Verwendung von numpy anstatt von Listen hat man nicht nur besser lesbaren Code, sondern häufig auch eine deutliche Reduzierung der Ausführungszeit:
import time
import numpy as np
start = time.perf_counter()
x = list(range(10000000))
y = list(range(10000000))
sum = [a + b for a, b in zip(x, y)]
end = time.perf_counter()
print(end - start) # 0.49263983300625114
# Schneller mit numpy:
start = time.perf_counter()
x = np.arange(10000000)
y = np.arange(10000000)
sum = x + y
end = time.perf_counter()
print(end - start) # 0.22649779099447187
x + y macht das gleiche, wie [a + b for a, b in zip(x, y)] ist jedoch deutlich schneller zu schreiben und einfacher zu lesen.
2 Funktionsweise
Das Herzstück des NumPy-Pakets ist das ndarray-Objekt. Dieses kapselt n-dimensionale Arrays homogener Datentypen, wobei viele Operationen in kompiliertem C-Code ausgeführt werden, was zu einer deutlich höheren Leistung im Vergleich zu Listen führt. Des Weiteren ist die Schreibweise kürzer und besser lesbar.
Eigenschaften eines ndarray:
- Es beschreibt eine Sammlung von “Elementen” des gleichen Typs.
- Die Elemente können beispielsweise mit
nganzen Zahlen indexiert werden. - Alle
ndarrayssind homogen: Jedes Element belegt einen gleich großen Speicherblock. - Ein Element aus dem Array wird durch ein
PyObjectrepräsentiert, das zu den eingebauten skalaren NumPy-Typen gehört.
3 Import
import numpy as np
Der Name np ist zwar frei wählbar, wird in den meisten Fällen jedoch nicht anders benannt.
4 Datentypen
| NumPy-Typ | C-Typ | Beschreibung |
|---|---|---|
bool_ | bool | Boolean (True oder False) |
int8 | signed char | Ganze Zahl, 8 Bit, Bereich: -128 bis 127 |
uint8 | unsigned char | Ganze Zahl, 8 Bit, Bereich: 0 bis 255 |
int16 | short | Ganze Zahl, 16 Bit, Bereich: -32k bis 32k |
uint16 | unsigned short | Ganze Zahl, 16 Bit, positiv |
int32 | int | Ganze Zahl, 32 Bit |
uint32 | unsigned int | Ganze Zahl, 32 Bit, positiv |
int64 | long / int64_t | Ganze Zahl, 64 Bit |
uint64 | unsigned long | Ganze Zahl, 64 Bit, positiv |
float16 | half | Fließkommazahl, 16 Bit (geringere Genauigkeit) |
float32 | float | Fließkommazahl, 32 Bit |
float64 | double | Fließkommazahl, 64 Bit (Standard) |
complex64 | float complex | Komplexe Zahl (2×32 Bit floats) |
complex128 | double complex | Komplexe Zahl (2×64 Bit floats) |
object_ | PyObject* | Beliebiges Python-Objekt |
string_ | char[] | Fester Byte-String (ASCII) |
unicode_ | Py_UNICODE[] | Unicode-String |
4.1 Definition eines Datentyps
arr = np.array([1, 2, 3], dtype=np.int32)
print(arr.dtype)
4.2 Casten (Typenumwandlung)
arr_float = arr.astype(np.float64)
arr_bool = arr.astype(bool)
arr_str = arr.astype(str)
5 Arrays erstellen
# Erstellen eines 1D-Arrays
arr = np.array([1, 2, 3, 4, 5])
# Erstellen eines 2D-Arrays
arr = np.array([[1, 2, 3], [4, 5, 6]])
# Null-Array erstellen
zeros = np.zeros(shape=(3, 3))
# Einsen-Array erstellen
ones = np.ones((2, 2))
# Einheitsmatrix
identity = np.eye(3)
# Leeres Array erstellen
empty_array = np.empty((2, 3))
# Array mit einem Wertebereich
range_array = np.arange(0, 10, 2)
# Gleichmäßig verteilte Werte
linspace_array = np.linspace(0, 1, 5)
6 Eigenschaften von Arrays
def print_array_info(array: np.ndarray) -> None:
print(f'ndim: {array.ndim}') # Anzahl der Dimensionen
print(f'shape: {array.shape}') # Form des Arrays
print(f'size: {array.size}') # Anzahl der Elemente
print(f'dtype: {array.dtype}') # Datentyp des Arrays
print(f'values:\n{array}\n') # Werte
Beispiel:
# Erstellen eines 2D-Arrays (3 Zeilen, 4 Spalten)
arr2D = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
# Eigenschaften des Arrays
ndim = arr2D.ndim
shape = arr2D.shape
size = arr2D.size
dtype = arr2D.dtype
print("Anzahl der Dimensionen:", ndim) # 2
print("Form des Arrays:", shape) # (3, 4)
print("Anzahl der Elemente:", size) # 12
print("Datentyp der Elemente:", dtype) # int64
7 Grundlegende Operationen
# Arithmetische Operationen
sum_array = arr + 2
sum_array = arr - 2
mul_array = arr * 2
div_array = arr / 2
# Möglich sind auch die kombinierten Zuweisungsoperatoren
arr += 2
arr -= 2
# ...
# Elementweise Operationen
exp_array = np.exp(arr)
sqrt_array = np.sqrt(arr)
log_array = np.log(arr)
8 Indizierung und Slicing
Die Array-Indexierung bezieht sich auf jede Verwendung von eckigen Klammern ([]), um auf Array-Werte zuzugreifen. Es gibt mehrere Möglichkeiten zur Indexierung, was die NumPy-Indexierung besonders leistungsfähig macht.
info
Slices von Arrays kopieren die internen Array-Daten nicht, sondern erzeugen lediglich neue Ansichten der Daten.
# Zugriff auf ein Element
val = arr[2] # Drittes Element
# Slicing
sub_array = arr[1:4] # Elemente von Index 1 bis 3
# Zugriff auf Zeilen und Spalten
row = arr[1, :] # Zweite Zeile
two_cols = arr[:, :2] # Erste zwei Spalten
inner = array[1:-1, 1:-1] # Array ohne die äußere Zeilen und Spalten
Allgemeine Syntax:
[start:stop:step]
Standardwerte (wenn nichts angegeben wird):
- Start:
0 - Stop: Bis zum Ende
- Step:
2(jeder 2. Wert)
NumPy user guide: Slicing and striding
9 Aggregationsfunktionen
sum_value = np.sum(arr)
mean_value = np.mean(arr)
min_value = np.min(arr)
max_value = np.max(arr)
std_dev = np.std(arr)
# Anzahl der Zahlen < 0
count_negative = np.count_nonzero(arr)
count_negative = np.sum(arr < 0) # Alternative
10 Mathematische Operationen
# Matrixmultiplikation
matmul_result = np.dot(arr, arr.T)
# Alternative Matrixmultiplikation
matmul_alt = arr @ arr.T
11 Array-Manipulation
# Reshape (Umformen)
reshaped = arr1.reshape(3, 2)
# Transponieren
transposed = arr.T
# Stapeln von Arrays
vstacked = np.vstack([arr1, arr2]) # Vertikales Stapeln
hstacked = np.hstack([arr1, arr2]) # Horizontales Stapeln
# Alternative zum Stapeln
vstacked = np.concatenate([arr1, arr2], axis=0) # Vertikales Stapeln
hstacked = np.concatenate([arr1, arr2], axis=1) # Horizontales Stapeln
# Arrays zusammenfügen mit concatenate
concatenated = np.concatenate([arr1, arr2]) # 1D Arrays
concat_2D_axis0 = np.concatenate([arr1, arr2], axis=0) # Zeilen anhängen
concat_2D_axis1 = np.concatenate([arr1, arr2], axis=1) # Spalten anhängen
# In 1D umwandeln
flat1 = arr.flatten() # Gibt eine Kopie zurück
flat2 = arr.ravel() # Gibt eine Ansicht zurück (wenn möglich)
Erweiterung der Dimension eines Arrays mit np.newaxis:
arr1D = np.array([1, 2, 3, 4])
# Umwandlung in Spalten-Vektor (4 Zeilen, 1 Spalte)
col_vector = arr1D[:, np.newaxis]
# Umwandlung in Zeilen-Vektor (1 Zeile, 4 Spalten)
row_vector = arr1D[np.newaxis, :]
print("Original:", arr1D.shape) # Original: (4,)
print("Spalten-Vektor:", col_vector.shape) # Spalten-Vektor: (4, 1)
print("Zeilen-Vektor:", row_vector.shape) # Zeilen-Vektor: (1, 4)
12 Zufallszahlen
Matrix der Größe 5x5 mit $M_{i,j}\in[-10, 10]$:
M = np.random.randint(low=-10, high=11, size=(5, 5))
# Der untere Wert ist inklusive, der obere nicht!
Als float:
N = np.random.uniform(-10, 10, size=(5, 5))
Weitere Funktionen:
rand_num = np.random.rand(3, 3) # Zufallszahlen zwischen 0 und 1
rand_ints = np.random.randint(0, 10, (2, 3)) # Zufällige Ganzzahlen
normal_dist = np.random.randn(3, 3) # Normalverteilte Zufallszahlen
13 Bedingte Auswahl
# Bedingte Auswahl mit Masken
mask = arr > 2
filtered = arr[mask] # Enthält nur Werte > 2
14 Speichern und Laden
np.save("array.npy", arr) # Speichern
loaded_array = np.load("array.npy") # Laden
15 Ufuncs (Universal Functions)
Eine Universal Function ist eine Funktion, welche auf ein ndarray elementweise angewendet wird. Es handelt sich um einen “vektorbasierten” Wrapper für eine Funktion, die eine feste Anzahl spezifischer Eingaben nimmt und eine feste Anzahl spezifischer Ausgaben erzeugt.
Ufuncs sind teilweise 1000-2000 x schneller, als eine Lösung über Python-Code (ohne Numpy).
16 Häufig verwendete Funktionen
| Funktion | Beschreibung |
|---|---|
np.add(x, y) | Elementweise Addition |
np.subtract(x, y) | Elementweise Subtraktion |
np.multiply(x, y) | Elementweise Multiplikation |
np.divide(x, y) | Elementweise Division |
np.power(x, y) | Potenzieren: x**y für jedes Element |
np.mod(x, y) | Rest der Division: x % y |
np.floor(x) | Abrunden auf nächste ganze Zahl (nach unten) |
np.ceil(x) | Aufrunden auf nächste ganze Zahl (nach oben) |
np.round(x) | Runden auf nächste ganze Zahl |
np.sqrt(x) | Quadratwurzel jedes Elements |
np.exp(x) | Exponentialfunktion: e^x für jedes Element |
np.log(x) | Natürlicher Logarithmus |
np.log10(x) | Zehner-Logarithmus |
np.sin(x) | Sinus jedes Elements (x in Radiant) |
np.cos(x) | Kosinus jedes Elements |
np.tan(x) | Tangens jedes Elements |
np.abs(x) | Absoluter Betrag jedes Elements |
np.maximum(x, y) | Größter Wert zwischen x und y (elementweise) |
np.minimum(x, y) | Kleinster Wert zwischen x und y (elementweise) |
| NumPy user guide: Universal functions |
Beispiel:
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
result = np.add(a, b) # [5, 7, 9]
17 Aggregates (Aggregat-Funktionen)
Aggregatfunktionen (engl. aggregate functions) sind Funktionen, die aus einer großen Menge von Werten einen einzigen zusammengefassten Wert berechnen.
Sie werden oft verwendet, um statistische Kennzahlen aus Arrays zu ermitteln, z. B. Summe, Mittelwert oder Maximum.
17.1 Typische Funktionen
| Funktion | Beschreibung |
|---|---|
np.sum() | Summe aller Elemente im Array |
np.mean() | Arithmetischer Mittelwert |
np.min()/np.max() | Kleinster / größter Wert im Array |
np.std() | Standardabweichung |
np.var() | Varianz |
np.median() | Median (Zentralwert) |
np.prod() | Produkt aller Elemente im Array |
Beispiel:
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
print(np.sum(arr)) # 15
print(np.mean(arr)) # 3.0
print(np.std(arr)) # 1.414...
Mit axis= kann man die Funktion entlang einer bestimmten Achse anwenden.
Beispiel:
import numpy as np
# 2D-Array mit 3 Zeilen und 4 Spalten
arr2D = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
# Summe pro Spalte (↓)
sum_cols = np.sum(arr2D, axis=0)
# Summe pro Zeile (→)
sum_rows = np.sum(arr2D, axis=1)
print("Summe pro Spalte:", sum_cols) # [15 18 21 24]
print("Summe pro Zeile:", sum_rows) # [10 26 42]
axis=0➝ Berechnung erfolgt über Zeilen hinweg → ergibt Spalten-Ergebnisseaxis=1➝ Berechnung erfolgt über Spalten hinweg → ergibt Zeilen-Ergebnisse
18 Broadcasting
Broadcasting beschreibt die Fähigkeit von NumPy, Arrays unterschiedlicher Form automatisch so zu erweitern, dass sie elementweise Operationen miteinander durchführen können.
👉 Wenn man z. B. ein Array mit einem Skalar oder mit einem kleineren Array kombiniert, passt NumPy intern die Formen an, ohne dass man explizit etwas tun muss.
Vorteile:
- Spart Schleifen
- Spart Speicher
- Sehr schnell dank NumPy-Optimierung
Beispiel 1: Array + Skalar
a = np.array([1, 2, 3])
b = 10
result = a + b # [11, 12, 13]
Beispiel 2: 2D + 1D
A = np.array([[1, 2, 3],
[4, 5, 6]])
B = np.array([10, 20, 30])
result = A + B
Intern:
[[ 1, 2, 3] [[10, 20, 30] [[11, 22, 33]
[ 4, 5, 6]] + [10, 20, 30]] = [14, 25, 36]]
18.1 Broadcasting-Regeln
18.2 Von hinten vergleichen:
NumPy vergleicht die Dimensionen von rechts nach links.
Beispiel:
A.shape = (3, 4)
B.shape = (4,) # entspricht intern (1, 4)
Vergleich:
A: (3, 4)
B: (4) → automatisch zu (1, 4)
✅ passt! → Broadcasting funktioniert → B wird auf jede der 3 Zeilen “kopiert”
18.3 Dimensionen müssen gleich sein oder eine von beiden muss 1 sein
Beispiel 1:
A = np.ones((5, 1))
B = np.ones((1, 4))
# Ergebnisform: (5, 4)
Vergleich:
A: (5, 1)
B: (1, 4)
✅ 1 kann zu 4 gebroadcastet werden → ergibt (5, 4)
Beispiel 2:
A = np.ones((3, 2))
B = np.ones((3,))
Vergleich:
A: (3, 2)
B: (3)
❌ 2 vs. 3 → nicht kompatibel, weil 2 ≠ 3 und keine davon ist 1
18.4 Fehlende Dimensionen werden als 1 ergänzt (vorne)
Beispiel:
A = np.ones((4,)) # (4,)
B = np.ones((3, 4)) # (3, 4)
Vergleich:
A: (1, 4)
B: (3, 4)
1funktioniert wie ein Platzhalter → es wird “in diese Richtung” kopiert- Fehlende Dimension? → einfach eine
1davor denken!
19 Masken und Vergleichsoperatoren
arr = np.array([1, 2, 3, 4, 5])
# Vergleichsoperationen erzeugen boolesche Masken
mask_gt = arr > 3 # [False False False True True]
mask_eq = arr == 2 # [False True False False False]
mask_le = arr <= 4 # [ True True True True False]
# Masken zur Auswahl verwenden
filtered = arr[mask_gt] # [4 5]
# Direkt als Bedingung
filtered_direct = arr[arr % 2 == 0] # [2 4] → alle geraden Zahlen
Indizes der Werte, die größer gleich Null sind:
idxs = np.where(arr >= 0)
Vergleichsoperatoren:
| Ausdruck | Bedeutung |
|---|---|
| == | gleich |
| != | ungleich |
< | kleiner als |
<= | kleiner oder gleich |
> | größer als |
>= | größer oder gleich |
Masken können mit logischen Operatoren kombiniert werden:
arr[(arr > 1) & (arr < 5)] # [2 3 4]
20 Zufallsfunktionen (np.random)
import numpy as np
# Gleichverteilte Zufallszahlen (0 bis 1)
uniform = np.random.rand(2, 3)
# Normalverteilte Zufallszahlen (Mittel=0, Std=1)
normal = np.random.randn(2, 3)
# Zufällige Ganzzahlen
rand_ints = np.random.randint(0, 10, size=(2, 3)) # von 0 bis 9
# Zufällige Auswahl aus Array
arr = np.array([10, 20, 30, 40])
choice = np.random.choice(arr, size=2, replace=False) # ohne Wiederholung
# Mischen eines Arrays (in-place)
np.random.shuffle(arr) # verändert arr direkt
# Setzen des Zufallsseeds für Reproduzierbarkeit
np.random.seed(42)
| Funktion | Beschreibung |
|---|---|
np.random.rand(dims...) | Gleichverteilte float-Zufallszahlen (zwischen 0 und 1) |
np.random.randn(dims...) | Normalverteilte Zufallszahlen (Mittel=0, Std=1) |
np.random.randint(a, b) | Zufällige Ganzzahlen im Bereich [a, b) |
np.random.choice() | Zufällige Auswahl aus einem Array |
np.random.shuffle() | Mischt ein Array in-place |
np.random.seed(n) | Setzt den Seed zur Steuerung der Zufälligkeit |
21 Weitere wichtige Funktionen
21.1 Achsen verschieben & transponieren
import numpy as np
a = np.zeros((2, 3, 4))
# Achsen vertauschen (z. B. für Bildverarbeitung)
a_moved = np.moveaxis(a, 0, -1) # Achse 0 nach hinten → shape (3, 4, 2)
a_transposed = np.transpose(a, (2, 0, 1)) # explizite Neuanordnung der Achsen
Wann moveaxis, wann transpose?
moveaxis: Wenn eine oder mehrere Achsen gezielt verschieben werden sollen.transpose: Wenn eine vollständige neue Achsenreihenfolge angegeben wird.
21.2 Rollendes Verschieben
arr = np.array([1, 2, 3, 4, 5])
rolled = np.roll(arr, shift=2) # [4 5 1 2 3]
Funktioniert auch mit mehrdimensionalen Arrays → np.roll(arr2D, shift=1, axis=0)
21.3 Indizes & Matrix-Tools
# Koordinaten eines Grids
idx = np.indices((2, 3)) # shape: (2, 2, 3) → für 2D: y- und x-Koordinaten
# Indizes der Diagonale
d_idx = np.diag_indices(n=3) # ([0,1,2], [0,1,2])
# Untere Dreiecksmatrix
tril_idx = np.tril_indices(n=3)
# Obere Dreiecksmatrix
triu_idx = np.triu_indices(n=3)
Diese sind besonders nützlich zum Indexieren von Diagonalen, Dreiecksbereichen oder Rasterpunkten.
21.4 Indizes nach Ordnung / Extremwerten
arr = np.array([10, 20, 5, 30])
# Index der Sortierung
idx_sorted = np.argsort(arr) # [2 0 1 3]
# Index des Maximums / Minimums
idx_max = np.argmax(arr) # 3
idx_min = np.argmin(arr) # 2
21.5 Sonstiges
# Diagonale mit Werten füllen
np.fill_diagonal(M, np.pi)
22 Daten speichern & laden mit NumPy
info
Die folgenden Funktionen sind auch mit Pandas möglich und i. d. R. einfacher zu handhaben.
22.1 Binäres Speichern (schnell & effizient)
np.save(): Speichert ein einzelnes Array als .npy-Datei
arr = np.array([1, 2, 3])
np.save("array.npy", arr) # speichert binär
np.savez(): Speichert mehrere Arrays in einer komprimierten ZIP-Datei
a = np.arange(5)
b = np.linspace(0, 1, 5)
np.savez("arrays.npz", first=a, second=b)
data = np.load("arrays.npz")
print(data["first"]) # Zugriff auf gespeicherte Arrays
np.load(): Lädt .npy- oder .npz-Dateien
loaded = np.load("array.npy")
22.2 Speichern im Textformat
np.savetxt(): Speichert Array als Klartext (CSV, TSV etc.)
arr = np.array([[1, 2], [3, 4]])
np.savetxt("array.txt", arr, fmt="%d", delimiter=",")
np.loadtxt(): Lädt ein Array aus einer Textdatei
loaded_txt = np.loadtxt("array.txt", delimiter=",")
Nur für einfache, numerische Arrays geeignet. Keine Metadaten oder Struktur.
22.3 Strukturierte Arrays
data = np.array([
(1, 1.5, "A"),
(2, 2.5, "B")
], dtype=[("id", "i4"), ("value", "f4"), ("label", "U1")])
print(data["id"]) # Zugriff auf Spalte
print(data[0]["label"]) # Zugriff auf Zelle
- .npy = kompaktes, schnelles Binärformat für ein Array
- .npz = ZIP-Container für mehrere Arrays
- Textformate sind lesbar, aber langsamer und weniger genau
Pandas
pandas ist eine leistungsstarke, offene Bibliothek für Datenanalyse und -manipulation. Sie bietet zwei zentrale Datenstrukturen:
Series: eine eindimensionale Liste (ähnlich wie ein Array mit Labels)DataFrame: eine tabellarische Datenstruktur (ähnlich wie Excel oder SQL-Tabellen)
Mit pandas kann man:
- Daten lesen, schreiben, filtern, sortieren
- fehlende Werte behandeln
- Gruppieren, aggregieren und berechnen
- Daten aus verschiedenen Quellen wie CSV, Excel, SQL, JSON usw. einlesen
pandas ist ein Standard-Tool für Datenanalyse mit Python - schnell, flexibel und einfach zu verwenden.
grundlegender unterschied zwischen numpy und pandas
Bei Numpy wird standardmäßig über Zeilen indiziert, bei Pandas über Spalten!
Grundlegender Unterschied zwischen Numpy und Pandas: Bei Numpy wird standardmäßig über Zeilen indiziert, bei Pandas über Spalten!
1 Importieren
import pandas as pd
Der Name pd ist zwar frei wählbar, es handelt sich hierbei jedoch um die geläufige Schreibweise.
2 Datentypen
Da pandas auf NumPy basiert sind viele der Datentypen direkt übernommen worden. Numpy-Datentypen wie int32, float64, bool_, object_ usw. sind in pandas-DataFrames direkt verwendbar.
| Datentyp | Beschreibung |
|---|---|
int8 | Ganze Zahl, 8 Bit |
int16 | Ganze Zahl, 16 Bit |
int32 | Ganze Zahl, 32 Bit (Standard für int) |
int64 | Ganze Zahl, 64 Bit |
uint8 | Ganze Zahl, 8 Bit, positiv |
uint16 | Ganze Zahl, 16 Bit, positiv |
uint32 | Ganze Zahl, 32 Bit, positiv |
uint64 | Ganze Zahl, 64 Bit, positiv |
float16 | Fließkommazahl, 16 Bit (geringe Genauigkeit) |
float32 | Fließkommazahl, 32 Bit |
float64 | Fließkommazahl, 64 Bit (Standard für float) |
bool | Boolescher Wert (True/False) |
object | Beliebiger Python-Objekttyp |
string | Pandas-eigener Stringtyp (mit NA-Unterstützung) |
category | Kategorischer Typ (für wenig verschiedene Textwerte) |
datetime64[ns] | Datum/Zeit-Typ im Nanosekundenformat |
timedelta[ns] | Zeitdifferenz-Typ im Nanosekundenformat |
2.1 Unterschiede bzw. pandas-spezifische Ergänzungen
string: keinobjectmehr, kann sauber mit fehlenden Werten umgehencategory: für Zellen mit wenigen verschiedenen Werten (z. B. Geschlecht = männlich oder weiblich)- Datum:
datetime64[ns]undtimedelta[ns]
2.2 Angabe des Datentyps
df = pd.read_csv('daten.csv', dtype={'Alter': 'int32', 'Name': 'string'})
Änderung des Datentyps:
df['Alter'] = df['Alter'].astype('float')
df['Datum'] = pd.to_datetime(df['Datum'])
3 Daten laden und speichern
df = pd.read_csv('datei.csv') # CSV-Datei
df = pd.read_excel('datei.xlsx') # Excel-Datei
df = pd.read_json('datei.json') # JSON-Datei
df.to_csv('neu.csv', index=False)
df.to_excel('neu.xlsx')
df.to_json('neu.xlsx')
4 Serie erstellen
Series ist ein eindimensionales, beschriftetes Array, das Daten beliebigen Typs enthalten kann. Die Achsenbeschriftungen werden gemeinsam als index bezeichnet.
Die grundlegende Methode zur Erstellung einer Series ist der Aufruf:
s = pd.Series(data, index=index)
data muss ein iterierbares Objekt sein, z. B. list oder np.ndarray.
Beispiel:
data = pd.Series(
[0.25, 0.5, 0.75, 1.0],
index=['a', 'b', 'c', 'd'],
)
5 DataFrame erstellen
DataFrame ist eine zweidimensionale, beschriftete Datenstruktur mit Spalten, die unterschiedliche Datentypen enthalten können.
Man kann sich einen DataFrame wie eine Tabelle in einer Tabellenkalkulation oder SQL-Datenbank bzw. wie ein Dictionary aus Series-Objekten vorstellen.
Er ist in der Regel das am häufigsten verwendete pandas-Objekt.
Wie bei Series akzeptiert der DataFrame viele verschiedene Eingabetypen:
- Dictionary aus 1D-ndarrays, Listen, Dictionaries oder Series
- Zwei-dimensionale
numpy.ndarray - Strukturierte oder rekordbasierte
ndarray - Eine einzelne
Series - Ein anderer
DataFrame
data = {'Name': ['Anna', 'Ben'], 'Alter': [25, 30]}
df = pd.DataFrame(data)
6 Eigenschaften von Serien und DataFrames
def print_series_info(series: pd.Series) -> None:
print(f'ndim: {series.ndim}') # Anzahl der Dimensionen
print(f'shape: {series.shape}') # Form (Zeilen, Spalten)
print(f'size: {series.size}') # Größe
print(f'dtype: {series.dtype}') # Datentyp
print(f'values:\n{series}\n') # Werte
def print_df_info(df: pd.DataFrame) -> None:
print(f'ndim: {df.ndim}') # Anzahl der Dimensionen
print(f'shape: {df.shape}') # Form (Zeilen, Spalten)
print(f'size: {df.size}') # Größe
print(f'dtype: {df.dtypes}') # Datentyp
print(f'values:\n{df}\n') # Werte
df.head() # Erste 5 Zeilen
df.tail(3) # Letzte 3 Zeilen
df.columns # Spaltennamen
df.info() # Übersicht
df.describe() # Statistiken
7 Basis-Funktionen
7.1 Zugriff auf Daten
Wir auf eine Spalte z. B. mit df['Name'] zugegriffen, wird eine Series zurückgegeben.
df['Name'] # Einzelne Spalte
df['Name', 'Alter']('Name',%20'Alter'.md) # Mehrere Spalten
df.iloc[0] # Erste Zeile (Position)
df.loc[0] # Erste Zeile (Label)
df.loc[0, 'Name'] # Einzelner Wert
7.2 Daten filtern & sortieren
df[df['Alter'] > 25] # Bedingung
df.sort_values('Alter', ascending=False) # Sortieren
7.3 Spalten bearbeiten
df['Alter_neu'] = df['Alter'] + 1 # Neue Spalte
df.rename(columns={'Name': 'Vorname'}) # Umbenennen
df.drop('Alter_neu', axis=1) # Spalte löschen
7.4 Fehlende Werte
df.isnull() # Wo sind fehlen Werte?
df.dropna() # Zeilen mit NaN entfernen
df.fillna(0) # NaN mit 0 ersetzen
7.5 Gruppieren & Aggregieren
df.groupby('Name').mean() # Durchschnitt pro Gruppe
df['Alter'].mean() # Mittelwert
df['Alter'].sum() # Summe
8 Iteration über Series und DataFrames in pandas
In pandas gibt es verschiedene Möglichkeiten, über Daten zu iterieren – je nachdem, ob man eine Series oder einen DataFrame vor sich hat. Allerdings ist Iteration oft langsamer als Vektoroperationen. Nur verwenden, wenn nötig!
8.1 Über eine Series iterieren
s = pd.Series([10, 20, 30])
for wert in s:
print(wert)
8.2 Über Zeilen eines DataFrames iterieren
8.2.1 iterrows(): Zeilenweise als (Index, Series)
for index, row in df.iterrows():
print(index, row['Spalte1'], row['Spalte2'])
- ✅ Einfach zu verstehen
- ⚠️ Langsam bei großen Datenmengen, siehe pandas
8.2.2 itertuples(): Zeilenweise als NamedTuple
for row in df.itertuples():
print(row.Index, row.Spalte1, row.Spalte2)
- ✅ Schneller als
iterrows() - ⚠️ Spaltennamen müssen wie Attribute verwendet werden
8.2.3 apply(): Elegante, performante Alternative
df['Ergebnis'] = df.apply(lambda row: row['A'] + row['B'], axis=1)
- ✅ Besser als Schleifen für Transformationen
- ⚠️ Immer
axis=1angeben, um über Zeilen zu gehen
8.3 Iteration über Spaltennamen
for spalte in df.columns:
print(df[spalte].mean())
9 Umgang mit fehlenden Daten (Missing Data)
Fehlende Werte sind in pandas durch NaN (Not a Number) oder None gekennzeichnet. pandas bietet leistungsstarke Werkzeuge, um mit ihnen umzugehen.
Fehlende Werte erkennen
df.isnull() # Gibt DataFrame mit True/False zurück
df.notnull() # Gegenteil von isnull()
df.isnull().sum() # Anzahl fehlender Werte pro Spalte
Zeilen/Spalten mit fehlenden Werten entfernen
df.dropna() # Entfernt Zeilen mit NaN
df.dropna(axis=1) # Entfernt Spalten mit NaN
df.dropna(how='all') # Entfernt nur Zeilen, wo alle Werte fehlen
df.dropna(thresh=2) # Behalte nur Zeilen mit mindestens 2 nicht-null Werten
Fehlende Werte ersetzen
df.fillna(0) # NaN mit 0 ersetzen
df.fillna(method='ffill') # Vorherigen Wert übernehmen (forward fill)
df.fillna(method='bfill') # Nachfolgenden Wert übernehmen (backward fill)
df.fillna({'A': 0, 'B': 'leer'}) # Spaltenweise unterschiedliche Werte
Werte gezielt setzen
df.loc[2, 'Spalte'] = None # Einzelne Zelle auf NaN setzen
hinweise
NaNzählt beimean(),sum()usw. nicht mit.- Wenn man mit Strings arbeitet, verwendet man
pd.NAund den Datentypstring, um sauber mit fehlenden Werten umzugehen. - Bei CSV-Importen:
pd.read_csv(..., na_values=["NA", "-", "n/a"])erkennt eigene Platzhalter als fehlend. - Fehlende Daten sind normal - wichtig ist, sie zu erkennen und sinnvoll damit umzugehen!
10 Bedingtes Filtern und logische Masken
Ein zentrales Feature von pandas ist die Möglichkeit, gezielt Zeilen auszuwählen, die bestimmten Bedingungen entsprechen - mithilfe von logischen Ausdrücken und boolschen Masken.
10.1 Beispiel: Überlebende mit Altersangabe filtern
survived = df['Survived'] == 1 # Überlebende
has_age = df['Age'] > 0 # Altersangabe vorhanden (> 0)
idxs = survived & has_age # Beide Bedingungen gleichzeitig
df_sliced = df[idxs] # DataFrame filtern
& verknüpft Bedingungen logisch (AND).
❗ Bedingungen müssen in Klammern gesetzt werden, wenn direkt kombiniert wird:
df[(df['Survived'] == 1) & (df['Age'] > 0)]
10.2 Werte zählen (z. B. Altersverteilung der Überlebenden)
survived_age_counts = df_sliced['Age'].value_counts()
print(survived_age_counts)
value_counts()zählt, wie oft jeder Wert in einer Spalte vorkommt.- Praktisch für Histogramme, Häufigkeitsanalysen etc.
df[df['Sex'] == 'female'] # Nur Frauen
df[(df['Fare'] > 100) & (df['Pclass'] == 1)] # Teure Tickets in 1. Klasse
df[df['Cabin'].notnull()] # Kabinenangabe vorhanden
10.3 Tipps
- Für komplexe Bedingungen immer Klammern setzen!
- Bedingungen können vorher benannt werden (wie
survived,has_age), das macht Code lesbarer. .value_counts(),.mean(),.sum()und andere Funktionen kann man direkt auf gefilterte Daten anwenden.
11 DataFrame-Styling mit df.style
Mit pandas.DataFrame.style kannst du DataFrames visuell aufbereiten - z. B. zur Anzeige in Jupyter Notebooks oder beim Export nach HTML. df.style gibt ein Styler-Objekt zurück, mit dem man Formatierungen anwenden kann.
11.1 Formatierung von Zahlen
11.1.1 Darstellung in Jupyter
df.style.format('{:.2f}') # Zwei Nachkommastellen
df.style.format({'A': '${:.1f}'}) # Spaltenweise Formatierung
import pandas as pd
from IPython.display import display
df = pd.DataFrame({
'A': [1.23456, 2.34567],
'B': [3.14159, 2.71828],
'C': [10, 20]
})
# Spaltenweise Formatierung definieren
format_dict = {
'A': '{:.1f}', # 1 Nachkommastelle
'B': '{:.3f}', # 3 Nachkommastellen
}
display(df.style.format(format_dict))
11.1.2 Konsolenausgabe
Globale Einstellung:
# Globale Einstellung für Anzahl der Nachkommastellen
pd.options.display.float_format = '{:.3f}'.format
# ...
# Globale Einstellung zurücksetzen
pd.reset_option('display.float_format')
Explizite Formatierung:
import pandas as pd
df = pd.DataFrame({
'A': [1.23456, 2.34567],
'B': [3.14159, 2.71828],
'C': [10, 20]
})
# Manuell formatierte Kopie (als Strings)
df_formatted = df.copy()
df_formatted['A'] = df_formatted['A'].map(lambda x: f"{x:.1f}")
df_formatted['B'] = df_formatted['B'].map(lambda x: f"{x:.3f}")
print(df_formatted)
Ausgabe:
A B C
0 1.2 3.142 10
1 2.3 2.718 20
11.2 Farben
Farbverläufe & Hervorhebungen
df.style.background_gradient(cmap='Blues') # Farbverlauf nach Werten
df.style.highlight_max(axis=0) # Höchstwerte markieren
df.style.highlight_min(axis=1) # Tiefstwerte pro Zeile
df.style.bar(subset=['A", 'B'], color='lightgreen') # Balken im Hintergrund
Bedingtes Styling
def rot_negative(val):
color = 'red' if val < 0 else 'black'
return f'color: {color}'
df.style.applymap(rot_negative, subset=['Gewinn'])
Kombinationen & Ketten
(df.style
.format('{:.1f}')
.highlight_null('red')
.set_caption('Umsatzübersicht')
)
hinwes
df.style beeinflusst nur die Darstellung, nicht den DataFrame selbst. Ideal für Berichte Dashboards oder visuelle Prüfungen.
Weitere Infos: pandas Styler Doku
12 Erweiterte Funktionen
12.1 Concatenation / Append / Join
Mehrere DataFrames können zu einem neuen kombiniert werden - entweder vertikal (Zeilen anhängen) oder horizontal (Spalten zusammenführen):
# Vertikal zusammenfügen (wie append)
df_all = pd.concat([df1, df2], axis=0)
# Horizontal zusammenfügen (wie join)
df_all = pd.concat([df1, df2], axis=1)
# DataFrame mit Keys zusammenfügen
df = pd.concat([df1, df2], keys=['x', 'y'])
Für Verknüpfungen wie bei SQL:
pd.merge(df1, df2, on='id', how='inner') # join via Spalte 'id'
pd.merge(df1, df2, left_on='A', right_on='B', how='outer')
Einzelne Zeile anhängen:
df.append({'Name': 'Max', 'Alter': 28}, ignore_index=True)
12.2 Apply / Map
apply() ist sehr mächtig, um Funktionen auf Zeilen oder Spalten anzuwenden.
# Spalte transformieren
df['neue_spalte'] = df['alte_spalte'].apply(lambda x: x + 10)
# Zeilenweise Funktion
def f(row):
return row['A'] + row['B']
df['Summe'] = df.apply(f, axis=1)
# map() für einzelne Werte in einer Series
s = pd.Series([1, 2, 3])
s.map({1: 'eins', 2: 'zwei'})
12.3 Zeitserien (Timeseries)
Mit pandas lassen sich Zeitreihen effizient analysieren und verarbeiten.
# Zeitreihe erzeugen
dates = pd.date_range('2023-01-01', periods=5, freq='D')
ts = pd.Series([1, 2, 3, 4, 5], index=dates)
# Umwandlung zu Datumsobjekten
df['Datum'] = pd.to_datetime(df['Datum'])
# Resampling – z. B. pro Monat mitteln
df.resample('M').mean()
# Zeitdifferenzen berechnen
df['delta'] = df['Ende'] - df['Start']
12.4 Split-Apply-Combine (GroupBy)
Ein zentrales Muster in pandas:
- Daten in Gruppen aufteilen (
split) - Funktion anwenden (
apply) - Ergebnis zusammenführen (
combine)
# Gruppieren nach Kategorie
grouped = df.groupby('Kategorie')
# Mittelwerte pro Gruppe
grouped.mean() # oder .min, .max, count, ...
# Mehrere Gruppierungen und Aggregationen
grouped = df.groupby(['Survived', 'Sex'])
grouped.Age.agg(['min', 'max', 'median', 'mean'])
# Mehrere Aggregationen gleichzeitig
df.groupby('Gruppe').agg({
'Umsatz': ['sum', 'mean'],
'Anzahl': 'count'
})
# Gruppenspezifische Transformation
df['z-Score'] = grouped['Wert'].transform(lambda x: (x - x.mean()) / x.std())
13 Performance-Optimierung
Große Datenmengen lassen sich durch gezielte Optimierungen effizienter verarbeiten.
13.1 Keine Python-Schleifen verwenden
Der folgende Code ist extrem langsam (Ausführungszeit ca. 500 ms):
col = 'A'
for _, row in df.iterrows():
if row[col] < 0.5:
row[col] = 0.0
Deutlich schneller ist es mit lambda-Funktion (Ausführungszeit ca. 2,4 ms):
df['A'] = df['A'].apply(lambda x: 0.0 if x < 0.5 else x)
Noch schneller ist es mit where-Methode (Ausführungszeit ca. 250 µs) :
df['A'] = np.where(df['C'] < 0.5, 0.0, df['C'])
Vektorisierung statt Schleifen
# Statt: df['x_neu'] = [x+1 for x in df['x']]
df['x_neu'] = df['x'] + 1
13.2 Daten aufbereiten
Datentypen verkleinern
df['int_spalte'] = df['int_spalte'].astype('int32')
df['float_spalte'] = df['float_spalte'].astype('float32')
Nur benötigte Spalten laden
pd.read_csv('daten.csv', usecols=['Name', 'Alter'])
13.3 Weitere Möglichkeiten
Weitere Leistungssteigerungen kann man durch die Verwendung von Cython oder Numba erreichen. Beide generieren C-Code. Da dieser maschinennäher ist, erreicht damit eine geringere Ausführungszeit
Matplotlib und Seaborn
Matplotlib und Seaborn sind zwei Bibliotheken zur Datenvisualisierung - also zum Erstellen von Diagrammen und Grafiken,n. Hier eine einfache Erklärung.
Matplotlib
- Eine grundlegende Bibliothek zur Visualisierung.
- Ermöglicht die Erstellung von Diagrammen.
- Sehr flexibel und anpassbar, jedoch manchmal etwas kompliziert und “low-level”.
- Das Modul
pyplot(oft importiert alsplt) ist der am häufigsten genutzte Teil.
Seaborn
- Baut auf Matplotlib auf - eine Erweiterung, die einfacher zu nutzen ist.
- Hat ein schöneres Design und liefert automatisch ansprechende Diagramme.
- Besonders gut für statistische Grafiken wie Boxplots, Violinplots, Heatmaps usw.
- Ist auf die Verwendung mit Pandas-DataFrames ausgelegt.
- Für das gleiche Ergebnis benötigt man mit Matplotlib häufig deutlich mehr Code als mit Seaborn.
kurz gesagt:
- Matplotlib = leistungsstark & flexibel (manchmal jedoch umständlich)
- Seaborn = einfacher, schöner & perfekt für schnelle Analysen
1 Matplotlib
1.1 Grundlagen
1.1.1 Import
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('dark_background')
Wie np bei NumPy und pd bei Pandas ist plt eine geläufige Bezeichnung und sollte nicht geändert werden.
1.1.2 Zeichnen einer Funktion
# x-Werte definieren
x = np.linspace(
start=0,
stop=2 * np.pi,
num=50,
)
# Grafik und Axen initialisieren
fig = plt.figure()
ax = plt.axes()
# Funktion festlegen
y = np.sin(x)
# Grafik zeichnen und anzeigen
ax.plot(x, y)
plt.show()
1.1.3 Plotten einer Linie
x = [1, 2, 3, 4]
y = [10, 20, 25, 30]
plt.plot(x, y)
plt.show()
1.1.4 Achsenbeschriftung, Titel & Legende
plt.plot(x, y, label='Werte')
plt.xlabel('X-Achse')
plt.ylabel('Y-Achse')
plt.title('Ein einfacher Plot')
plt.legend()
plt.show()
1.1.5 Linienstil, Marker, Farben
plt.plot(x, y, color='red', linestyle='--', linewidth=2, marker='o', markersize=8)
1.2 Diagrammtypen
1.2.1 Scatter Plot
plt.scatter(x, y, color='blue', marker='x', s=100, alpha=0.7)
1.2.2 Balkendiagramm
plt.bar(['A', 'B', 'C'], [10, 20, 15], color='green')
plt.barh(['A', 'B', 'C'], [10, 20, 15], color='orange')
1.2.3 Histogramm
data = [1, 2, 2, 3, 3, 3, 4, 4, 5]
plt.hist(data, bins=5, color='purple', edgecolor='black')
1.2.4 Boxplot
data1 = [1, 2, 5, 7, 8]
data2 = [2, 3, 6, 8, 9]
plt.boxplot([data1, data2], labels=['Gruppe 1', 'Gruppe 2'])
1.2.5 Mehrere Subplots
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.plot(x, y)
plt.subplot(1, 2, 2)
plt.bar(x, y)
plt.tight_layout()
plt.show()
1.2.6 Speichern
plt.savefig('mein_plot.png', dpi=300, bbox_inches='tight')
2 Seaborn
import seaborn as sns
import matplotlib.pyplot as plt
df = sns.load_dataset('tips')
2.1 Stil und Themes
sns.set_style('whitegrid')
sns.set_context('notebook')
2.2 Plots nach Datentyp
2.2.1 Line Plot
sns.lineplot(data=df, x='total_bill', y='tip')
2.2.2 Scatter Plot mit Gruppierung
sns.scatterplot(data=df, x='total_bill', y='tip', hue='sex', style='smoker', size='size')
2.2.3 Histogramm & Verteilung
sns.histplot(df['total_bill'], kde=True, bins=20)
sns.kdeplot(df['total_bill'], fill=True)
2.2.4 Boxplot & Violinplot
sns.boxplot(data=df, x='day', y='total_bill', hue='sex')
sns.violinplot(data=df, x='day', y='total_bill', inner='quartile')
2.2.5 Stripplot & Swarmplot
sns.stripplot(data=df, x='day', y='total_bill', jitter=True)
sns.swarmplot(data=df, x='day', y='total_bill')
2.3 Korrelation & Matrix-Plots
2.3.1 Heatmap
corr = df.corr(numeric_only=True)
sns.heatmap(corr, annot=True, cmap='coolwarm', linewidths=0.5)
2.3.2 Pairplot
sns.pairplot(df, hue='sex', diag_kind='kde')
2.3.3 Jointplot
sns.jointplot(data=df, x='total_bill', y='tip', kind='reg')
2.4 Nützliche Tipps & Tricks
| Aufgabe | Matplotlib | Seaborn |
|---|---|---|
| Farbe ändern | color='red' | palette='pastel' oder hue="Kategorie" |
| Transparenz | alpha=0.5 | alpha=0.5 |
| Figurgröße | plt.figure(figsize=(8, 4)) | plt.figure(figsize=(8, 4)) davor verwenden |
| Achsenticks drehen | plt.xticks(rotation=45) | plt.xticks(rotation=45) |
| Farbschema | – | palette='Set2' (oder ‘deep’, ‘muted’, etc.) |
| Theme | – | sns.set_style('darkgrid') |
Scipy, Sklearn und OpenCV
Scipy, Sklearn und OpenCV sind Bibliotheken, die in der Datenverarbeitung, im maschinellen Lernen und in der Bildverarbeitung häufig verwendet werden.
SciPy:
- Name: Scientific Python
- Zweck: Wissenschaftliches Rechnen und technische Berechnungen
- Typische Anwendungen:
- Numerische Integration
- Optimierung
- Signalverarbeitung
- Statistik
- Lineare Algebra
- Beispiel: Wenn man z.B. ein Differentialgleichungssystem lösen will oder Fourier-Transformationen braucht.
- Verwandt mit: NumPy (SciPy baut darauf auf)
Scikit-learn (sklearn):
- Name: Scikit-learn
- Zweck: Maschinelles Lernen
- Typische Anwendungen:
- Klassifikation (z.B. Spam vs. Nicht-Spam)
- Regression (z.B. Preisvorhersage)
- Clustering (z.B. Kundensegmentierung)
- Modellbewertung und -auswahl
- -> Man hat Daten gesammelt und will aus diesem lernen
- Beispiel: Ein Entscheidungsbaum-Modell auf einem Datensatz trainieren.
- Verwandt mit: Pandas, NumPy, Matplotlib (oft zusammen verwendet)
OpenCV:
- Name: Open Computer Vision
- Zweck: Bild- und Videoverarbeitung
- Typische Anwendungen:
- Objekterkennung
- Gesichtserkennung
- Kamerakalibrierung
- Bildfilter (z.B. Weichzeichnen, Kanten finden)
- Beispiel: Ein Bild einlesen, in Graustufen umwandeln und Kanten erkennen (Canny-Filter).
- OpenCV ist ursprünglich in C++ geschrieben, aber es gibt eine sehr beliebte Python-Schnittstelle.