W3docs

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 — ABC und abstractmethod
  • Methodenverträge mit @abstractmethod erzwingen
  • Abstrakte Eigenschaften (Properties)
  • Konkrete Methoden in einer abstrakten Klasse
  • Abstrakte Klassen vs. NotImplementedError auslö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 time

Der 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 area

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

NameBedeutung
ABCEine Hilfsbasisklasse. Von ihr erben macht eine Klasse abstrakt.
abstractmethodEin Dekorator, der eine Methode als abstrakt markiert.

So werden sie importiert:

from abc import ABC, abstractmethod

Eine 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):
        pass

Shape 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, perimeter

Eine 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())            # 20

Sowohl 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 perimeter

Die 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: Meow

Die 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.2743

Das 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 $100

Das 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.

Übungen

Übung
Was passiert, wenn man versucht, eine Klasse zu instanziieren, die von ABC erbt, aber nicht alle abstrakten Methoden implementiert?
Was passiert, wenn man versucht, eine Klasse zu instanziieren, die von ABC erbt, aber nicht alle abstrakten Methoden implementiert?
Was this page helpful?