Python & AI Tutorials Logo
Python Programmierung

32. Klassen mit Vererbung und Polymorphie erweitern

In Kapitel 30 haben wir gelernt, wie Sie Ihre eigenen Klassen erstellen, um reale Konzepte zu modellieren. Wir haben Klassen wie BankAccount und Student gebaut, die Daten und Verhalten zusammengefasst haben. Aber was passiert, wenn Sie eine neue Klasse erstellen müssen, die einer vorhandenen ähnlich ist, aber einige Unterschiede oder Ergänzungen hat?

Vererbung (inheritance) ist Pythons Mechanismus, um neue Klassen auf Basis vorhandener zu erstellen. Statt Code zu kopieren und einzufügen, können Sie eine Unterklasse (subclass) erstellen, die automatisch alle Attribute und Methoden von einer Elternklasse (parent class) (auch Basisklasse (base class) oder Superklasse (superclass) genannt) erhält und dann hinzufügen oder anpassen, was Sie brauchen.

Dieses Kapitel untersucht, wie Vererbung Ihnen erlaubt, Hierarchien verwandter Klassen aufzubauen, wie Sie geerbtes Verhalten anpassen und wie Polymorphie (polymorphism) es ermöglicht, unterschiedliche Klassen austauschbar zu verwenden, wenn sie gemeinsame Schnittstellen teilen.

32.1) Unterklassen aus bestehenden Klassen erstellen

32.1.1) Die grundlegende Syntax der Vererbung

Vererbung erlaubt Ihnen, eine neue Klasse (eine Unterklasse(subclass) oder Kindklasse(child class)) auf Basis einer bestehenden Klasse (einer Elternklasse(parent class) oder Basisklasse(base class)) zu erstellen. Die Unterklasse erbt automatisch alle Methoden und Attribute von der Elternklasse.

Wenn Sie eine Unterklasse erstellen, geben Sie die Elternklasse in Klammern nach dem Klassennamen an.

python
# Elternklasse
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
# Unterklasse, die von Animal erbt
class Dog(Animal):
    pass  # Noch kein zusätzlicher Code nötig
 
# Eine Dog-Instanz erstellen
buddy = Dog("Buddy")
print(buddy.speak())  # Output: Buddy makes a sound
print(buddy.name)     # Output: Buddy

Obwohl Dog keinen eigenen Code hat (nur pass), erbt es alles von Animal. Die Klasse Dog hat automatisch die Methode __init__ und die Methode speak von ihrer Elternklasse.

Animal

+name

+init(name)

+speak()

Dog

32.1.2) Warum Vererbung wichtig ist

Vererbung löst ein häufiges Programmierproblem: duplizierten Code. Stellen Sie sich vor, Sie bauen ein System, um unterschiedliche Arten von Mitarbeitenden zu verwalten:

python
# Ohne Vererbung – viel Duplizierung
class FullTimeEmployee:
    def __init__(self, name, employee_id, salary):
        self.name = name
        self.employee_id = employee_id
        self.salary = salary
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"
 
class PartTimeEmployee:
    def __init__(self, name, employee_id, hourly_rate):
        self.name = name
        self.employee_id = employee_id
        self.hourly_rate = hourly_rate
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"

Beachten Sie, wie name, employee_id und get_info() dupliziert sind. Mit Vererbung können wir diese Duplizierung vermeiden:

python
# Mit Vererbung – gemeinsamer Code in der Elternklasse
class Employee:
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"
 
class FullTimeEmployee(Employee):
    def __init__(self, name, employee_id, salary):
        Employee.__init__(self, name, employee_id)  # __init__ der Elternklasse aufrufen
        # Hinweis: In Abschnitt 32.3 lernen wir eine bessere Methode dafür mit super()
        self.salary = salary
 
class PartTimeEmployee(Employee):
    def __init__(self, name, employee_id, hourly_rate):
        Employee.__init__(self, name, employee_id)  # __init__ der Elternklasse aufrufen
        self.hourly_rate = hourly_rate
 
# Beide Unterklassen erben get_info()
alice = FullTimeEmployee("Alice", "E001", 75000)
bob = PartTimeEmployee("Bob", "E002", 25)
 
print(alice.get_info())  # Output: Alice (ID: E001)
print(bob.get_info())    # Output: Bob (ID: E002)

Jetzt liegen die gemeinsamen Attribute und Methoden in Employee, und jede Unterklasse definiert nur das, was sie einzigartig macht.

32.1.3) Neue Methoden zu Unterklassen hinzufügen

Unterklassen können eigene Methoden hinzufügen, die die Elternklasse nicht hat:

python
class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
    
    def get_description(self):
        return f"{self.brand} {self.model}"
 
class Car(Vehicle):
    def __init__(self, brand, model, num_doors):
        Vehicle.__init__(self, brand, model)
        self.num_doors = num_doors
    
    def honk(self):  # Neue Methode, spezifisch für Car
        return "Beep beep!"
 
class Motorcycle(Vehicle):
    def __init__(self, brand, model, has_sidecar):
        Vehicle.__init__(self, brand, model)
        self.has_sidecar = has_sidecar
    
    def rev_engine(self):  # Neue Methode, spezifisch für Motorcycle
        return "Vroom vroom!"
 
# Jede Unterklasse hat die Methoden der Elternklasse plus ihre eigenen
my_car = Car("Toyota", "Camry", 4)
print(my_car.get_description())  # Output: Toyota Camry
print(my_car.honk())             # Output: Beep beep!
 
my_bike = Motorcycle("Harley", "Sportster", False)
print(my_bike.get_description())  # Output: Harley Sportster
print(my_bike.rev_engine())       # Output: Vroom vroom!

Die Klasse Car hat sowohl get_description() (geerbt) als auch honk() (eigen). Die Klasse Motorcycle hat get_description() (geerbt) und rev_engine() (eigen).

32.1.4) Neue Attribute zu Unterklassen hinzufügen

Unterklassen können auch eigene Instanzattribute hinzufügen. Typischerweise machen Sie das in der __init__-Methode der Unterklasse:

python
class BankAccount:
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        return self.balance
 
class SavingsAccount(BankAccount):
    def __init__(self, account_number, balance, interest_rate):
        BankAccount.__init__(self, account_number, balance)
        self.interest_rate = interest_rate  # Neues Attribut
    
    def apply_interest(self):  # Neue Methode, die das neue Attribut nutzt
        interest = self.balance * self.interest_rate
        self.balance += interest
        return interest
 
# SavingsAccount hat alle BankAccount-Attribute plus interest_rate
savings = SavingsAccount("SA001", 1000, 0.03)
savings.deposit(500)  # Geerbte Methode
print(f"Balance: ${savings.balance}")  # Output: Balance: $1500
 
interest_earned = savings.apply_interest()
print(f"Interest earned: ${interest_earned:.2f}")  # Output: Interest earned: $45.00
print(f"New balance: ${savings.balance:.2f}")      # Output: New balance: $1545.00

SavingsAccount hat account_number und balance von BankAccount sowie zusätzlich das eigene Attribut interest_rate.

32.1.5) Mehrere Vererbungsebenen

Klassen können von Klassen erben, die selbst von anderen Klassen erben, wodurch eine Vererbungshierarchie entsteht:

python
class LivingThing:
    def __init__(self, name):
        self.name = name
    
    def is_alive(self):
        return True
 
class Animal(LivingThing):
    def __init__(self, name, species):
        LivingThing.__init__(self, name)
        self.species = species
    
    def move(self):
        return f"{self.name} is moving"
 
class Dog(Animal):
    def __init__(self, name, breed):
        Animal.__init__(self, name, "Dog")
        self.breed = breed
    
    def bark(self):
        return f"{self.name} says: Woof!"
 
# Dog erbt von Animal, und Animal erbt von LivingThing
max_dog = Dog("Max", "Golden Retriever")
 
# Methoden aus allen drei Ebenen funktionieren
print(max_dog.is_alive())  # Output: True (from LivingThing)
print(max_dog.move())      # Output: Max is moving (from Animal)
print(max_dog.bark())      # Output: Max says: Woof! (from Dog)
 
# Attribute aus allen drei Ebenen existieren
print(max_dog.name)     # Output: Max (from LivingThing)
print(max_dog.species)  # Output: Dog (from Animal)
print(max_dog.breed)    # Output: Golden Retriever (from Dog)

LivingThing

+name

+is_alive()

Animal

+species

+move()

Dog

+breed

+bark()

Dog erbt von Animal, und Animal erbt von LivingThing. Das bedeutet, dass Dog Zugriff auf Methoden und Attribute aus beiden Elternklassen hat.

32.2) Methoden in Unterklassen überschreiben

32.2.1) Was das Überschreiben von Methoden bedeutet

Manchmal muss eine Unterklasse ändern, wie eine geerbte Methode funktioniert. Überschreiben von Methoden(method overriding) bedeutet, in der Unterklasse eine Methode mit demselben Namen zu definieren wie eine Methode in der Elternklasse. Wenn Sie diese Methode auf einer Unterklasseninstanz aufrufen, verwendet Python die Version der Unterklasse statt der der Elternklasse.

python
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):  # Die speak-Methode der Elternklasse überschreiben
        return f"{self.name} says: Woof!"
 
class Cat(Animal):
    def speak(self):  # Überschreiben mit anderem Verhalten
        return f"{self.name} says: Meow!"
 
# Jede Klasse hat ihre eigene Version von speak()
generic_animal = Animal("Generic")
print(generic_animal.speak())  # Output: Generic makes a sound
 
buddy = Dog("Buddy")
print(buddy.speak())  # Output: Buddy says: Woof!
 
whiskers = Cat("Whiskers")
print(whiskers.speak())  # Output: Whiskers says: Meow!

Wenn Sie buddy.speak() aufrufen, sucht Python zuerst in der Klasse Dog nach der Methode speak, da buddy eine Dog-Instanz ist. Weil Dog eine eigene speak-Methode definiert, verwendet Python diese Version. Wenn Dog keine speak-Methode hätte, würde Python anschließend in der Elternklasse Animal suchen und stattdessen diese Version verwenden.

Diese Suchreihenfolge – beginnend bei der Klasse der Instanz und dann weiter zur Elternklasse – ist die Art, wie das Überschreiben von Methoden funktioniert und wie Unterklassen geerbtes Verhalten anpassen.

32.2.2) Warum Methoden überschreiben?

Das Überschreiben von Methoden ermöglicht Ihnen, spezialisierte Versionen allgemeinen Verhaltens zu erstellen. Betrachten Sie eine Formen-Hierarchie:

python
class Shape:
    def __init__(self, name):
        self.name = name
    
    def area(self):
        return 0  # Standardimplementierung
    
    def describe(self):
        return f"{self.name} with area {self.area()}"
 
class Rectangle(Shape):
    def __init__(self, width, height):
        Shape.__init__(self, "Rectangle")
        self.width = width
        self.height = height
    
    def area(self):  # Überschreiben mit rechteck-spezifischer Berechnung
        return self.width * self.height
 
class Circle(Shape):
    def __init__(self, radius):
        Shape.__init__(self, "Circle")
        self.radius = radius
    
    def area(self):  # Überschreiben mit kreis-spezifischer Berechnung
        return 3.14159 * self.radius ** 2
 
# Jede Form berechnet die Fläche anders
rect = Rectangle(5, 3)
print(rect.describe())  # Output: Rectangle with area 15
 
circle = Circle(4)
print(circle.describe())  # Output: Circle with area 50.26544

Die Methode describe() wird von beiden Unterklassen geerbt und funktioniert korrekt, weil jede Unterklasse ihre eigene area()-Implementierung bereitstellt.

Wenn Sie rect.describe() aufrufen, wird die geerbte Methode describe() ausgeführt, aber self bezieht sich auf die Rectangle-Instanz. Wenn describe() also self.area() aufruft, sucht Python zuerst in der Klasse Rectangle nach area() und findet die überschreibende Version.

32.2.3) __init__ überschreiben und die Initialisierung der Elternklasse aufrufen

Wenn Sie __init__ überschreiben, müssen Sie typischerweise __init__ der Elternklasse aufrufen, um sicherzustellen, dass die Initialisierung der Elternklasse stattfindet:

python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def introduce(self):
        return f"I'm {self.name}, {self.age} years old"
 
class Student(Person):
    def __init__(self, name, age, student_id, major):
        # __init__ der Elternklasse aufrufen, um name und age zu setzen
        Person.__init__(self, name, age)
        # Danach studentenspezifische Attribute setzen
        self.student_id = student_id
        self.major = major
    
alice = Student("Alice", 20, "S12345", "Computer Science")
print(alice.name)        # Output: Alice
print(alice.student_id)  # Output: S12345

Beachten Sie, wie Student.__init__ zuerst Person.__init__(self, name, age) aufruft, um die Attribute der Elternklasse zu initialisieren, und dann die eigenen Attribute hinzufügt.

32.3) super() verwenden, um auf Verhalten der Elternklasse zuzugreifen

32.3.1) Was super() macht

Im vorherigen Abschnitt haben wir Elternmethoden explizit aufgerufen: ParentClass.method(self, ...). Python bietet dafür einen saubereren Weg: die Funktion super(). super() gibt ein temporäres Objekt zurück, mit dem Sie Methoden der Elternklasse aufrufen können, ohne die Elternklasse explizit zu benennen.

python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # Sauberer als Animal.__init__(self, name)
        self.breed = breed
    
    def speak(self):
        parent_sound = super().speak()  # speak() der Elternklasse aufrufen
        return f"{parent_sound} - specifically, Woof!"
 
buddy = Dog("Buddy", "Labrador")
print(buddy.speak())
# Output: Buddy makes a sound - specifically, Woof!

Die Verwendung von super() hat mehrere Vorteile:

  • Sie müssen die Elternklasse nicht explizit benennen
  • Es funktioniert korrekt bei Mehrfachvererbung (später behandelt)
  • Es macht Code leichter wartbar, wenn Sie die Elternklasse ändern

32.3.2) super() in __init__ verwenden

Die häufigste Verwendung von super() ist das Aufrufen von __init__ der Elternklasse:

python
class Employee:
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
        self.is_active = True
    
    def deactivate(self):
        self.is_active = False
 
class Manager(Employee):
    def __init__(self, name, employee_id, department):
        super().__init__(name, employee_id)  # Attribute der Elternklasse initialisieren
        self.department = department
        self.team = []  # Managerspezifisches Attribut
    
    def add_team_member(self, employee):
        self.team.append(employee)
 
# Manager bekommt alle Employee-Attribute plus seine eigenen
sarah = Manager("Sarah", "M001", "Engineering")
print(sarah.name)        # Output: Sarah
print(sarah.is_active)   # Output: True
print(sarah.department)  # Output: Engineering

Indem super().__init__(name, employee_id) aufgerufen wird, stellt die Klasse Manager sicher, dass die gesamte Initialisierungslogik aus Employee ausgeführt wird, einschließlich dem Setzen von is_active auf True.

32.3.3) Elternmethoden mit super() erweitern

Sie können super() verwenden, um eine Elternmethode zu erweitern, statt sie vollständig zu ersetzen:

python
class BankAccount:
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.balance = balance
        self.transaction_count = 0
    
    def deposit(self, amount):
        self.balance += amount
        self.transaction_count += 1
        return self.balance
 
class CheckingAccount(BankAccount):
    def __init__(self, account_number, balance, overdraft_limit):
        super().__init__(account_number, balance)
        self.overdraft_limit = overdraft_limit
    
    def deposit(self, amount):
        was_overdrawn = self.balance < 0
        
        # deposit der Elternklasse aufrufen, um die grundlegende Logik zu behandeln
        new_balance = super().deposit(amount)
        
        # Checking-spezifisches Verhalten hinzufügen
        if was_overdrawn and new_balance >= 0:
            print("Account is no longer overdrawn")
        return new_balance
 
checking = CheckingAccount("C001", -50, 100)
checking.deposit(75)
# Output: Account is no longer overdrawn
print(f"Balance: ${checking.balance}")  # Output: Balance: $25
print(f"Transactions: {checking.transaction_count}")  # Output: Transactions: 1

Die Methode CheckingAccount.deposit() ruft super().deposit(amount) auf, um die grundlegende Einzahlungslogik zu behandeln (Kontostand und Transaktionszähler aktualisieren), und fügt anschließend eine eigene Prüfung des Überziehungsstatus hinzu.

32.3.4) Wann super() vs. direkter Aufruf der Elternklasse

Verwenden Sie in den meisten Fällen super():

python
class Vehicle:
    def __init__(self, brand):
        self.brand = brand
 
class Car(Vehicle):
    def __init__(self, brand, model):
        super().__init__(brand)  # Bevorzugt
        self.model = model

Verwenden Sie direkte Elternaufrufe, wenn Sie in einem Mehrfachvererbungsszenario eine bestimmte Elternklasse aufrufen müssen (später behandelt) oder wenn Sie explizit sein möchten, welche Elternklasse Sie aufrufen:

python
class Car(Vehicle):
    def __init__(self, brand, model):
        Vehicle.__init__(self, brand)  # Explizit, aber weniger flexibel
        self.model = model

Bei einfacher Vererbung (eine Elternklasse) ist super() fast immer die bessere Wahl.

32.3.5) super() mit anderen Methoden

Sie können super() mit jeder Methode verwenden, nicht nur mit __init__:

python
class TextProcessor:
    def process(self, text):
        # Grundverarbeitung: Leerzeichen am Rand entfernen
        return text.strip()
 
class UppercaseProcessor(TextProcessor):
    def process(self, text):
        # Zuerst die Verarbeitung der Elternklasse
        processed = super().process(text)
        # Dann Großbuchstaben-Konvertierung hinzufügen
        return processed.upper()
 
class PrefixProcessor(UppercaseProcessor):
    def __init__(self, prefix):
        self.prefix = prefix
    
    def process(self, text):
        # Zuerst die Verarbeitung der Elternklasse (die auch ihre Elternklasse aufruft)
        processed = super().process(text)
        # Dann ein Präfix hinzufügen
        return f"{self.prefix}: {processed}"
 
processor = PrefixProcessor("ALERT")
result = processor.process("  system error  ")
print(result)  # Output: ALERT: SYSTEM ERROR

32.4) Polymorphie: Mit kompatiblen Klassen arbeiten

32.4.1) Was Polymorphie bedeutet

Polymorphie (polymorphism, aus dem Griechischen: „viele Formen“) ist die Fähigkeit, Objekte unterschiedlicher Klassen auf dieselbe Weise zu behandeln, wenn sie dieselben Methoden bereitstellen.

In Python können Sie, wenn mehrere Klassen Methoden mit demselben Namen haben, diese Methoden aufrufen, ohne die genaue Klasse des Objekts zu kennen.

python
class Dog:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Woof!"
 
class Cat:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Meow!"
 
class Bird:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Tweet!"
 
# Funktion, die mit jedem Objekt funktioniert, das eine speak()-Methode hat
def make_animal_speak(animal):
    print(animal.speak())
 
# Funktioniert mit unterschiedlichen Klassen
buddy = Dog("Buddy")
whiskers = Cat("Whiskers")
tweety = Bird("Tweety")
 
make_animal_speak(buddy)     # Output: Buddy says: Woof!
make_animal_speak(whiskers)  # Output: Whiskers says: Meow!
make_animal_speak(tweety)    # Output: Tweety says: Tweet!

Die Funktion make_animal_speak() ist egal, welche Klasse der Parameter animal hat—sie braucht nur, dass das Objekt eine Methode speak() hat. Das ist Polymorphie in Aktion.

32.4.2) Polymorphie mit Vererbung

Polymorphie ist besonders mächtig mit Vererbung, bei der Unterklassen Methoden der Elternklasse überschreiben:

python
class PaymentMethod:
    def process_payment(self, amount):
        return f"Processing ${amount:.2f}"
 
class CreditCard(PaymentMethod):
    def __init__(self, card_number):
        self.card_number = card_number
    
    def process_payment(self, amount):
        return f"Charging ${amount:.2f} to credit card ending in {self.card_number[-4:]}"
 
class PayPal(PaymentMethod):
    def __init__(self, email):
        self.email = email
    
    def process_payment(self, amount):
        return f"Sending ${amount:.2f} via PayPal to {self.email}"
 
class BankTransfer(PaymentMethod):
    def __init__(self, account_number):
        self.account_number = account_number
    
    def process_payment(self, amount):
        return f"Transferring ${amount:.2f} to account {self.account_number}"
 
# Funktion, die mit jeder PaymentMethod funktioniert
def complete_purchase(payment_method, amount):
    print(payment_method.process_payment(amount))
    print("Purchase complete!")
 
# Alle Zahlungsmethoden funktionieren mit derselben Funktion
credit = CreditCard("1234567890123456")
paypal = PayPal("user@example.com")
bank = BankTransfer("9876543210")
 
complete_purchase(credit, 99.99)
# Output: Charging $99.99 to credit card ending in 3456
# Output: Purchase complete!
 
complete_purchase(paypal, 49.50)
# Output: Sending $49.50 via PayPal to user@example.com
# Output: Purchase complete!
 
complete_purchase(bank, 199.00)
# Output: Transferring $199.00 to account 9876543210
# Output: Purchase complete!

Die Funktion complete_purchase() funktioniert mit jeder PaymentMethod-Unterklasse. Jede Unterklasse liefert ihre eigene Implementierung von process_payment(), aber die Funktion muss nicht wissen, mit welcher konkreten Klasse sie arbeitet.

32.4.3) Duck Typing: „Wenn es läuft wie eine Ente …“

Pythons Polymorphie setzt nicht voraus, dass Klassen durch Vererbung miteinander verwandt sind. Das nennt man Duck Typing(duck typing): „Wenn es läuft wie eine Ente und quakt wie eine Ente, dann ist es eine Ente.“ Mit anderen Worten: Python interessiert sich dafür, was ein Objekt kann (seine Methoden), nicht dafür, was es ist (seine Klasse). Wenn ein Objekt die Methoden hat, die Sie brauchen, können Sie es verwenden, unabhängig von seiner Klassenhierarchie.

python
class FileWriter:
    def __init__(self, filename):
        self.filename = filename
    
    def write(self, data):
        print(f"Writing to {self.filename}: {data}")
 
class DatabaseWriter:
    def __init__(self, table_name):
        self.table_name = table_name
    
    def write(self, data):
        print(f"Inserting into {self.table_name}: {data}")
 
class ConsoleWriter:
    def write(self, data):
        print(f"Console output: {data}")
 
# Funktion, die mit jedem Objekt funktioniert, das eine write()-Methode hat
def save_data(writer, data):
    writer.write(data)
 
# Alle drei Klassen funktionieren, obwohl sie nicht miteinander verwandt sind
file_writer = FileWriter("data.txt")
db_writer = DatabaseWriter("users")
console_writer = ConsoleWriter()
 
save_data(file_writer, "User data")
# Output: Writing to data.txt: User data
 
save_data(db_writer, "User data")
# Output: Inserting into users: User data
 
save_data(console_writer, "User data")
# Output: Console output: User data

Keine dieser Klassen erbt von einer gemeinsamen Elternklasse, aber sie funktionieren alle mit save_data(), weil sie alle eine Methode write() haben. Das ist Duck Typing—der Funktion ist die Klasse egal, sie kümmert sich nur um die Schnittstelle (die verfügbaren Methoden).

32.4.4) Praktisches Beispiel: Ein Plugin-System

Polymorphie ermöglicht flexible, erweiterbare Systeme. Hier ist ein einfaches Plugin-System zur Datenverarbeitung:

python
class DataProcessor:
    def process(self, data):
        return data  # Basisimplementierung tut nichts
 
class UppercaseProcessor(DataProcessor):
    def process(self, data):
        return data.upper()
 
class ReverseProcessor(DataProcessor):
    def process(self, data):
        return data[::-1]
 
class RemoveSpacesProcessor(DataProcessor):
    def process(self, data):
        return data.replace(" ", "")
 
class DataPipeline:
    def __init__(self):
        self.processors = []
    
    def add_processor(self, processor):
        self.processors.append(processor)
    
    def run(self, data):
        result = data
        for processor in self.processors:
            result = processor.process(result)  # Polymorpher Aufruf
        return result
 
# Eine Verarbeitungs-Pipeline aufbauen
pipeline = DataPipeline()
pipeline.add_processor(UppercaseProcessor())
pipeline.add_processor(RemoveSpacesProcessor())
pipeline.add_processor(ReverseProcessor())
 
# Daten durch die Pipeline verarbeiten
input_data = "Hello World"
output = pipeline.run(input_data)
print(f"Input:  {input_data}")   # Output: Input:  Hello World
print(f"Output: {output}")        # Output: Output: DLROWOLLEH

Die DataPipeline muss nicht wissen, welche konkreten Prozessoren sie enthält—sie ruft einfach für jeden process() auf. Sie können problemlos neue Prozessor-Typen hinzufügen, ohne den Pipeline-Code zu ändern.

32.5) Typen und Klassenbeziehungen prüfen (isinstance, issubclass)

32.5.1) Instanztypen mit isinstance() prüfen

Manchmal müssen Sie prüfen, ob ein Objekt eine Instanz einer bestimmten Klasse ist. Die Funktion isinstance() macht das:

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
class Cat(Animal):
    pass
 
buddy = Dog()
whiskers = Cat()
 
# Prüfen, ob ein Objekt eine Instanz einer Klasse ist
print(isinstance(buddy, Dog))     # Output: True
print(isinstance(buddy, Animal))  # Output: True (Dog inherits from Animal)
print(isinstance(buddy, Cat))     # Output: False
 
print(isinstance(whiskers, Cat))    # Output: True
print(isinstance(whiskers, Animal)) # Output: True
print(isinstance(whiskers, Dog))    # Output: False

Beachten Sie, dass isinstance(buddy, Animal) True zurückgibt, obwohl buddy eine Dog-Instanz ist. Das liegt daran, dass Dog von Animal erbt, also wird eine Dog-Instanz auch als Animal-Instanz betrachtet.

32.5.2) Warum isinstance() Vererbung berücksichtigt

Die Funktion isinstance() prüft die gesamte Vererbungskette:

python
class Vehicle:
    pass
 
class Car(Vehicle):
    pass
 
class ElectricCar(Car):
    pass
 
tesla = ElectricCar()
 
# Alle Vererbungsebenen prüfen
print(isinstance(tesla, ElectricCar))  # Output: True
print(isinstance(tesla, Car))          # Output: True
print(isinstance(tesla, Vehicle))      # Output: True
print(isinstance(tesla, str))          # Output: False

ist ein

ist ein

ist ein

tesla-Instanz

ElectricCar

Car

Vehicle

Das Objekt tesla ist eine Instanz von ElectricCar, aber es ist auch eine Instanz von Car und Vehicle wegen der Vererbung.

32.5.3) Mehrere Typen auf einmal prüfen

Sie können prüfen, ob ein Objekt eine Instanz einer von mehreren Klassen ist, indem Sie ein Tupel übergeben:

python
class Dog:
    pass
 
class Cat:
    pass
 
class Bird:
    pass
 
def is_pet(animal):
    return isinstance(animal, (Dog, Cat, Bird))
 
buddy = Dog()
whiskers = Cat()
tweety = Bird()
rock = "just a rock"
 
print(is_pet(buddy))     # Output: True
print(is_pet(whiskers))  # Output: True
print(is_pet(tweety))    # Output: True
print(is_pet(rock))      # Output: False

Das ist knapper als isinstance(animal, Dog) or isinstance(animal, Cat) or isinstance(animal, Bird) zu schreiben.

32.5.4) Klassenbeziehungen mit issubclass() prüfen

Die Funktion issubclass() prüft, ob eine Klasse eine Unterklasse einer anderen ist:

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
class Cat(Animal):
    pass
 
class Poodle(Dog):
    pass
 
# Klassenbeziehungen prüfen
print(issubclass(Dog, Animal))    # Output: True
print(issubclass(Cat, Animal))    # Output: True
print(issubclass(Poodle, Dog))    # Output: True
print(issubclass(Poodle, Animal)) # Output: True (indirect inheritance)
print(issubclass(Dog, Cat))       # Output: False
 
# Eine Klasse gilt als Unterklasse von sich selbst
print(issubclass(Dog, Dog))       # Output: True

Beachten Sie, dass issubclass() mit Klassen arbeitet, nicht mit Instanzen. Verwenden Sie isinstance() für Instanzen und issubclass() für Klassen.

32.5.5) Praktische Anwendungsfälle für Typprüfung

Typprüfung ist nützlich, wenn Sie je nach Typ unterschiedliches Verhalten benötigen:

python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
 
class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus
 
class Contractor:
    def __init__(self, name, hourly_rate, hours):
        self.name = name
        self.hourly_rate = hourly_rate
        self.hours = hours
 
def calculate_payment(worker):
    # Wenn Sie isinstance() mit Vererbung verwenden, prüfen Sie Unterklassen vor Elternklassen
    if isinstance(worker, Manager):  # Manager zuerst prüfen
        return worker.salary + worker.bonus
    elif isinstance(worker, Employee):  # Dann Employee (Elternklasse) prüfen
        return worker.salary
    elif isinstance(worker, Contractor):
        return worker.hourly_rate * worker.hours
    else:
        return 0
 
alice = Employee("Alice", 50000)
bob = Manager("Bob", 70000, 10000)
charlie = Contractor("Charlie", 50, 160)
 
print(f"Alice's payment: ${calculate_payment(alice)}")    # Output: Alice's payment: $50000
print(f"Bob's payment: ${calculate_payment(bob)}")        # Output: Bob's payment: $80000
print(f"Charlie's payment: ${calculate_payment(charlie)}")# Output: Charlie's payment: $8000

In vielen Fällen ist jedoch Polymorphie (wenn jede Klasse eine gemeinsame Methode implementiert) besser als Typprüfung:

python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def calculate_payment(self):
        return self.salary
 
class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus
    
    def calculate_payment(self):
        return self.salary + self.bonus
 
class Contractor:
    def __init__(self, name, hourly_rate, hours):
        self.name = name
        self.hourly_rate = hourly_rate
        self.hours = hours
    
    def calculate_payment(self):
        return self.hourly_rate * self.hours
 
# Keine Typprüfung nötig – Polymorphie erledigt das
workers = [
    Employee("Alice", 50000),
    Manager("Bob", 70000, 10000),
    Contractor("Charlie", 50, 160)
]
 
for worker in workers:
    payment = worker.calculate_payment()  # Polymorpher Aufruf
    print(f"{worker.name}'s payment: ${payment}")

Output:

Alice's payment: $50000
Bob's payment: $80000
Charlie's payment: $8000

Dieser polymorphe Ansatz ist flexibler und leichter mit neuen Worker-Typen zu erweitern. Sie müssen den aufrufenden Code nicht ändern, wenn Sie eine neue Worker-Klasse hinzufügen—stellen Sie nur sicher, dass sie eine Methode calculate_payment() hat.


Vererbung und Polymorphie sind leistungsstarke Werkzeuge, um Code zu organisieren und flexible, erweiterbare Systeme zu erstellen. Indem Sie Unterklassen erstellen, können Sie bestehenden Code wiederverwenden und gleichzeitig das Verhalten Ihrer Klassen erweitern oder anpassen. Indem Sie Methoden überschreiben, können Sie anpassen, wie Unterklassen funktionieren. Und indem Sie Polymorphie verwenden, können Sie Code schreiben, der über eine gemeinsame Schnittstelle mit vielen unterschiedlichen Klassen funktioniert.

Der Schlüssel ist, diese Konzepte bewusst einzusetzen:

  • Verwenden Sie Vererbung, wenn es eine echte „ist-ein“-Beziehung gibt (ein Dog ist ein Animal)
  • Überschreiben Sie Methoden, um Verhalten zu spezialisieren, nicht um vollständig zu ändern, was eine Klasse tut
  • Verwenden Sie super(), um Verhalten der Elternklasse zu erweitern, statt es vollständig zu ersetzen
  • Bevorzugen Sie Polymorphie (gemeinsame Methoden) gegenüber Typprüfung, wenn möglich

Wenn Sie größere Programme erstellen, helfen Ihnen diese objektorientierten Techniken dabei, Code zu schreiben, der leichter zu verstehen, zu warten und zu erweitern ist.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai