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.
# 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: BuddyObwohl 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.
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:
# 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:
# 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:
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:
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.00SavingsAccount 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:
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)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.
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:
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.26544Die 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:
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: S12345Beachten 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.
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:
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: EngineeringIndem 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:
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: 1Die 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():
class Vehicle:
def __init__(self, brand):
self.brand = brand
class Car(Vehicle):
def __init__(self, brand, model):
super().__init__(brand) # Bevorzugt
self.model = modelVerwenden 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:
class Car(Vehicle):
def __init__(self, brand, model):
Vehicle.__init__(self, brand) # Explizit, aber weniger flexibel
self.model = modelBei 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__:
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 ERROR32.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.
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:
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.
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 dataKeine 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:
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: DLROWOLLEHDie 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:
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: FalseBeachten 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:
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: FalseDas 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:
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: FalseDas 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:
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: TrueBeachten 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:
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: $8000In vielen Fällen ist jedoch Polymorphie (wenn jede Klasse eine gemeinsame Methode implementiert) besser als Typprüfung:
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: $8000Dieser 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
Dogist einAnimal) - Ü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.