Fortgeschrittene Objektorientierung
1 type(), isinstance() und issubclass()
Diese eingebauten Funktionen helfen beim Umgang mit Typen und Vererbung.
type(obj)gibt den exakten Typ des Objekts zurück.isinstance(obj, cls)prüft, obobjeine Instanz vonclsoder einer abgeleiteten Klasse ist.issubclass(sub, super)prüft, obsubeine Unterklasse vonsuperist.
class Animal: pass
class Dog(Animal): pass
a = Animal()
d = Dog()
print(type(d)) # <class '__main__.Dog'>
print(isinstance(d, Animal)) # True
print(issubclass(Dog, Animal)) # True
Diese Funktionen sind wichtig für dynamisches Verhalten, Validierung und Typprüfung.
2 __init__ vs __new__
__new__ist für das Erzeugen eines neuen Objekts zuständig.__init__wird anschließend aufgerufen, um das Objekt zu initialisieren.
class Custom:
def __new__(cls, *args, **kwargs):
print('Creating instance')
return super().__new__(cls)
def __init__(self, value):
print('Initializing with', value)
self.value = value
obj = Custom(42)
__new__ wird z. B. bei unveränderlichen Typen wie str oder tuple benötigt, wenn diese beeinflusst werden sollen oder in der Metaprogrammierung (Singleton-Pattern).
3 Methodenarten
3.1 StaticMethods und ClassMethods
@staticmethoddefiniert eine Methode, die keinen Zugriff aufselfoderclsbenötigt.@classmethodarbeitet mitclsund kann so auf die Klasse zugreifen.
class Example:
@staticmethod
def utility():
print('Static method called')
@classmethod
def construct(cls):
print(f'Creating instance of {cls.__name__}')
return cls()
Example.utility()
obj = Example.construct()
classmethod wird häufig für alternative Konstruktoren verwendet.
3.2 Properties (Private Attribute)
Mit @property können Methoden wie Attribute verwendet werden. Das ist nützlich für gekapselte Attribute, z. B. mit Validierung oder automatischer Berechnung.
class Person:
def __init__(self, name):
self._name = name # Private Konvention
@property
def name(self):
return self._name
@name.setter
def name(self, new):
self._name = new
p = Person('Alice')
print(p.name)
p.name = 'Bob'
print(p.name)
Durch Properties kann die API einfach bleiben, während intern komplexe Logik stattfinden kann.
4 Dunder Methods
Dunder (Double Underscore) Methoden ermöglichen benutzerdefiniertes Verhalten für Operatoren und eingebaute Funktionen (__str__, __len__, __getitem__ usw.).
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f'({self.x}, {self.y})'
def __eq__(self, other):
return self.x == other.x and self.y == other.y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(str(p1)) # (1, 2)
print(p1 == p2) # True
Diese Methoden machen Objekte “pythonisch”.
5 Abstraktion und Vererbung
5.1 Abstract Methods
Das abc-Modul erlaubt die Definition abstrakter Basisklassen. Methoden mit @abstractmethod müssen in Subklassen implementiert werden.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def area(self):
return 3.14 * 5 * 5
Abstrakte Methoden zwingen Unterklassen zur Implementierung und helfen beim Design stabiler APIs.
5.2 Method Resolution Order
Python verwendet das C3 Linearization-Verfahren, um die Reihenfolge von Vererbungen zu bestimmen. Die mro()-Methode zeigt die Aufrufreihenfolge.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.mro()) # [D, B, C, A, object]
Wichtig bei Mehrfachvererbung.
6 Interation und Indizierung
6.1 Iterator-Klasse und Generator-Funktion
Ein Iterator benötigt die Methoden __iter__ und __next__. Alternativ kann yield verwendet werden.
class Count:
def __init__(self, max):
self.max = max
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current >= self.max:
raise StopIteration
self.current += 1
return self.current
Generatoren vereinfachen Iteration:
def count_up_to(max):
current = 0
while current < max:
current += 1
yield current
6.2 Indexing-Klasse
Mithilfe von __getitem__ und __setitem__ können Objekte wie Listen verwendet werden.
class CustomList:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index]
def __setitem__(self, index, value):
self.data[index] = value
Sehr nützlich für eigene Containerklassen.
7 Datenpräsentation und Speicheroptimierung
7.1 Dataclass
Dataclasses automatisieren Konstruktor, Vergleich und Darstellung.
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
7.1.1 Slots
Mit __slots__ wird der Speicherverbrauch reduziert und der Zugriff beschleunigt.
class Slim:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
Seit Python 3.10 ist folgendes möglich (gleichwertiger Code):
@dataclass(slots=True)
class Slim:
x: int
y: int
was macht `__slots__` bzw. `slots=true`?
Normalerweise speichert Python Objektattribute in einem internen Dictionary namens __dict__. Das ist flexibel, jedoch nicht speichereffizient oder performant.
Wenn __slots__ bzw. slots=True verwendet wird, wird dieses Dictionary durch ein festeres Layout ersetzt, bei dem nur die im Slot definierten Felder erlaubt sind.
Warum verbessert das die Leistung?
-
Weniger Speicherverbrauch: Jedes Objekt braucht weniger Speicher, weil kein
__dict__mehr angelegt wird. -
Schnellerer Zugriff auf Attribute: Statt eines Dictionary-Lookups wird ein schnellerer, indexbasierter Zugriff verwendet.
-
Bessere Caching-Effekte: Schlankere Objekte passen besser in den Cache, was zu weiteren Geschwindigkeitsgewinnen führen kann – besonders bei großen Listen von Objekten.
# Ohne __slots__ (Standard)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# Mit __slots__
class PersonSlotted:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
Speichervergleich:
import sys
p1 = Person('Alice', 30)
p2 = PersonSlotted('Alice', 30)
print(sys.getsizeof(p1.__dict__)) # ~232 bytes
print(sys.getsizeof(p2)) # ~56 bytes
Vorteile von __slots__
1. Reduzierter Speicherverbrauch:
- Kein
__dict__pro Instanz - Besonders wichtig bei vielen Objekten (z.B. 1 Million Instanzen)
2. Schnellerer Attributzugriff:
- Direkter Array-Zugriff statt Dictionary-Lookup
- ~20-30% schneller
3. Typsicherheit:
- Nur definierte Attribute erlaubt
- Verhindert Tippfehler
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(10, 20)
p.z = 30 # AttributeError: 'Point' object has no attribute 'z'
7.1.2 frozen=True
Das frozen=True-Argument macht die Instanz unveränderlich (immutable):
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
print(p.x) # -> 1
p.x = 10 # -> Fehler: cannot assign to field 'x'
- Man kann keine Attribute mehr ändern nach der Erstellung der Instanz.
- Python generiert automatisch einen
setattr, der Änderungen verhindert. - Die Klasse wird hashbar (vorausgesetzt alle Felder sind auch hashbar), was z. B. erlaubt, sie als Schlüssel in einem
dictoder als Elemente in einemsetzu verwenden. - Die Verwendung von
frozen=Trueführt zu einer Leistungssteigerung, diese ist jedoch minimal.
7.2 Namedtuple
Ein namedtuple ist ein leichtgewichtiger, unveränderlicher Datenträger mit benannten Feldern.
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
print(p.x, p.y)
Effizient und leserlich – eine gute Alternative zu kleinen Klassen.
8 Enum
Enum erlaubt es, symbolische Konstanten mit Namen zu versehen.
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
Gleichbedeutend mit dem obigen Beispiel ist:
from enum import Enum
class Color(Enum):
RED = auto()
GREEN = auto()
BLUE = auto()
Enums verbessern die Lesbarkeit und Typsicherheit im Code.
9 Class Decorator
Class Decorators modifizieren Klassen beim Erzeugen. Sie eignen sich für Registrierung, Debugging oder Vererbung.
def debug(cls):
original_init = cls.__init__
def new_init(self, *args, **kwargs):
print(f'Creating {cls.__name__} with {args}, {kwargs}')
original_init(self, *args, **kwargs)
cls.__init__ = new_init
return cls
@debug
class Product:
def __init__(self, name):
self.name = name
p = Product('Book')
Class Decorators sind ein mächtiges Meta-Programmierungstool.
10 Metaclasses – Klassen von Klassen
Metaclasses sind ein fortgeschrittenes Feature, mit dem man das Verhalten beim Erstellen von Klassen selbst kontrollieren kann. Sie sind die “Klassen von Klassen”.
10.1 Grundkonzept
In Python ist alles ein Objekt – auch Klassen. Klassen sind Instanzen von Metaclasses.
# Normale Hierarchie
class Dog:
pass
dog = Dog()
# Was ist was?
print(type(dog)) # <class '__main__.Dog'> - dog ist Instanz von Dog
print(type(Dog)) # <class 'type'> - Dog ist Instanz von type
print(type(type)) # <class 'type'> - type ist seine eigene Metaclass
# isinstance-Checks
print(isinstance(dog, Dog)) # True
print(isinstance(Dog, type)) # True
Die Kette:
dog → Instanz von → Dog → Instanz von → type (Metaclass)
10.2 type() als Metaclass
type kann auf zwei Arten verwendet werden:
1. Als Funktion (Typ abfragen):
x = 5
print(type(x)) # <class 'int'>
2. Als Metaclass (Klasse erstellen):
# Normale Klassendefinition
class Dog:
def bark(self):
return "Woof!"
# Äquivalent mit type()
Dog = type('Dog', (), {'bark': lambda self: "Woof!"})
dog = Dog()
print(dog.bark()) # "Woof!"
Syntax von type() zur Klassenerstellung:
type(name, bases, dict)
name: Klassenname (String)bases: Tuple der Basisklassendict: Dictionary mit Attributen und Methoden
Beispiel mit Vererbung:
# Basisklasse
class Animal:
def breathe(self):
return "Breathing..."
# Mit type() erstellen
Dog = type(
'Dog', # Name
(Animal,), # Basisklassen
{
'species': 'Canis familiaris',
'bark': lambda self: "Woof!"
}
)
dog = Dog()
print(dog.breathe()) # "Breathing..." (geerbt)
print(dog.bark()) # "Woof!"
print(dog.species) # "Canis familiaris"
10.3 Eigene Metaclass erstellen
Eine Metaclass ist eine Klasse, die von type erbt.
class Meta(type):
def __new__(mcs, name, bases, attrs):
print(f"Creating class {name}")
# Klasse erstellen
cls = super().__new__(mcs, name, bases, attrs)
return cls
class MyClass(metaclass=Meta):
pass
# Output beim Import/Ausführung:
# Creating class MyClass
Parameter von __new__:
mcs: Die Metaclass selbst (wieclsbei@classmethod)name: Name der zu erstellenden Klassebases: Tuple der Basisklassenattrs: Dictionary der Klassenattribute
10.4 __new__ vs. __init__ in Metaclasses
class Meta(type):
def __new__(mcs, name, bases, attrs):
"""Wird WÄHREND der Klassenerstellung aufgerufen"""
print(f"__new__: Creating {name}")
cls = super().__new__(mcs, name, bases, attrs)
return cls
def __init__(cls, name, bases, attrs):
"""Wird NACH der Klassenerstellung aufgerufen"""
print(f"__init__: Initializing {name}")
super().__init__(name, bases, attrs)
class MyClass(metaclass=Meta):
pass
# Output:
# __new__: Creating MyClass
# __init__: Initializing MyClass
Wann was verwenden:
__new__: Wenn man die Klasse vor ihrer Erstellung modifizieren will__init__: Wenn man die Klasse nach ihrer Erstellung modifizieren will
10.5 Praktische Anwendungsfälle
10.5.1 Attribute validieren
class ValidatedMeta(type):
def __new__(mcs, name, bases, attrs):
# Alle Attribute müssen mit Großbuchstaben beginnen
for key in attrs:
if not key.startswith('_'): # Private ignorieren
if not key[0].isupper():
raise ValueError(
f"Attribute {key} must start with uppercase letter"
)
return super().__new__(mcs, name, bases, attrs)
# ✅ Funktioniert
class GoodClass(metaclass=ValidatedMeta):
Name = "valid"
Age = 25
# ❌ Fehler
# class BadClass(metaclass=ValidatedMeta):
# name = "invalid" # ValueError!
10.5.2 Automatische Registrierung
class RegistryMeta(type):
_registry = {}
def __new__(mcs, name, bases, attrs):
cls = super().__new__(mcs, name, bases, attrs)
# Klasse automatisch registrieren
mcs._registry[name] = cls
return cls
@classmethod
def get_registry(mcs):
return mcs._registry
class Plugin(metaclass=RegistryMeta):
pass
class AudioPlugin(Plugin):
pass
class VideoPlugin(Plugin):
pass
# Alle Plugins automatisch registriert
print(RegistryMeta.get_registry())
# {'Plugin': <class '__main__.Plugin'>,
# 'AudioPlugin': <class '__main__.AudioPlugin'>,
# 'VideoPlugin': <class '__main__.VideoPlugin'>}
10.5.3 Singleton-Pattern
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self):
print("Connecting to database...")
# Erste Instanz
db1 = Database() # "Connecting to database..."
# Zweite "Instanz" - gibt dieselbe zurück
db2 = Database() # (kein Output)
print(db1 is db2) # True
10.5.4 Automatische __repr__ Methode
class AutoReprMeta(type):
def __new__(mcs, name, bases, attrs):
# Automatisch __repr__ generieren
def auto_repr(self):
attrs_str = ', '.join(
f"{k}={v!r}"
for k, v in self.__dict__.items()
)
return f"{name}({attrs_str})"
# Nur hinzufügen, wenn nicht vorhanden
if '__repr__' not in attrs:
attrs['__repr__'] = auto_repr
return super().__new__(mcs, name, bases, attrs)
class Point(metaclass=AutoReprMeta):
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(10, 20)
print(p) # Point(x=10, y=20) - automatisch generiert!
10.5.5 Interface/Abstract Base Class erzwingen
class InterfaceMeta(type):
def __new__(mcs, name, bases, attrs):
# Prüfe, ob alle abstrakten Methoden implementiert sind
if bases: # Nicht für die Basisklasse selbst
for base in bases:
if hasattr(base, '_required_methods'):
for method in base._required_methods:
if method not in attrs:
raise TypeError(
f"{name} must implement {method}()"
)
return super().__new__(mcs, name, bases, attrs)
class Shape(metaclass=InterfaceMeta):
_required_methods = ['area', 'perimeter']
# ❌ Fehler - area fehlt
# class Circle(Shape):
# def perimeter(self):
# return 2 * 3.14 * self.radius
# ✅ Funktioniert
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
def perimeter(self):
return 2 * 3.14 * self.radius
10.6 Metaclass-Vererbung
class MetaA(type):
def __new__(mcs, name, bases, attrs):
print(f"MetaA creating {name}")
return super().__new__(mcs, name, bases, attrs)
class MetaB(MetaA):
def __new__(mcs, name, bases, attrs):
print(f"MetaB creating {name}")
return super().__new__(mcs, name, bases, attrs)
class MyClass(metaclass=MetaB):
pass
# Output:
# MetaB creating MyClass
# MetaA creating MyClass
10.7 __call__ in Metaclasses
__call__ wird aufgerufen, wenn eine Instanz der Klasse erstellt wird.
class CounterMeta(type):
def __init__(cls, name, bases, attrs):
super().__init__(name, bases, attrs)
cls._instance_count = 0
def __call__(cls, *args, **kwargs):
# Wird bei MyClass() aufgerufen
instance = super().__call__(*args, **kwargs)
cls._instance_count += 1
print(f"Created instance #{cls._instance_count}")
return instance
class MyClass(metaclass=CounterMeta):
pass
obj1 = MyClass() # Created instance #1
obj2 = MyClass() # Created instance #2
obj3 = MyClass() # Created instance #3
print(MyClass._instance_count) # 3
10.8 __prepare__ – Dictionary für Klassenattribute vorbereiten
__prepare__ bestimmt, welches Dictionary für die Klassenattribute verwendet wird (normalerweise ein normales dict).
from collections import OrderedDict
class OrderedMeta(type):
@classmethod
def __prepare__(mcs, name, bases):
"""Wird VOR __new__ aufgerufen"""
print(f"Preparing namespace for {name}")
return OrderedDict()
def __new__(mcs, name, bases, namespace):
print(f"Attributes in order: {list(namespace.keys())}")
return super().__new__(mcs, name, bases, dict(namespace))
class MyClass(metaclass=OrderedMeta):
z = 3
a = 1
m = 2
# Output:
# Preparing namespace for MyClass
# Attributes in order: ['__module__', '__qualname__', 'z', 'a', 'm']
10.9 Metaclass-Konflikte vermeiden
Bei Mehrfachvererbung können Metaclass-Konflikte auftreten:
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
class B(metaclass=MetaB):
pass
# ❌ Fehler: metaclass conflict
# class C(A, B):
# pass
# ✅ Lösung: Gemeinsame Metaclass
class MetaC(MetaA, MetaB):
pass
class C(A, B, metaclass=MetaC):
pass
10.10 Metaclasses vs. Alternativen
Metaclasses sind mächtig, aber oft gibt es einfachere Alternativen.
10.10.1 Class Decorators
# Mit Metaclass
class AutoStrMeta(type):
def __new__(mcs, name, bases, attrs):
def __str__(self):
return f"{name} instance"
attrs['__str__'] = __str__
return super().__new__(mcs, name, bases, attrs)
class MyClass(metaclass=AutoStrMeta):
pass
# Mit Decorator (einfacher!)
def auto_str(cls):
def __str__(self):
return f"{cls.__name__} instance"
cls.__str__ = __str__
return cls
@auto_str
class MyClass:
pass
10.10.2 __init_subclass__ (Python 3.6+)
# Mit Metaclass
class RegistryMeta(type):
_registry = []
def __new__(mcs, name, bases, attrs):
cls = super().__new__(mcs, name, bases, attrs)
mcs._registry.append(cls)
return cls
# Mit __init_subclass__ (moderner!)
class Plugin:
_registry = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls._registry.append(cls)
class AudioPlugin(Plugin):
pass
print(Plugin._registry) # [<class 'AudioPlugin'>]
10.11 Debugging von Metaclasses
class DebugMeta(type):
def __new__(mcs, name, bases, attrs):
print(f"\n=== Creating class {name} ===")
print(f"Metaclass: {mcs}")
print(f"Bases: {bases}")
print(f"Attributes: {list(attrs.keys())}")
cls = super().__new__(mcs, name, bases, attrs)
print(f"Class created: {cls}")
print(f"MRO: {cls.__mro__}")
return cls
class Parent:
parent_attr = "parent"
class Child(Parent, metaclass=DebugMeta):
child_attr = "child"
# Output zeigt detaillierte Informationen über Klassenerstellung
10.12 Best Practices
✅ Wann Metaclasses verwenden:
- Framework-/Library-Entwicklung
- Automatische Registrierung/Plugin-Systeme
- Enforcing von Code-Standards
- DSL (Domain Specific Language) Implementation
- Komplexe ORM-Systeme (wie Django Models)
❌ Wann NICHT verwenden:
- Für alltägliche Programmierung
- Wenn Class Decorators ausreichen
- Wenn
__init_subclass__ausreicht - Wenn es den Code unleserlich macht
Alternativen prüfen:
- Class Decorators (meistens ausreichend)
__init_subclass__(Python 3.6+)- Descriptor Protocol
- Erst dann: Metaclasses
Zitat von Tim Peters:
“Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t.”
10.13 Zusammenfassung
| Konzept | Beschreibung |
|---|---|
type | Standard-Metaclass aller Klassen |
__new__ | Klasse während Erstellung modifizieren |
__init__ | Klasse nach Erstellung initialisieren |
__call__ | Instanzerstellung kontrollieren |
__prepare__ | Namespace-Dictionary vorbereiten |
metaclass= | Custom Metaclass zuweisen |
Kernprinzip: Metaclasses kontrollieren die Klassenerstellung selbst. Sie sind ein sehr mächtiges Werkzeug, sollten aber sparsam eingesetzt werden. In den meisten Fällen sind Class Decorators oder __init_subclass__ die bessere Wahl.
Entscheidungsbaum:
- Brauche ich wirklich Metaprogrammierung? → Oft: Nein
- Reicht ein Class Decorator? → Meistens: Ja
- Reicht
__init_subclass__? → Oft: Ja - Brauche ich volle Kontrolle über Klassenerstellung? → Dann: Metaclass
11 Itertools – Leistungsstarke Iterator-Werkzeuge
Das itertools-Modul bietet spezialisierte Iterator-Funktionen für effiziente Schleifen und funktionale Programmierung. Alle Funktionen sind speichereffizient, da sie Iteratoren statt Listen zurückgeben.
11.1 Unendliche Iteratoren
11.1.1 count() – Unendliches Zählen
from itertools import count
# Zählt ab 10 in 2er-Schritten
counter = count(start=10, step=2)
for i in counter:
if i > 20:
break
print(i) # 10, 12, 14, 16, 18, 20
# Mit zip für begrenzte Iteration
for i, letter in zip(count(1), ['a', 'b', 'c']):
print(f"{i}: {letter}")
# 1: a
# 2: b
# 3: c
11.1.2 cycle() – Elemente wiederholen
from itertools import cycle
# Zyklisch durch Elemente iterieren
colors = cycle(['red', 'green', 'blue'])
for i, color in enumerate(colors):
if i >= 7:
break
print(color)
# red, green, blue, red, green, blue, red
Praktisches Beispiel – Zeilen abwechselnd einfärben:
from itertools import cycle
rows = ['Row 1', 'Row 2', 'Row 3', 'Row 4', 'Row 5']
colors = cycle(['white', 'gray'])
for row, color in zip(rows, colors):
print(f"{row} - {color}")
11.1.3 repeat() – Element wiederholen
from itertools import repeat
# Unbegrenzt
for item in repeat('X'):
print(item) # X, X, X, ... (unendlich)
break
# Begrenzt
for item in repeat('X', 3):
print(item) # X, X, X
# Praktisch mit map
result = list(map(pow, [2, 3, 4], repeat(3)))
print(result) # [8, 27, 64] (2³, 3³, 4³)
11.2 Kombinatorische Iteratoren
11.2.1 product() – Kartesisches Produkt
from itertools import product
# Alle Kombinationen
colors = ['red', 'blue']
sizes = ['S', 'M', 'L']
for color, size in product(colors, sizes):
print(f"{color}-{size}")
# red-S, red-M, red-L, blue-S, blue-M, blue-L
# Äquivalent zu verschachtelten Schleifen
for color in colors:
for size in sizes:
print(f"{color}-{size}")
# Mit repeat-Parameter
for item in product(range(2), repeat=3):
print(item)
# (0,0,0), (0,0,1), (0,1,0), (0,1,1), (1,0,0), (1,0,1), (1,1,0), (1,1,1)
11.2.2 permutations() – Permutationen
from itertools import permutations
# Alle Anordnungen von 3 Elementen
items = ['A', 'B', 'C']
for perm in permutations(items):
print(perm)
# ('A','B','C'), ('A','C','B'), ('B','A','C'), ('B','C','A'), ('C','A','B'), ('C','B','A')
# Permutationen mit Länge 2
for perm in permutations(items, 2):
print(perm)
# ('A','B'), ('A','C'), ('B','A'), ('B','C'), ('C','A'), ('C','B')
# Anzahl: n! / (n-r)! für Länge r
import math
n, r = 3, 2
count = math.factorial(n) // math.factorial(n - r)
print(count) # 6
11.2.3 combinations() – Kombinationen (ohne Wiederholung)
from itertools import combinations
# Alle 2er-Kombinationen
items = ['A', 'B', 'C', 'D']
for combo in combinations(items, 2):
print(combo)
# ('A','B'), ('A','C'), ('A','D'), ('B','C'), ('B','D'), ('C','D')
# Anzahl: n! / (r! * (n-r)!)
import math
n, r = 4, 2
count = math.factorial(n) // (math.factorial(r) * math.factorial(n - r))
print(count) # 6
11.2.4 combinations_with_replacement() – Kombinationen mit Wiederholung
from itertools import combinations_with_replacement
items = ['A', 'B', 'C']
for combo in combinations_with_replacement(items, 2):
print(combo)
# ('A','A'), ('A','B'), ('A','C'), ('B','B'), ('B','C'), ('C','C')
11.3 Terminierende Iteratoren
11.3.1 chain() – Iterables verketten
from itertools import chain
# Mehrere Iterables zu einem kombinieren
list1 = [1, 2, 3]
list2 = [4, 5, 6]
list3 = [7, 8, 9]
for item in chain(list1, list2, list3):
print(item) # 1, 2, 3, 4, 5, 6, 7, 8, 9
# Äquivalent zu:
result = list1 + list2 + list3
# chain.from_iterable für verschachtelte Iterables
nested = [[1, 2], [3, 4], [5, 6]]
flattened = list(chain.from_iterable(nested))
print(flattened) # [1, 2, 3, 4, 5, 6]
11.3.2 compress() – Filtern mit Boolean-Mask
from itertools import compress
data = ['A', 'B', 'C', 'D', 'E']
selectors = [True, False, True, False, True]
result = list(compress(data, selectors))
print(result) # ['A', 'C', 'E']
# Praktisches Beispiel
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
is_even = [n % 2 == 0 for n in numbers]
evens = list(compress(numbers, is_even))
print(evens) # [2, 4, 6, 8, 10]
11.3.3 dropwhile() und takewhile() – Bedingte Iteration
from itertools import dropwhile, takewhile
data = [1, 4, 6, 4, 1]
# dropwhile: Überspringt bis Bedingung False wird
result = list(dropwhile(lambda x: x < 5, data))
print(result) # [6, 4, 1] (ab erstem x >= 5)
# takewhile: Nimmt bis Bedingung False wird
result = list(takewhile(lambda x: x < 5, data))
print(result) # [1, 4] (bis erstes x >= 5)
11.3.4 filterfalse() – Umgekehrtes filter()
from itertools import filterfalse
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# filter gibt True-Werte zurück
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4, 6, 8, 10]
# filterfalse gibt False-Werte zurück
odds = list(filterfalse(lambda x: x % 2 == 0, numbers))
print(odds) # [1, 3, 5, 7, 9]
11.3.5 groupby() – Gruppieren nach Schlüssel
from itertools import groupby
# Daten müssen nach Gruppierungsschlüssel SORTIERT sein!
data = [
('Alice', 'A'),
('Bob', 'B'),
('Charlie', 'C'),
('David', 'A'),
('Eve', 'B')
]
# Nach zweitem Element gruppieren (NICHT sortiert → falsche Gruppen!)
for key, group in groupby(data, key=lambda x: x[1]):
print(f"{key}: {list(group)}")
# A: [('Alice', 'A')]
# B: [('Bob', 'B')]
# C: [('Charlie', 'C')]
# A: [('David', 'A')] ← Neue Gruppe!
# B: [('Eve', 'B')] ← Neue Gruppe!
# Richtig: Erst sortieren
data_sorted = sorted(data, key=lambda x: x[1])
for key, group in groupby(data_sorted, key=lambda x: x[1]):
print(f"{key}: {list(group)}")
# A: [('Alice', 'A'), ('David', 'A')]
# B: [('Bob', 'B'), ('Eve', 'B')]
# C: [('Charlie', 'C')]
Praktisches Beispiel – Nach Länge gruppieren:
from itertools import groupby
words = ['a', 'bb', 'ccc', 'dd', 'e', 'fff']
words_sorted = sorted(words, key=len)
for length, group in groupby(words_sorted, key=len):
print(f"Length {length}: {list(group)}")
# Length 1: ['a', 'e']
# Length 2: ['bb', 'dd']
# Length 3: ['ccc', 'fff']
11.3.6 islice() – Slice für Iteratoren
from itertools import islice, count
# Wie list-slicing, aber für Iteratoren
data = range(10)
# islice(iterable, stop)
result = list(islice(data, 5))
print(result) # [0, 1, 2, 3, 4]
# islice(iterable, start, stop)
result = list(islice(data, 2, 7))
print(result) # [2, 3, 4, 5, 6]
# islice(iterable, start, stop, step)
result = list(islice(data, 0, 10, 2))
print(result) # [0, 2, 4, 6, 8]
# Sehr nützlich für unendliche Iteratoren
result = list(islice(count(10), 5))
print(result) # [10, 11, 12, 13, 14]
11.3.7 starmap() – map mit Argument-Unpacking
from itertools import starmap
# map wendet Funktion auf einzelne Elemente an
result = list(map(pow, [2, 3, 4], [5, 2, 3]))
print(result) # [32, 9, 64]
# starmap entpackt Tupel als Argumente
data = [(2, 5), (3, 2), (4, 3)]
result = list(starmap(pow, data))
print(result) # [32, 9, 64]
# Praktisches Beispiel
points = [(1, 2), (3, 4), (5, 6)]
result = list(starmap(lambda x, y: x + y, points))
print(result) # [3, 7, 11]
11.3.8 tee() – Iterator duplizieren
from itertools import tee
data = range(5)
it1, it2 = tee(data, 2) # 2 unabhängige Kopien
# Beide können unabhängig verwendet werden
print(list(it1)) # [0, 1, 2, 3, 4]
print(list(it2)) # [0, 1, 2, 3, 4]
# Warnung: Original-Iterator nicht mehr verwenden!
11.3.9 zip_longest() – Zip mit Auffüllung
from itertools import zip_longest
# Normales zip stoppt bei kürzester Sequenz
a = [1, 2, 3]
b = ['a', 'b']
print(list(zip(a, b))) # [(1, 'a'), (2, 'b')]
# zip_longest füllt mit None auf
print(list(zip_longest(a, b)))
# [(1, 'a'), (2, 'b'), (3, None)]
# Mit custom fillvalue
print(list(zip_longest(a, b, fillvalue='?')))
# [(1, 'a'), (2, 'b'), (3, '?')]
11.4 Akkumulatoren
11.4.1 accumulate() – Kumulative Werte
from itertools import accumulate
import operator
# Standardmäßig: Addition
numbers = [1, 2, 3, 4, 5]
result = list(accumulate(numbers))
print(result) # [1, 3, 6, 10, 15] (laufende Summe)
# Mit custom Operation
result = list(accumulate(numbers, operator.mul))
print(result) # [1, 2, 6, 24, 120] (laufendes Produkt)
# Maximum-Tracking
numbers = [5, 2, 8, 1, 9, 3]
result = list(accumulate(numbers, max))
print(result) # [5, 5, 8, 8, 9, 9]
# Mit Lambda
result = list(accumulate(numbers, lambda x, y: x if x > y else y))
print(result) # [5, 5, 8, 8, 9, 9]
11.5 Praktische Kombinationen
11.5.1 Paarweise Iteration
from itertools import tee
def pairwise(iterable):
"""s -> (s0,s1), (s1,s2), (s2,s3), ..."""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
# Verwendung
data = [1, 2, 3, 4, 5]
for pair in pairwise(data):
print(pair)
# (1, 2), (2, 3), (3, 4), (4, 5)
# Ab Python 3.10 eingebaut:
from itertools import pairwise
for pair in pairwise(data):
print(pair)
11.5.2 Fenster-Iteration (Sliding Window)
from itertools import islice
def sliding_window(iterable, n):
"""Gleitet mit Fenster der Größe n über iterable"""
iterators = tee(iterable, n)
for i, it in enumerate(iterators):
for _ in range(i):
next(it, None)
return zip(*iterators)
# Verwendung
data = [1, 2, 3, 4, 5, 6]
for window in sliding_window(data, 3):
print(window)
# (1, 2, 3), (2, 3, 4), (3, 4, 5), (4, 5, 6)
11.5.3 Batching (Chunks)
from itertools import islice
def batched(iterable, n):
"""iterable -> [chunk1, chunk2, ...]"""
iterator = iter(iterable)
while True:
batch = list(islice(iterator, n))
if not batch:
break
yield batch
# Verwendung
data = range(10)
for batch in batched(data, 3):
print(batch)
# [0, 1, 2], [3, 4, 5], [6, 7, 8], [9]
# Ab Python 3.12 eingebaut:
# from itertools import batched
11.5.4 Flatten (Verschachtelung auflösen)
from itertools import chain
def flatten(nested_list):
"""[[1,2], [3,4]] -> [1, 2, 3, 4]"""
return chain.from_iterable(nested_list)
nested = [[1, 2], [3, 4], [5, 6]]
result = list(flatten(nested))
print(result) # [1, 2, 3, 4, 5, 6]
# Für beliebige Verschachtelung (rekursiv)
def deep_flatten(nested):
for item in nested:
if isinstance(item, (list, tuple)):
yield from deep_flatten(item)
else:
yield item
deeply_nested = [1, [2, [3, [4, 5]]]]
result = list(deep_flatten(deeply_nested))
print(result) # [1, 2, 3, 4, 5]
11.6 Performance-Vorteile
import time
from itertools import islice, count
# ❌ Ineffizient: Liste erstellen
start = time.time()
large_list = list(range(10_000_000))
first_100 = large_list[:100]
print(f"List: {time.time() - start:.3f}s")
# ✅ Effizient: Iterator verwenden
start = time.time()
first_100 = list(islice(count(), 100))
print(f"Iterator: {time.time() - start:.3f}s")
# Speichervergleich
import sys
numbers_list = list(range(1_000_000))
numbers_iter = range(1_000_000)
print(f"List: {sys.getsizeof(numbers_list)} bytes") # ~8 MB
print(f"Iterator: {sys.getsizeof(numbers_iter)} bytes") # ~48 bytes
11.7 Best Practices
✅ DO:
- Nutze Iteratoren für große Datenmengen (speichereffizient)
- Kombiniere itertools-Funktionen für komplexe Operationen
- Sortiere Daten vor
groupby() - Verwende
chain.from_iterable()statt verschachtelter Loops
❌ DON’T:
- Konvertiere Iteratoren nicht unnötig zu Listen
- Vergiss nicht, dass Iteratoren nur einmal durchlaufen werden können
- Verwende
groupby()nicht ohne vorheriges Sortieren - Original-Iterator nach
tee()nicht weiterverwenden
11.8 Zusammenfassung
| Funktion | Zweck | Rückgabe |
|---|---|---|
count() | Unendliches Zählen | 10, 11, 12, … |
cycle() | Elemente zyklisch wiederholen | A, B, C, A, B, … |
repeat() | Element wiederholen | X, X, X, … |
product() | Kartesisches Produkt | (A,1), (A,2), … |
permutations() | Alle Anordnungen | (A,B), (B,A), … |
combinations() | Kombinationen ohne Wiederholung | (A,B), (A,C), … |
chain() | Iterables verketten | 1, 2, 3, 4, … |
compress() | Filtern mit Boolean-Mask | [True], [False], … |
groupby() | Gruppieren (nach Sortierung!) | Gruppen nach Key |
islice() | Slice für Iteratoren | Teilbereich |
accumulate() | Kumulative Werte | 1, 3, 6, 10, … |
Kernprinzip: itertools bietet speichereffiziente, kombinierbare Iterator-Funktionen für funktionale Programmierung und große Datenmengen. Iteratoren sind lazy (verzögerte Auswertung) und können nur einmal durchlaufen werden.
12 Zusammenfassung
-
Typprüfung und Instanzen
- Mit
type(),isinstance()undissubclass()prüft man Objekttypen und Vererbungsbeziehungen.
- Mit
-
Objekt-Erzeugung
__new__erzeugt das Objekt (besonders bei Immutable-Typen wichtig).__init__initialisiert das Objekt nach der Erzeugung.
-
Methodenarten
@staticmethod: Kein Zugriff auf Klassen- oder Instanzdaten.@classmethod: Zugriff auf die Klasse (cls).@property: Ermöglicht kontrollierten Zugriff auf Attribute wie bei einem Feld.
-
Spezialmethoden (Dunder Methods)
- Methoden wie
__str__,__eq__,__getitem__erlauben es, benutzerdefinierte Objekte wie eingebaute Typen zu behandeln.
- Methoden wie
-
Abstraktion und Vererbung
- Abstrakte Klassen (
ABC,@abstractmethod) erzwingen Implementierungen in Unterklassen. - Die Method Resolution Order (MRO) bestimmt die Aufrufreihenfolge bei Mehrfachvererbung.
- Abstrakte Klassen (
-
Iteration und Indexierung
- Iterator-Klassen und Generator-Funktionen ermöglichen eigene Iterationslogik.
- Mit
__getitem__und__setitem__lassen sich Objekte wie Listen verwenden.
-
Speicher- und Datenrepräsentation
@dataclassreduziert Boilerplate für Datenobjekte.__slots__spart Speicher durch festen Attributsatz.frozen=Truemacht die Dataclass unveränderbar.
namedtupleist eine kompakte, unveränderliche Datenstruktur mit Feldnamen.
-
Enumerationen
- Mit
Enumkann man symbolische Konstanten definieren, die lesbar und typsicher sind.
- Mit
-
Klassen-Dekoratoren
- Decorators für Klassen können beim Erzeugen einer Klasse deren Verhalten ändern oder erweitern – ideal für Logging, Validierung oder automatische Registrierung.