Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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)

Deutschsprachige Ressourcen

Tutorials & Kurse

Datentypen

1 Immutable (Unveränderlich)

DatentypBeschreibungWertebereich
intGanze ZahlenTheoretisch unbegrenzt (abhängig vom Speicher)
floatGleitkommazahlenCa. ±1.8 × 10³⁰⁸ (IEEE 754, 64-Bit)
complexKomplexe ZahlenKombination aus zwei float-Werten (Real- und Imaginärteil)
boolWahrheitswerte{True, False}
strZeichenkettenBeliebige Zeichenfolgen (Unicode)
tupleTupel (unveränderlich)Beliebige Anzahl von Elementen unterschiedlicher Typen
frozensetUnveränderliche MengeUngeordnete, nicht doppelte Elemente beliebiger immutable Typen
bytesByte-SequenzFolge von Bytes (0–255)
NoneTypeRepräsentiert “kein Wert”{None}

2 Mutable (Veränderlich)

DatentypBeschreibungWertebereich
listListe (Array in anderen Sprachen)Beliebige Anzahl von Elementen unterschiedlicher Typen
dictWörterbuch (Key-Value-Paare)Schlüssel: Immutable Typen, Werte: Beliebige Typen
setMenge (keine doppelten Werte)Ungeordnete, nicht doppelte Elemente beliebiger immutable Typen
bytearrayVeränderbare Byte-SequenzFolge 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

CollectionVerwendungVorteil
CounterElemente zählenEinfache Häufigkeitsanalyse
defaultdictDictionary mit Auto-InitialisierungKein KeyError, weniger Code
dequeQueue/Stack mit Zugriff an beiden EndenO(1) append/pop an beiden Enden
ChainMapMehrere Dictionaries verkettenKonfiguration mit Fallbacks
namedtupleTupel mit benannten FeldernLesbar, 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

AusdruckBedeutung
a == ba ist gleich b
a != ba ist ungleich b
a < ba ist kleiner als b
a > ba ist größer als b
a <= ba ist kleiner oder gleich b
a >= ba ist größer oder gleich b

2 Arithmetische Operatoren

AusdruckBedeutung
a + ba wird zu b addiert
a - bb wird von a subtrahiert
a / ba wird durch b geteilt
a // bGanzzahldivision von a durch b
a % bRest von a durch b
a * ba wird mit b multipliziert
a ** ba hoch b (Potenz)

3 Bitweise Operatoren

AusdruckBedeutung
a & bBitweises AND
a | bBitweises OR
a ^ bBitweises XOR
~aBitweises NOT (Eins-Komplement)
a << bBitweise Linksverschiebung
a >> bBitweise Rechtsverschiebung

4 Logische Operatoren

AusdruckBedeutung
a and bBeide sind wahr (AND)
a or bEiner ist wahr (OR)
not aa ist falsch (NOT)

5 Zusammengesetzte Zuweisungsoperatoren

AusdruckBedeutung
a += bWert addieren und zuweisen (a = a + b)
a -= bWert subtrahieren und zuweisen (a = a - b)
a /= bWert teilen und zuweisen (a = a / b)
a //= bGanzzahldivision und zuweisen (a = a // b)
a *= bWert multiplizieren und zuweisen (a = a * b)
a **= bPotenzieren und zuweisen (a = a ** b)
a |= bBitweises ODER und zuweisen (a = a | b)
a &= bBitweises UND und zuweisen (a = a & b)
a ^= bBitweises XOR und zuweisen (a = a ^ b)
a <<= bLinksverschiebung und zuweisen (a = a << b)
a >>= bRechtsverschiebung 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 match fü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-TypBeispielVerwendung
Literalcase 200:Exakter Wert
Wildcardcase _:Match alles (default)
Capturecase x:Wert in Variable speichern
ORcase 200 | 201 | 204:Mehrere Werte
Sequencecase [x, y, z]:Listen/Tupel mit fester Länge
Sequence (rest)case [first, *rest]:Variable Länge
Mappingcase {"key": value}:Dictionaries
Classcase Point(x, y):Objekte/Dataclasses
Guardcase x if x > 0:Zusätzliche Bedingung
AScase [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) statt len(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:

  • a ist positional-only, weil es vor / steht. $\Rightarrow$ Darf nicht als a=1 übergeben werden. • b kann positional oder keyword sein, weil es zwischen / und * steht. • c ist keyword-only, weil es nach * steht.

6.5 Zusammenfassung / und *

SchreibweiseBedeutung
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:

  1. Das ältere os-Modul (gemeinsam mit os.path)
  2. Das modernere pathlib-Modul

8.1 Übersicht

Funktionos / os.pathpathlib.Path
Pfad erstellenos.path.join()Path() / Path.joinpath()
Existenz prüfenos.path.exists()Path.exists()
Datei/Verzeichnis prüfenos.path.isfile() / .isdir()Path.is_file() / Path.is_dir()
Absoluter Pfados.path.abspath()Path.resolve()
Datei lesen/schreibenopen(path)Path.read_text() / Path.write_text()
Verzeichnisinhalt auflistenos.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: Path ist 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.path kann noch genutzt werden.
  • Beide Module bieten ähnliche Funktionalität, aber pathlib ist 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

PythonJSON
dictobject
list, tuplearray
strstring
int, floatnumber
Truetrue
Falsefalse
Nonenull

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() (immer safe_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

AspektJSONYAMLPickle
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 CaseAPIs, ConfigConfig, CI/CDPython 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() statt yaml.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

FormatZweckVorteil
JSONAPI, Config, DatenaustauschStandard, sicher, schnell
YAMLConfig, CI/CD, menschenlesbarKommentare, lesbar
PicklePython-Cache, temporäre SpeicherungAlle 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

FormatVorteileGeeignet für
NumPyWissenschaftliche StandardsData Science, ML, Forschung
GoogleEinfach zu lesen, klare StrukturAllgemeine Python-Projekte
reST (Sphinx)Unterstützt in automatisierten DokusGroß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

FehlertypBeschreibung
SyntaxErrorFehler in der Code-Syntax
TypeErrorFalscher Typ einer Variablen oder eines Arguments
ValueErrorUngültiger Wert für eine Operation
IndexErrorZugriff auf nicht vorhandenen Index in einer Liste
KeyErrorZugriff auf nicht vorhandenen Schlüssel in einem Dictionary
ZeroDivisionErrorDivision durch Null
FileNotFoundErrorDatei nicht gefunden
ImportErrorModul 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

  • try und except dienen der Fehlerbehandlung.
  • Der else-Block wird ausgeführt, wenn kein Fehler auftritt.
  • Der finally-Block wird immer ausgeführt.
  • Mit raise kö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

  • class definiert eine Klasse.
  • __init__ ist der Konstruktor.
  • super() ruft Methoden der Elternklasse auf.
  • Abstrakte Klassen werden mit ABC definiert.
  • property ermö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, ob obj eine Instanz von cls oder einer abgeleiteten Klasse ist.
  • issubclass(sub, super) prüft, ob sub eine Unterklasse von super ist.
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

  • @staticmethod definiert eine Methode, die keinen Zugriff auf self oder cls benötigt.
  • @classmethod arbeitet mit cls und 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?

  1. Weniger Speicherverbrauch: Jedes Objekt braucht weniger Speicher, weil kein __dict__ mehr angelegt wird.

  2. Schnellerer Zugriff auf Attribute: Statt eines Dictionary-Lookups wird ein schnellerer, indexbasierter Zugriff verwendet.

  3. 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 dict oder als Elemente in einem set zu verwenden.
  • Die Verwendung von frozen=True fü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 Basisklassen
  • dict: 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 (wie cls bei @classmethod)
  • name: Name der zu erstellenden Klasse
  • bases: Tuple der Basisklassen
  • attrs: 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:

  1. Class Decorators (meistens ausreichend)
  2. __init_subclass__ (Python 3.6+)
  3. Descriptor Protocol
  4. 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

KonzeptBeschreibung
typeStandard-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:

  1. Brauche ich wirklich Metaprogrammierung? → Oft: Nein
  2. Reicht ein Class Decorator? → Meistens: Ja
  3. Reicht __init_subclass__? → Oft: Ja
  4. 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

FunktionZweckRückgabe
count()Unendliches Zählen10, 11, 12, …
cycle()Elemente zyklisch wiederholenA, B, C, A, B, …
repeat()Element wiederholenX, 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 verketten1, 2, 3, 4, …
compress()Filtern mit Boolean-Mask[True], [False], …
groupby()Gruppieren (nach Sortierung!)Gruppen nach Key
islice()Slice für IteratorenTeilbereich
accumulate()Kumulative Werte1, 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

  1. Typprüfung und Instanzen

    • Mit type(), isinstance() und issubclass() prüft man Objekttypen und Vererbungsbeziehungen.
  2. Objekt-Erzeugung

    • __new__ erzeugt das Objekt (besonders bei Immutable-Typen wichtig).
    • __init__ initialisiert das Objekt nach der Erzeugung.
  3. 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.
  4. Spezialmethoden (Dunder Methods)

    • Methoden wie __str__, __eq__, __getitem__ erlauben es, benutzerdefinierte Objekte wie eingebaute Typen zu behandeln.
  5. Abstraktion und Vererbung

    • Abstrakte Klassen (ABC, @abstractmethod) erzwingen Implementierungen in Unterklassen.
    • Die Method Resolution Order (MRO) bestimmt die Aufrufreihenfolge bei Mehrfachvererbung.
  6. Iteration und Indexierung

    • Iterator-Klassen und Generator-Funktionen ermöglichen eigene Iterationslogik.
    • Mit __getitem__ und __setitem__ lassen sich Objekte wie Listen verwenden.
  7. Speicher- und Datenrepräsentation

    • @dataclass reduziert Boilerplate für Datenobjekte.
      • __slots__ spart Speicher durch festen Attributsatz.
      • frozen=True macht die Dataclass unveränderbar.
    • namedtuple ist eine kompakte, unveränderliche Datenstruktur mit Feldnamen.
  8. Enumerationen

    • Mit Enum kann man symbolische Konstanten definieren, die lesbar und typsicher sind.
  9. 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): Wendet func auf jedes Element an.
  • filter(func, iterable): Filtert Elemente basierend auf Wahrheitswert von func.
  • 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
  • partial erstellt 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
  • factor bleibt 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_decorator ersetzt say_hello durch wrapper.
  • 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)
  • *args und **kwargs fangen 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 ohne self
  • @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_two wird zuerst angewendet, dann decorator_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

FunktionZweck
lru_cacheMemoization / Ergebnisse cachen
cacheUnbegrenzter Cache (3.9+)
wrapsMetadaten in Decorators erhalten
partialFunktionen teilweise anwenden
reduceIterable auf einzelnen Wert reduzieren
singledispatchFunktions-Overloading nach Typ
cmp_to_keyVergleichsfunktion → Key-Funktion
total_orderingAlle Vergleichsoperatoren generieren
cached_propertyProperty mit einmaliger Berechnung

Best Practices:

  • Immer @wraps in Decorators verwenden
  • lru_cache für teure, wiederholte Berechnungen
  • singledispatch statt if-elif Typ-Checks
  • total_ordering für vergleichbare Klassen
  • cached_property fü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

VerhaltenBeschreibungBeispiel-Use-Case
ThrottleMaximal alle X Sekunden ausführenLive-Preview, Autosave
DebounceNur wenn X Sekunden nichts passiertValidierung, End-Save
KombinationRegelmäßig + abschließend nach RuheMarkdown-Live + Final Save

10 Zusammenfassung

KonzeptNutzen
ClosuresZustand bewahren, Daten kapseln
LambdaKürzere anonyme Funktionen
Higher-Order FuncsFlexible Funktionskomposition
map/filter/reduceFunktionale Verarbeitung von Listen
Partial FunctionsVorbelegte Funktionen
DecoratorsFunktion erweitern ohne Quellcode zu ändern
functoolsWerkzeuge für funktionale Programmierung (cache, wraps, etc.)
Throttle & DebounceEvent-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 bound wird eine Obergrenze festgelegt (z. B. eine Basisklasse oder ein Typ wie float).
  • 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 T und U annehmen.
  • 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

KonzeptBedeutung
TypeVargenerischer Typ
Generic[T]Klasse/Funktion ist generisch
bound=Typ auf Obergrenze beschränken
constraintsListe erlaubter Typen
Protocolstrukturelle Typprüfung
TypedDicttypisierte 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 Objekts p zurück, nämlich Point. Daher wird in dem Beispiel der Vergleich falsch ausgewertet.
  • isinstance() überprüft, ob p eine Instanz von tuple oder einer Unterklasse davon ist. Da namedtuple eine Unterklasse von tuple ist, gibt isinstance(p, tuple) True zurü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

OptionBedeutung
--strictAktiviert alle strengen Checks
--ignore-missing-importsIgnoriert fehlende Type Stubs von Drittbibliotheken
--disallow-untyped-defsVerlangt Typen für alle Funktionsdefinitionen
--check-untyped-defsPrüft auch Funktionen ohne Typannotationen
--warn-return-anyWarnt bei Any als Rückgabetyp
--show-error-codesZeigt 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

KriteriummypyPyright
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ürCI/CD, CommandlineVS 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:

  1. Kritische/neue Module zuerst typisieren
  2. # type: ignore für Legacy-Code nutzen
  3. Schrittweise strengere mypy-Optionen aktivieren
  4. 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
  • strict Mode 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 Any verwenden (verliert Typsicherheit)
  • Type Checking bei Tests vernachlässigen
  • # type: ignore ohne 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

ToolVerwendung
mypyStandard Type Checker, CLI, CI/CD
PyrightSchneller Checker, VS Code Integration
Type StubsTypen für Drittbibliotheken
# type: ignoreEinzelne 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:

  1. 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.
  2. 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-Modul
  • sys.getrefcount(obj): Anzahl der Referenzen auf ein Objekt
  • id(obj): Gibt die Speicheradresse des Objekts zurück
  • hex(id(obj)): Speicher Adresse als HEX

1.4 Zusammenfassung

FeatureBeschreibung
HeapSpeicherort aller Objekte
ReferenzzählungBasis-Mechanismus zur Speicherfreigabe
Zyklische GCEntfernt Objektzyklen, die Referenzzähler nicht abfängt
gc-ModulErmö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
  • is prü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.
  • is prü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

  • int
  • float
  • str
  • tuple
  • frozenset
  • bool
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

  • list
  • dict
  • set
  • bytearray
  • 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

DatentypMutable
intNein
strNein
boolNein
tupleNein (aber Inhalt kann mutable sein)
listJa
dictJa
setJa
user classAbhä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]]
  • shallow und original teilen 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]]
  • deep ist vollständig unabhängig von original.
  • Ä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?

SituationEmpfehlung
Nur äußere Struktur kopierencopy.copy()
Vollständig unabhängig kopierencopy.deepcopy()
Performance wichtig, Inhalt flachlist(), 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 deepcopy den 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:

  • tracemalloc fü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:

  • tracemalloc in Produktion laufen lassen (Performance-Overhead ~2-3x)
  • Zu häufig Snapshots nehmen (selbst speicherintensiv)
  • Ohne Filter arbeiten bei großen Projekten
  • start() ohne stop() 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

FunktionZweck
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__

ParameterBeschreibung
exc_typeTyp der Exception (z.B. ValueError) oder None
exc_valueException-Objekt oder None
tracebackTraceback-Objekt oder None
RückgabewertTrue 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 yield entspricht __enter__
  • yield gibt den Wert zurück (wie return in __enter__)
  • Code nach yield entspricht __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@contextmanagerKlasse 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

KonzeptVerwendung
with-StatementAutomatisches Setup/Cleanup von Ressourcen
__enter__ / __exit__Kontextmanager als Klasse implementieren
@contextmanagerGenerator-basierter Kontextmanager (einfacher)
contextlib.suppressExceptions unterdrücken
contextlib.redirect_stdoutAusgaben umleiten
contextlib.ExitStackDynamische Anzahl von Kontextmanagern
async withAsynchrone 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 aus
  • s: step – springt in Funktionsaufrufe hinein
  • c: continue – setzt das Programm bis zum nächsten Haltepunkt fort
  • q: quit – verlässt den Debugger
  • p 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.

LevelBeschreibung
DEBUGDetaillierte Debug-Informationen
INFONur zur Info: Dinge, die wie vorgesehen passiert sein
WARNINGWenn etwas Unerwartetes passiert ist, es hat jedoch nicht zu einem Absturz geführt hat
ERRORSchwerwiegender Fehler, der nicht zu einem Programmabsturz geführt hat (z. B. das Programm konnte eine bestimmte Funktion nicht ausführen)
CRITICALSchwerwiegender, 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

loguru

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

FeatureVerwendung
FixturesSetup/Teardown, Wiederverwendung
ParametrizeMehrere Input/Output-Kombinationen
MarksTests kategorisieren/überspringen
MockingExterne Dependencies ersetzen
CoverageCode-Abdeckung messen
conftest.pyGemeinsame 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:

  1. 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.
  2. 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.
  3. 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:

  1. Hohe Auflösung
    • Nutzt die genaueste verfügbare Uhr des Systems.
    • Auf modernen Systemen meist Nanosekunden- oder Mikrosekunden-genau.
  2. Monoton steigend
    • Kann nicht durch Systemzeitänderungen beeinflusst werden.
    • Die Werte steigen immer an, niemals rückwärts.
  3. 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:

  1. Einen Test schreiben, der fehlschlägt (weil es die Funktion noch nicht gibt).
  2. Den minimalen Code implementieren, der den Test bestehen lässt.
  3. Refaktorisiere, falls nötig, und sicherstellen, dass der Test weiterhin grün ist.
  4. 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 unittest Methoden wie assertEqual(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, hypothesis uvm.

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, pytest oder nose als Framework auswählen.
  • Danach: Python: Run All Tests oder Run Test direkt über dem Test mit dem kleinen „Play“-Icon.

[!INFO] VS Code erkennt pytest automatisch, wenn die Datei mit test_*.py beginnt und assert-Statements enthält.

5.2 Profiling mit Erweiterung: “Python Profile”

  1. Optional: Erweiterung “Python Profiler” installieren.
  2. Datei öffnen
  3. Einen Breakpoint setzen oder über Run → Start Debugging ausführen.
  4. Man kann cProfile-Ausgaben direkt im Terminal oder über Plugins visualisieren lassen.

5.3 Kurzbefehle

AktionShortcut
Testdatei ausführenCtrl+F5 oder ▶ oben
Terminal öffnen`Ctrl+``
Befehlspalette öffnenCtrl+Shift+P
Tests entdeckenPython: Discover Tests
Nur einen Test ausführenRechtsklick > 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 (.h oder .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 #include benutzt 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 #include die 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

AspektctypescffiPython 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:

ToolSprachePerformanceMemory SafetyKomplexität
Pybind11C++✅ Sehr hoch⚠️ ManuellMittel
PyO3Rust✅ Sehr hoch✅ GarantiertMittel
ctypesC✅ Hoch❌ UnsicherNiedrig
CythonPython✅ Hoch⚠️ ManuellNiedrig

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

AspektPyO3 (Rust)Pybind11 (C++)
SpracheRustC++
Memory Safety✅ Garantiert⚠️ Manuell
Performance✅✅ Sehr hoch✅✅ Sehr hoch
Build-Toolmaturin, CargoCMake, setuptools
Learning Curve⚠️ Rust-Kenntnisse⚠️ C++-Kenntnisse
Ökosystemcrates.iovcpkg, conan
Async Support✅ Tokio⚠️ Komplex

8.12 Best Practices

✅ DO:

  • Nutze maturin develop --release fü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

ToolUse CaseComplexityPerformance
ctypesQuick FFI, System LibsLowMedium
cffiModern FFI, PyPy-compatibleMediumHigh
Python C-APIFull control, NumPy-likeHighVery High
Pybind11C++ IntegrationMediumVery High
CythonPython-like, gradual optimizationLow-MediumHigh

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

ToolSpracheAnwendungsfallKomplexitätPerformanceSpeichersicherheitBuild-ZeitLernkurvePyPy-Support
ctypesCSchneller FFI, System-LibsNiedrigMittel❌ UnsicherKeine✅ Einfach⚠️ Langsam
cffiCModern FFI, PyPy-kompatibelMittelHoch⚠️ ManuellSchnell✅ Einfach✅ Schnell
Python C-APICVolle Kontrolle, NumPy-ähnlichHochSehr hoch❌ UnsicherMittel❌ Schwer❌ Nein
Pybind11C++C++-Integration, modernMittelSehr hoch⚠️ ManuellLangsam⚠️ Mittel❌ Nein
PyO3RustModern, sicher, performantMittelSehr hoch✅ SicherMittel⚠️ Mittel❌ Nein
CythonPython+CPython-ähnlich, graduelle OptimierungNiedrig-MittelHoch⚠️ ManuellMittel✅ Einfach❌ Nein
NumbaPythonJIT, NumPy-fokussiertNiedrigSehr hoch✅ SicherKeine (JIT)✅ Einfach❌ Nein
MypycPythonTypisiertes Python → CNiedrigHoch✅ SicherMittel✅ Einfach❌ Nein
PyPyPythonJIT für reines PythonKeineSehr hoch✅ SicherKeine (JIT)✅ Keine✅ Nativ
NuitkaPythonPython → C CompilerNiedrigHoch✅ SicherLangsam✅ 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

ToolGeeignet fürVermeiden beiÖkosystemAsync-Support
ctypesSystem-Libs, PrototypingKomplexe InteraktionenStandard-LibN/A
cffiModerne C-Libs, PyPyEinfache Aufgaben (übertrieben)PyPIN/A
Python C-APINumPy-ähnliche ExtensionsEinfache OptimierungenNur CPythonKomplex
Pybind11Moderner C++-CodeReiner C-CodeHeader-onlyKomplex
PyO3Moderne Rust-IntegrationEinfache Aufgabencrates.io✅ Tokio
CythonGraduelle OptimierungReine Python-AlternativenPyPIEingeschränkt
NumbaNumPy-Arrays, SchleifenString-Ops, komplexe ObjekteConda/PyPIEingeschränkt
MypycTypisiertes Python beschleunigenDynamisches PythonMyPy-ÖkosystemEingeschränkt
PyPyLang laufendes reines PythonKurze Scripts, C-ExtensionsPyPI (begrenzt)✅ Nativ
NuitkaGanzes Programm optimierenEntwicklung (langsame Kompilierung)StandaloneNatives Python

8.3 Entscheidungshilfe

Szenario: Numerische Berechnungen mit ArraysNumba (einfachste Option) oder Cython (volle Kontrolle)

Szenario: Vorhandene C-Bibliothek einbindencffi (modern) oder ctypes (schnell & einfach)

Szenario: Vorhandene C++-CodebasisPybind11

Szenario: Moderne, sichere ExtensionPyO3 (Rust) oder Pybind11 (C++)

Szenario: Python-Code beschleunigen ohne neue SprachePyPy (reines Python) oder Numba (NumPy-fokussiert)

Szenario: Typisiertes Python zu nativem CodeMypyc

Szenario: Python + C HybridCython

Szenario: Maximale Performance, volle KontrollePython C-API oder PyO3 (mit Speichersicherheit)

Szenario: Distribution als BinaryNuitka (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 await wird 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

KriteriumThreadsProcesses
SpeicherGemeinsamer SpeicherGetrennter Speicher
StartzeitSchnellEtwas langsamer
CPU-bound PerformanceSchlecht (wegen GIL)Gut
I/O-bound PerformanceGutGut
KommunikationEinfach (gemeinsamer Speicher)Komplexer (IPC nötig)
Nutzung des GILJaNein

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
SpeicherGemeinsamGetrenntGemeinsam (1 Thread)
KommunikationEinfachKomplex (IPC nötig)Intern (await/gather)
Nutzung von TasksThreadsProzesseCoroutines
StartzeitSchnellLangsamerSehr 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:

  1. Built-in Modules (in Python kompiliert)
  2. Aktuelles Verzeichnis (wo das Script liegt)
  3. PYTHONPATH Environment Variable
  4. Standard-Library-Pfade
  5. 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:

  1. Python prüft sys.modules (Cache) ob Modul bereits geladen
  2. Falls nicht: Suche nach Modul in sys.path
  3. Modul-Datei wird gefunden und kompiliert zu Bytecode (.pyc)
  4. Code wird ausgeführt
  5. Modul-Objekt wird in sys.modules gecacht
  6. 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

KonzeptZweck
sys.pathListe der Suchpfade für Module
sys.modulesCache geladener Module
importlibProgrammatisches Importieren
PYTHONPATHEnvironment Variable für Suchpfade
__init__.pyPaket-Initialisierung und Exporte
__all__Kontrolliert from package import *
Namespace PackagesPackages 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:

  1. 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: .
  1. docs/requirements.txt:
sphinx>=5.0
sphinx-rtd-theme
  1. GitHub Repository → Read the Docs verbinden
  2. 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

ToolKomplexitätFeaturesFormatUse Case
SphinxHochSehr vielereStructuredGroße Projekte, Standard
MkDocsNiedrigMittelMarkdownModerne Docs, einfach
pdocSehr niedrigAutomatischDocstringsSchnelle 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

AspektEmpfehlung
Docstring-StilGoogle Style (lesbar, klar)
Große ProjekteSphinx + Read the Docs
Einfache DocsMkDocs + Material Theme
Schnelle APIpdoc (automatisch)
HostingRead the Docs / GitHub Pages
Validierungpydocstyle / 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:

  • venv ist 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/.

  • 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

ToolTypSpracheGeschwindigkeitConformanceLSP/IDEStatus
RuffLinterRust✅✅ Extrem schnellHoch⚠️ Kein Type LSP✅ Stabil
Flake8LinterPython❌ LangsamMittel❌ Nein⛔ Obsolet
PylintLinterPython❌ Sehr langsamSehr hoch❌ Nein⛔ Obsolet
PyrightType CheckerTypeScript✅ Schnell✅✅ Sehr hoch✅ Pylance (VS Code)✅ Stabil
mypyType CheckerPython⚠️ Mittel✅ Hoch❌ Nein✅ Stabil
tyType CheckerRust✅✅ Extrem schnell❌ ~15%✅ Eingebaut⚠️ Beta
PyreflyType CheckerRust✅ Sehr schnell⚠️ ~70%✅ Eingebaut⚠️ Beta
ZubanType CheckerRust✅ Schnell✅ ~69%✅ Eingebaut⚠️ Beta
PyreType CheckerOCaml⚠️ 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 → Ruff
  • black → Ruff format
  • isort → Ruff
  • Pylint → 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

  1. Im Terminal zum Ordner des Pakets wechseln (cd ...)
  2. Richtige Umgebung auswählen (conda activate ...)
  3. 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:

  1. Paket deinstallieren:
pip uninstall <paketname> -y
  1. .egg-Datei löschen !!

Diese Datei löschen:

/opt/anaconda3/envs/<envname>/lib/python<version>/site-packages/<paketname>.egg-link
  1. 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

Featurepip + venvpip-toolsPoetryHatch
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

  1. Installation
pip install auto-py-to-exe
  1. Ausführen:
auto-py-to-exe
  1. Konfigurationsdatei auto-py-to-exe_settings.json aus Projekt laden
  2. 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

  1. Installation
pip install pyinstaller
  1. 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'
  1. Richtiges Verhalten prüfen: In der DoneZilla.spec ist eingestellt, dass die Qt-Dateien NICHT im App-Bundle enthalten sind. Für LGPL-Konformität werden die Qt-Dateien aus dem Ordner external_libs geladen. Das heißt, sollte der Ordner nicht existieren oder anders heißen, darf die App nicht starten.
  2. Ordner external_libs mit den Unterordnern PySide6 und shiboken6 in den gleichen Ordner wie das App-Bundle platzieren
  3. Ordner external_libs aufrä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

ToolPlatformBesonderheit
PyInstallerAllStandard, feature-reich
auto-py-to-exeWindowsGUI für PyInstaller
cx_FreezeAllAlternative zu PyInstaller
py2appmacOSmacOS-spezifisch
py2exeWindowsWindows-only, veraltet
NuitkaAllKompiliert zu C (schneller)
PyOxidizerAllRust-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

ToolZweckEmpfohlen für
venvVirtual EnvironmentsAlle Projekte
pipPaket-InstallationBasis-Tool
pip-toolsDependency-PinningDeterministische Builds
PoetryAll-in-One ManagementNeue Projekte, Libraries
HatchAlternative zu PoetryModerne Projekte, Multi-Python
pyproject.tomlStandard-KonfigurationAlle modernen Projekte

Moderne Empfehlung:

  • Einfache Projekte: venv + pip + pyproject.toml
  • Professionelle Projekte: pip-tools oder Poetry
  • Library-Entwicklung: Poetry oder Hatch
  • Legacy-Migration: Schrittweise zu pyproject.toml wechseln

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_FunctionDef wird 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 + 3 durch 5.
  • 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_FALSE und 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

ThemaZweck
disAnalyse des Bytecodes
astStrukturierte Analyse und Manipulation von Code
Flow GraphAnalyse 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

Kriteriumrequestsaiohttp
Synchron/AsyncSynchron (blocking)Asynchron (non-blocking)
Performance (single)✅ Ausreichend⚠️ Etwas Overhead
Performance (parallel)❌ Langsam (sequenziell)✅ Sehr schnell
Einfachheit✅ Sehr einfach⚠️ Async-Kenntnisse nötig
Use CasesNormale Scripts, CLI-ToolsWeb Scraping, viele APIs
HTTP/2 Support✅ (mit aioh2)
Ecosystem✅ Riesig✅ Wachsend

Faustregel:

  • requests: Für normale Scripts, wenige (<10) Requests, Einfachheit
  • aiohttp: Für viele parallele Requests (>50), Web Scraping, Performance-kritisch

13 Zusammenfassung

ThemaVerwendung
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
timeoutMaximale Wartezeit definieren
headersCustom Headers, Authentication
aiohttpAsynchrone 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 aiohttp verwenden

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/KonzeptVerwendung
sqlite3Einfache, dateibasierte Datenbank
cursor.execute()SQL-Befehle ausführen
conn.commit()Änderungen speichern
SQLAlchemy EngineDatenbankverbindung
SQLAlchemy ORMPython-Objekte ↔ Datenbank
BaseBasis-Klasse für Models
relationship()Beziehungen zwischen Tabellen
session.query()Daten abfragen
joinedload()Eager Loading (N+1 Problem vermeiden)
AlembicDatenbank-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

FrameworkBasisPaketePerformanceLadezeitPython-Version
PyScriptPyodide/WASM✅✅ Viele⚠️ Mittel⏱️ Lang3.11
PyodideWASM✅✅ Viele⚠️ Mittel⏱️ Lang3.11
BrythonJS⚠️ Wenige✅ Schneller✅ Kurz3.10
SkulptJS❌ Minimal✅ Schneller✅ Kurz2.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

AspektBewertung
Einstieg✅ Einfach (HTML + Python)
Performance⚠️ Langsamer als JS, aber akzeptabel
Use CasesVisualisierung, 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

  1. Numpy: fundamentale mathematische Operationen auf Vektoren und Matrizen
  2. Pandas: Microsoft Excel auf Steroide
  3. Matplotlib: Erstellung von grafischen Darstellungen mit wenig Code
  4. Scipy: Implementiert alles, was Numpy nicht bietet

NumPy

NumPy user guide

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 n ganzen Zahlen indexiert werden.
  • Alle ndarrays sind homogen: Jedes Element belegt einen gleich großen Speicherblock.
  • Ein Element aus dem Array wird durch ein PyObject reprä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-TypC-TypBeschreibung
bool_boolBoolean (True oder False)
int8signed charGanze Zahl, 8 Bit, Bereich: -128 bis 127
uint8unsigned charGanze Zahl, 8 Bit, Bereich: 0 bis 255
int16shortGanze Zahl, 16 Bit, Bereich: -32k bis 32k
uint16unsigned shortGanze Zahl, 16 Bit, positiv
int32intGanze Zahl, 32 Bit
uint32unsigned intGanze Zahl, 32 Bit, positiv
int64long / int64_tGanze Zahl, 64 Bit
uint64unsigned longGanze Zahl, 64 Bit, positiv
float16halfFließkommazahl, 16 Bit (geringere Genauigkeit)
float32floatFließkommazahl, 32 Bit
float64doubleFließkommazahl, 64 Bit (Standard)
complex64float complexKomplexe Zahl (2×32 Bit floats)
complex128double complexKomplexe 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

FunktionBeschreibung
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

FunktionBeschreibung
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-Ergebnisse
  • axis=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)
  • 1 funktioniert wie ein Platzhalter → es wird “in diese Richtung” kopiert
  • Fehlende Dimension? → einfach eine 1 davor 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:

AusdruckBedeutung
==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)
FunktionBeschreibung
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.

DatentypBeschreibung
int8Ganze Zahl, 8 Bit
int16Ganze Zahl, 16 Bit
int32Ganze Zahl, 32 Bit (Standard für int)
int64Ganze Zahl, 64 Bit
uint8Ganze Zahl, 8 Bit, positiv
uint16Ganze Zahl, 16 Bit, positiv
uint32Ganze Zahl, 32 Bit, positiv
uint64Ganze Zahl, 64 Bit, positiv
float16Fließkommazahl, 16 Bit (geringe Genauigkeit)
float32Fließkommazahl, 32 Bit
float64Fließkommazahl, 64 Bit (Standard für float)
boolBoolescher Wert (True/False)
objectBeliebiger Python-Objekttyp
stringPandas-eigener Stringtyp (mit NA-Unterstützung)
categoryKategorischer 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

  1. string: kein object mehr, kann sauber mit fehlenden Werten umgehen
  2. category: für Zellen mit wenigen verschiedenen Werten (z. B. Geschlecht = männlich oder weiblich)
  3. Datum: datetime64[ns] und timedelta[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=1 angeben, 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

  • NaN zählt bei mean(), sum() usw. nicht mit.
  • Wenn man mit Strings arbeitet, verwendet man pd.NA und den Datentyp string, 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:

  1. Daten in Gruppen aufteilen (split)
  2. Funktion anwenden (apply)
  3. 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 als plt) 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

AufgabeMatplotlibSeaborn
Farbe änderncolor='red'palette='pastel' oder hue="Kategorie"
Transparenzalpha=0.5alpha=0.5
Figurgrößeplt.figure(figsize=(8, 4))plt.figure(figsize=(8, 4)) davor verwenden
Achsenticks drehenplt.xticks(rotation=45)plt.xticks(rotation=45)
Farbschemapalette='Set2' (oder ‘deep’, ‘muted’, etc.)
Themesns.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.