Python Abstrakte Basisklassen (ABC)
Erfahren Sie, wie Python abstrakte Basisklassen (ABC) mit @abstractmethod und dem abc-Modul eine einheitliche Schnittstelle für Unterklassen erzwingen.
Eine abstrakte Klasse ist eine Klasse, die nicht direkt instanziiert werden kann. Sie dient ausschließlich dazu, eine gemeinsame Schnittstelle zu definieren — eine Menge von Methoden, die jede konkrete Unterklasse implementieren muss. Python stellt dies durch das eingebaute abc-Modul (Abstract Base Classes) bereit.
Dieses Kapitel behandelt:
- Warum abstrakte Klassen existieren und wann man sie einsetzt
- Das
abc-Modul —ABCundabstractmethod - Methodenverträge mit
@abstractmethoderzwingen - Abstrakte Eigenschaften (Properties)
- Konkrete Methoden in einer abstrakten Klasse
- Abstrakte Klassen vs.
NotImplementedErrorauslösen - Ein praxisnahes Beispiel
Bevor Sie dieses Kapitel lesen, stellen Sie sicher, dass Sie mit Python-Klassen und -Objekten sowie mit Vererbung vertraut sind.
Warum abstrakte Klassen verwenden?
Wenn man einer Klassenhierarchie immer mehr Unterklassen hinzufügt, vergisst man leicht, eine erforderliche Methode zu implementieren. Das Problem tritt erst zur Laufzeit auf, tief im Code, was das Debuggen mühsam macht.
Abstrakte Basisklassen lösen dieses Problem, indem sie den Fehler auf den Moment der Instanziierung vorverlegen — nicht auf den Moment, wenn eine fehlende Methode aufgerufen wird. Sie wirken wie ein Vertrag: Jede konkrete Unterklasse muss jede abstrakte Methode implementieren, sonst verweigert Python die Erzeugung eines Objekts daraus.
Abstrakte Klassen vs. NotImplementedError auslösen
Bevor das abc-Modul existierte, war es üblich, NotImplementedError in einer Basisklassenmethode auszulösen:
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
class Circle(Shape):
pass # forgot to implement area()
c = Circle() # No error yet — Python lets this through
c.area() # NotImplementedError only here, at call timeDer Nachteil: Python erstellt das Objekt ohne Beanstandung. Der Fehler bleibt unsichtbar, bis die Methode tatsächlich aufgerufen wird — was an einer völlig anderen Stelle im Programm sein kann.
Mit einer abstrakten Klasse tritt der Fehler sofort auf:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
pass # still missing area()
c = Circle()
# TypeError: Can't instantiate abstract class Circle
# with abstract method areaDie Klasse ist beim Erstellen fehlerhaft — genau dort, wo der Vertrag verletzt wurde.
Das abc-Modul
Pythons abc-Modul stellt zwei Werkzeuge bereit, die man ständig verwendet:
| Name | Bedeutung |
|---|---|
ABC | Eine Hilfsbasisklasse. Von ihr erben macht eine Klasse abstrakt. |
abstractmethod | Ein Dekorator, der eine Methode als abstrakt markiert. |
So werden sie importiert:
from abc import ABC, abstractmethodEine abstrakte Klasse erstellen
Von ABC erben und mindestens eine Methode mit @abstractmethod dekorieren:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
passShape erklärt damit, dass alles, was sich als Form bezeichnet, area() und perimeter() bereitstellen muss. Eine Shape kann nicht direkt erstellt werden:
s = Shape()
# TypeError: Can't instantiate abstract class Shape
# with abstract methods area, perimeterEine abstrakte Klasse implementieren
Eine konkrete Klasse erbt von der abstrakten Klasse und implementiert jede abstrakte Methode. Erst dann können Instanzen erzeugt werden:
Abstrakte Klasse mit Circle und Rectangle implementieren
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
c = Circle(5)
r = Rectangle(4, 6)
print(round(c.area(), 4)) # 78.5398
print(round(c.perimeter(), 4)) # 31.4159
print(r.area()) # 24
print(r.perimeter()) # 20Sowohl Circle als auch Rectangle implementieren jede abstrakte Methode, daher erlaubt Python deren Instanziierung.
Was passiert, wenn eine Methode fehlt?
Implementiert eine Unterklasse nur einige abstrakte Methoden, bleibt sie abstrakt — eine Instanziierung löst einen TypeError aus, der die fehlende Methode benennt:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class IncompleteShape(Shape):
def area(self):
return 0
# perimeter() not implemented
s = IncompleteShape()
# TypeError: Can't instantiate abstract class IncompleteShape
# with abstract method perimeterDie Fehlermeldung zeigt genau, welche Methode fehlt — weit hilfreicher als ein generischer Laufzeitfehler.
Abstrakte Properties
Man kann auch eine Property als abstrakt markieren. Das zwingt Unterklassen dazu, eine Property (oder ein von einer Property berechnetes Attribut) bereitzustellen, nicht nur eine gewöhnliche Methode. Dazu werden @property und @abstractmethod zusammen verwendet, wobei @property oben steht:
Abstrakte Properties verwenden
from abc import ABC, abstractmethod
class Animal(ABC):
@property
@abstractmethod
def sound(self):
pass
def describe(self):
return f"I make the sound: {self.sound}"
class Dog(Animal):
@property
def sound(self):
return "Woof"
class Cat(Animal):
@property
def sound(self):
return "Meow"
dog = Dog()
cat = Cat()
print(dog.describe()) # I make the sound: Woof
print(cat.describe()) # I make the sound: MeowDie sound-Property in jeder Unterklasse verhält sich aus Sicht des Aufrufers wie ein schreibgeschütztes Attribut, während die abstrakte Klasse sicherstellt, dass jede konkrete Unterklasse eine solche bereitstellt.
Konkrete Methoden in einer abstrakten Klasse
Abstrakte Klassen sind nicht auf abstrakte Methoden beschränkt. Sie können vollständig implementierte (konkrete) Methoden enthalten, die alle Unterklassen automatisch erben. Dies ist der entscheidende Unterschied zu reinen Schnittstellen in anderen Sprachen — man kann gemeinsame Logik in die abstrakte Klasse auslagern:
Gemeinsame Logik über eine konkrete Methode teilen
from abc import ABC, abstractmethod
class Logger(ABC):
def log(self, message):
"""Concrete method — shared by all subclasses."""
formatted = self.format_message(message)
self.write(formatted)
def format_message(self, message):
return f"[LOG] {message}"
@abstractmethod
def write(self, message):
"""Abstract — each subclass decides how to output."""
pass
class ConsoleLogger(Logger):
def write(self, message):
print(message)
class FileLogger(Logger):
def __init__(self):
self.entries = []
def write(self, message):
self.entries.append(message)
console = ConsoleLogger()
console.log("Server started") # prints: [LOG] Server started
file_log = FileLogger()
file_log.log("Database connected")
print(file_log.entries) # ['[LOG] Database connected']Hier sind log() und format_message() konkrete gemeinsame Methoden. Nur write() — der Teil, der sich zwischen den Backends unterscheidet — ist abstrakt. Unterklassen müssen die Formatierungslogik nie neu implementieren.
isinstance() und abstrakte Klassen
Ein Objekt, das eine Instanz einer konkreten Unterklasse ist, gilt auch als Instanz der abstrakten Basisklasse. Dadurch lassen sich Funktionen schreiben, die jedes Objekt akzeptieren, das die Schnittstelle erfüllt, ohne Rücksicht auf den konkreten Typ:
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
shapes = [Circle(3), Rectangle(4, 5)]
for shape in shapes:
print(isinstance(shape, Shape)) # True for both
def total_area(shapes):
return sum(s.area() for s in shapes)
print(round(total_area(shapes), 4)) # 48.2743Das ist der Kernvorteil: total_area() funktioniert mit jeder Shape — vergangener, gegenwärtiger oder zukünftiger — solange das Objekt die Schnittstelle implementiert.
Praxisbeispiel: Zahlungsabwickler
Abstrakte Klassen glänzen, wenn man mehrere Implementierungen desselben Konzepts benötigt. Stellen Sie sich ein Zahlungssystem vor, das verschiedene Zahlungsanbieter unterstützen muss:
Abstrakte Klasse für die Zahlungsabwicklung
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount):
pass
@abstractmethod
def refund(self, amount):
pass
def process(self, amount):
"""Concrete method — shared processing logic."""
print(f"Processing payment of ${amount}")
self.charge(amount)
class StripeProcessor(PaymentProcessor):
def charge(self, amount):
print(f"Stripe: charged ${amount}")
def refund(self, amount):
print(f"Stripe: refunded ${amount}")
class PayPalProcessor(PaymentProcessor):
def charge(self, amount):
print(f"PayPal: charged ${amount}")
def refund(self, amount):
print(f"PayPal: refunded ${amount}")
processors = [StripeProcessor(), PayPalProcessor()]
for p in processors:
p.process(100)
# Processing payment of $100
# Stripe: charged $100
# Processing payment of $100
# PayPal: charged $100Das Hinzufügen eines dritten Zahlungsanbieters — etwa BraintreeProcessor — erfordert nur die Implementierung von charge() und refund(). Die process()-Logik und alle isinstance-Prüfungen funktionieren weiterhin ohne Änderungen.
Wann abstrakte Klassen verwenden?
Eine abstrakte Klasse ist sinnvoll, wenn:
- Man eine Familie verwandter Klassen hat, die alle bestimmte Methoden bereitstellen müssen.
- Man gemeinsamen Code (konkrete Methoden) teilen möchte und gleichzeitig andere Methoden zur Überschreibung zwingt.
- Python sofort einen Fehler auslösen soll, wenn eine Unterklasse unvollständig ist, anstatt auf einen Laufzeitfehler zu warten.
Eine abstrakte Klasse ist wahrscheinlich nicht nötig, wenn:
- Man nur eine oder zwei Unterklassen hat und die Hierarchie kaum wachsen wird.
- Die gemeinsame Schnittstelle bereits durch andere Mittel erzwungen wird (z. B. Python-Dataclasses oder Duck Typing).
- Man nur die direkte Instanziierung verhindern möchte — eine einfachere Lösung ist ein
__init_subclass__-Hook oder ein schlichter Kommentar.
Für eng verwandte Konzepte siehe Python-Vererbung und Python-Polymorphismus.