W3docs

Python Polymorphismus

Python Polymorphismus verstehen: Method Overriding, Duck Typing, Operator-Überladung und abstrakte Interfaces — mit klaren Beispielen.

Polymorphismus (vom griechischen „viele Formen") ermöglicht es einem einzelnen Stück Code, mit Objekten verschiedener Typen zu arbeiten, solange diese Objekte die erwartete Schnittstelle unterstützen. Sie rufen denselben Methodennamen auf jedem Objekt auf, und jedes Objekt antwortet gemäß seiner eigenen Implementierung.

Polymorphismus ist eine der vier Säulen der objektorientierten Programmierung, zusammen mit Kapselung, Vererbung und Abstraktion. Er sorgt dafür, dass Funktionen, die einen Basisklassen-Typ akzeptieren, automatisch mit jeder Unterklasse funktionieren.

Dieses Kapitel behandelt:

  • Was Polymorphismus ist und warum er wichtig ist
  • Polymorphismus durch Method Overriding
  • Polymorphismus durch Duck Typing
  • Operator-Überladung — eine in Python eingebaute Form des Polymorphismus
  • Abstrakte Basisklassen als formaler Weg zur Definition polymorpher Schnittstellen
  • Praktische Muster und häufige Fallstricke

Bevor Sie dieses Kapitel lesen, stellen Sie sicher, dass Sie mit Python-Klassen und -Objekten und Python-Vererbung vertraut sind.

Warum Polymorphismus wichtig ist

Ohne Polymorphismus würde eine Funktion, die mit Tieren arbeitet, eine explizite if/elif-Kette für jeden Tiertyp benötigen:

def make_sound(animal):
    if type(animal).__name__ == "Dog":
        print("Woof!")
    elif type(animal).__name__ == "Cat":
        print("Meow!")
    elif type(animal).__name__ == "Bird":
        print("Tweet!")
    # ... add a new branch every time you add a new animal type

Das ist fehleranfällig. Jeder neue Tiertyp erfordert eine Änderung dieser Funktion. Mit Polymorphismus schreibt man:

def make_sound(animal):
    animal.speak()   # works for any object that has a speak() method

Das Hinzufügen eines neuen Tiertyps erfordert nur die Definition seiner speak()-Methode — die Funktion selbst ändert sich nie. Dies ist das Open/Closed-Prinzip: offen für Erweiterungen, geschlossen für Modifikationen.

Polymorphismus durch Method Overriding

Die häufigste Form des Polymorphismus in Python ist das Method Overriding: Eine Unterklasse stellt ihre eigene Version einer Methode bereit, die von der Elternklasse definiert wurde.

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} makes a sound."


class Dog(Animal):
    def speak(self):
        return f"{self.name} says woof!"


class Cat(Animal):
    def speak(self):
        return f"{self.name} says meow!"


class Bird(Animal):
    def speak(self):
        return f"{self.name} says tweet!"

Jetzt funktioniert eine einzige Schleife für alle drei Typen:

animals = [Dog("Rex"), Cat("Whiskers"), Bird("Tweety")]

for animal in animals:
    print(animal.speak())

# Rex says woof!
# Whiskers says meow!
# Tweety says tweet!

animal.speak() wird zur Laufzeit basierend auf dem tatsächlichen Typ des Objekts an die richtige Version weitergeleitet. Diese Laufzeit-Weiterleitung wird als dynamisches Dispatch oder spätes Binden bezeichnet.

Erweitern vs. Ersetzen der Elternmethode

Beim Überschreiben können Sie entweder das Verhalten der Elternklasse vollständig ersetzen oder es mit super() erweitern:

class Animal:
    def speak(self):
        print("[Animal vocalization]")


class Dog(Animal):
    def speak(self):
        super().speak()           # keep the parent's output
        print("Woof! (Dog override adds this)")


Dog().speak()
# [Animal vocalization]
# Woof! (Dog override adds this)

Verwenden Sie super(), wenn die Version der Elternklasse nützliche Initialisierung oder Protokollierung enthält, die noch ausgeführt werden soll. Lassen Sie es weg, wenn Sie das Verhalten vollständig ersetzen möchten.

Polymorphismus durch Duck Typing

Pythons Typsystem ist strukturell statt nominal. Ein Objekt muss nicht zu einer bestimmten Klassenhierarchie gehören; es muss nur die richtigen Methoden haben. Dies nennt man Duck Typing — benannt nach dem Ausspruch „Wenn es wie eine Ente läuft und wie eine Ente quakt, dann ist es eine Ente."

class Dog:
    def speak(self):
        return "Woof!"


class Robot:
    def speak(self):
        return "Beep boop."


class Human:
    def speak(self):
        return "Hello!"


def introduce(entity):
    print(entity.speak())


introduce(Dog())    # Woof!
introduce(Robot())  # Beep boop.
introduce(Human())  # Hello!

Dog, Robot und Human teilen keine gemeinsame Elternklasse (außer dem eingebauten object). Dennoch funktioniert introduce() mit allen dreien, weil jeder eine speak()-Methode hat. Die Funktion prüft nicht, welchen Typ entity hat — sie ruft einfach die Methode auf und vertraut darauf, dass das Objekt korrekt reagiert.

Wann Duck Typing vs. Vererbung verwenden

SituationBevorzugter Ansatz
Objekte sind logisch verwandt (alle Tiere)Vererbungshierarchie
Objekte sind unverwandt, teilen aber ein VerhaltenDuck Typing
Sie möchten die Schnittstelle zur Definitionszeit erzwingenAbstrakte Basisklassen
Arbeiten mit eingebauten Typen oder Drittanbieter-Klassen, die Sie nicht ändern könnenDuck Typing

Duck Typing ist idiomatisches Python und wird in der Standardbibliothek häufig verwendet — zum Beispiel funktioniert len() auf jedem Objekt, das __len__ definiert, unabhängig von seiner Klasse.

Polymorphismus mit Funktionen und Schleifen

Sie können eine einzelne Funktion schreiben, die verschiedene Typen durch Polymorphismus einheitlich behandelt. Betrachten Sie eine Zeichenanwendung:

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        import math
        return math.pi * self.radius ** 2

    def describe(self):
        return f"Circle with radius {self.radius}"


class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def describe(self):
        return f"Rectangle {self.width}x{self.height}"


class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

    def describe(self):
        return f"Triangle base={self.base} height={self.height}"


shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]

for shape in shapes:
    print(f"{shape.describe()}: area = {shape.area():.2f}")

# Circle with radius 5: area = 78.54
# Rectangle 4x6: area = 24.00
# Triangle base=3 height=8: area = 12.00

Die Schleife ruft area() und describe() auf jeder Form auf, ohne sich darum zu kümmern, zu welcher Klasse das Objekt gehört. Das spätere Hinzufügen einer Pentagon-Klasse erfordert nur das Schreiben der neuen Klasse — die Schleife ändert sich nicht.

Operator-Überladung

Pythons arithmetische und Vergleichsoperatoren sind ebenfalls polymorph. + auf ganzen Zahlen addiert Zahlen; + auf Strings verkettet sie; + auf Listen führt sie zusammen. Python erreicht dies durch spezielle (Dunder-)Methoden.

Sie können Ihre eigenen Klassen auf Operatoren reagieren lassen, indem Sie diese Methoden definieren:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"


v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)    # Vector(4, 6)
print(v1 * 3)     # Vector(3, 6)

Derselbe +-Operator verhält sich nun unterschiedlich, je nachdem ob die Operanden ganze Zahlen, Strings oder Vector-Objekte sind. Dies ist Polymorphismus auf Operator-Ebene.

Für eine tiefergehende Betrachtung von Pythons speziellen Methoden, siehe Python Magic Methods.

Polymorphismus mit abstrakten Basisklassen

Abstrakte Basisklassen (ABCs) gehen beim Duck Typing einen Schritt weiter, indem sie die Schnittstelle zur Klassen-Definitionszeit erzwingen. Wenn eine Unterklasse eine erforderliche Methode nicht implementiert, löst Python einen TypeError aus, sobald Sie versuchen, sie zu instanziieren.

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        """Return the area of the shape."""

    @abstractmethod
    def perimeter(self) -> float:
        """Return the perimeter of the shape."""


class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        import math
        return math.pi * self.radius ** 2

    def perimeter(self) -> float:
        import math
        return 2 * math.pi * self.radius


class Square(Shape):
    def __init__(self, side: float):
        self.side = side

    def area(self) -> float:
        return self.side ** 2

    def perimeter(self) -> float:
        return 4 * self.side


def print_info(shape: Shape) -> None:
    print(f"Area:      {shape.area():.2f}")
    print(f"Perimeter: {shape.perimeter():.2f}")


print_info(Circle(5))
# Area:      78.54
# Perimeter: 31.42

print_info(Square(4))
# Area:      16.00
# Perimeter: 16.00

Wenn Sie versuchen, eine Unterklasse zu instanziieren, die area() nicht implementiert:

class Blob(Shape):
    pass   # forgot to implement area() and perimeter()

b = Blob()
# TypeError: Can't instantiate abstract class Blob with abstract methods area, perimeter

ABCs bieten Ihnen das Sicherheitsnetz eines formalen Vertrags und erlauben dabei weiterhin Laufzeit-Polymorphismus.

ABCs vs. Duck Typing — Was sollten Sie verwenden?

  • Duck Typing ist einfacher und flexibler. Bevorzugen Sie es für kleine interne Codebasen und Skripte.
  • ABCs machen die erwartete Schnittstelle explizit, fangen fehlende Methoden-Fehler früh ab und erscheinen in der IDE-Autovervollständigung und in Typprüfern. Bevorzugen Sie sie in größeren Codebasen, öffentlichen Bibliotheken und überall dort, wo Sie einen Vertrag erzwingen möchten.

Ein reales Beispiel: Zahlungsverarbeitung

Polymorphismus glänzt bei Plugin-artigen Designs. Betrachten Sie ein Zahlungssystem, das mehrere Anbieter unterstützen muss:

from abc import ABC, abstractmethod


class PaymentProvider(ABC):
    @abstractmethod
    def charge(self, amount: float, currency: str) -> bool:
        """Attempt to charge the given amount. Return True on success."""

    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        """Refund a previous transaction. Return True on success."""


class StripeProvider(PaymentProvider):
    def charge(self, amount: float, currency: str) -> bool:
        print(f"[Stripe] Charged {amount} {currency}")
        return True

    def refund(self, transaction_id: str) -> bool:
        print(f"[Stripe] Refunded transaction {transaction_id}")
        return True


class PayPalProvider(PaymentProvider):
    def charge(self, amount: float, currency: str) -> bool:
        print(f"[PayPal] Charged {amount} {currency}")
        return True

    def refund(self, transaction_id: str) -> bool:
        print(f"[PayPal] Refunded transaction {transaction_id}")
        return True


def process_order(provider: PaymentProvider, amount: float) -> None:
    success = provider.charge(amount, "USD")
    if success:
        print("Order complete.")


process_order(StripeProvider(), 99.99)
# [Stripe] Charged 99.99 USD
# Order complete.

process_order(PayPalProvider(), 49.5)
# [PayPal] Charged 49.5 USD
# Order complete.

process_order weiß nicht und kümmert sich nicht darum, ob es einen StripeProvider oder einen PayPalProvider erhält. Das Hinzufügen eines neuen CryptoProvider erfordert nur das Schreiben der neuen Klasse — sonst ändert sich nichts. Dies ist Polymorphismus, der reale Erweiterbarkeit liefert.

Häufige Fallstricke

Fallstrick 1: Typprüfung mit type() statt isinstance()

Der Vergleich type(obj) == Dog untergräbt den Polymorphismus, weil er für Unterklassen False zurückgibt. Bevorzugen Sie isinstance(obj, Animal), das sowohl für Dog als auch für jede zukünftige Unterklasse True zurückgibt:

class Animal:
    pass

class Dog(Animal):
    pass

d = Dog()

# Fragile — breaks for subclasses:
print(type(d) == Animal)   # False

# Correct — subclass-aware:
print(isinstance(d, Animal))  # True

Fallstrick 2: Inkonsistente Methodensignaturen

Polymorphismus setzt voraus, dass alle Implementierungen einer Methode dieselben Argumente akzeptieren. Wenn Dog.speak() ein Argument erfordert, das Cat.speak() nicht hat, werden Aufrufer, die sie einheitlich behandeln, Fehler verursachen:

# Inconsistent — will cause errors in a loop
class Dog:
    def speak(self, volume):   # extra argument!
        return f"Woof at volume {volume}"

class Cat:
    def speak(self):
        return "Meow!"

Halten Sie Methodensignaturen über polymorphe Klassen hinweg konsistent.

Fallstrick 3: Veränderliche Standardargumente in überschriebenen Methoden

Dies ist ein allgemeinerer Python-Fallstrick, aber er trifft Menschen in Klassenhierarchien besonders: Verwenden Sie niemals ein veränderliches Standardargument (list, dict) als Standardargumentwert — es wird einmal erstellt und über alle Aufrufe hinweg geteilt.

# Bug: the list is shared across all instances
class Item:
    def __init__(self, tags=[]):   # BAD
        self.tags = tags

# Fix:
class Item:
    def __init__(self, tags=None):
        self.tags = tags if tags is not None else []

Zusammenfassung

KonzeptBedeutung
Method OverridingEine Unterklasse stellt ihre eigene Version einer Elternmethode bereit
Dynamisches DispatchPython wählt die richtige Methodenversion zur Laufzeit
Duck TypingJedes Objekt mit den richtigen Methoden funktioniert, unabhängig von seiner Klasse
Operator-ÜberladungDunder-Methoden (__add__, __len__, …) machen Operatoren polymorph
Abstrakte BasisklassenErzwingen formal die Schnittstelle, die Unterklassen implementieren müssen

Polymorphismus macht Code erweiterbar ohne Modifikation. Schreiben Sie Funktionen, die von Verhalten (Methodennamen) abhängen, nicht von konkreten Typen, und Ihr Code nimmt neue Klassen natürlich ohne Änderungen auf.

Übungen

Übung
Which of the following statements about Python polymorphism are correct?
Which of the following statements about Python polymorphism are correct?
Was this page helpful?