Zum Inhalt
wannacode

Vererbung

Kurz gesagt: Vererbung bedeutet, dass eine Klasse (die Unterklasse) die Felder und Methoden einer anderen Klasse (der Basisklasse) bekommt, sodass das Gemeinsame an einer Stelle steht.

Problem

Zwei Kopien desselben Codes

Das Gehaltsprogramm hat eine Klasse Employee mit Name, Gehalt und einer Methode für eine Erhöhung. Dann brauchtest du auch einen Manager, der zusätzlich einen Bonus bekommt. Am schnellsten ging es, Employee in eine neue Klasse Manager zu kopieren und den Bonus hinzuzufügen.

Einen Monat später gibt jemand eine Erhöhung von -10% statt 10% ein. Also fügst du Employee eine Prüfung hinzu. In Manager vergisst du sie (die Methode heißt raise_salary, weil raise in Python ein reserviertes Wort ist):

# in Employee
def raise_salary(self, percent):
if percent > 0:
self._salary += self._salary * percent // 100
# in Manager: eine Kopie von vor der Prüfung
def raise_salary(self, percent):
self._salary += self._salary * percent // 100
ivan = Employee("Ivan", 2000)
maria = Manager("Maria", 3000, 500)
ivan.raise_salary(-10)
maria.raise_salary(-10)
print(ivan)
print(maria)
Ausgabe
Ivan: 2000 €
Maria: 2700 €

Das Gehalt von Ivan ist geschützt, aber das von Maria ist um 300 € gefallen. Die zwei Klassen sind zu 90% gleich, und trotzdem muss jede Korrektur zweimal gemacht werden.

Die Idee

Das Gemeinsame kommt in die Basisklasse

Ein Manager ist ein Mitarbeiter, nur mit etwas zusätzlich. Sag es im Code: class Manager(Employee):. In Manager schreibst du nur, was neu ist, den Bonus:

class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus
@property
def bonus(self):
return self._bonus
maria = Manager("Maria", 3000, 500)
maria.raise_salary(-10)
maria.raise_salary(10)
print(maria)
print(f"Bonus: {maria.bonus} €")
Ausgabe
Maria: 3300 €
Bonus: 500 €

Manager hat jetzt alles, was Employee hat: einen Namen, ein Gehalt, raise_salary() mit der Prüfung, __str__(). Deshalb ist die Erhöhung um -10% nicht durchgegangen. Die Prüfung steht einmal da und funktioniert für beide Klassen.

Das nennt man Vererbung. Employee ist die Basisklasse (Elternklasse), und Manager ist die Unterklasse. Die Unterklasse nimmt alles von der Basisklasse und fügt ihr Eigenes hinzu.

Ob Vererbung sinnvoll ist, prüfst du mit dem Satz „X ist ein Y“. „Ein Manager ist ein Mitarbeiter“ stimmt. „Eine Rechnung ist ein Mitarbeiter“ stimmt nicht, also erbt die Rechnung nicht von Employee, auch wenn sie einen Betrag und einen Namen hat.

Probier es aus

Was bekommt die Praktikantin?

Intern hat nichts Eigenes: Der Rumpf ist nur pass.

Was gibt das Programm aus?

class Intern(Employee):
pass
ana = Intern("Ana", 1200)
ana.raise_salary(5)
print(ana)
Antwort

Die Idee

Die Basisklasse kommt zuerst

Manager hat ein eigenes __init__, weil er auch einen Bonus hat. Name und Gehalt richtet aber das __init__ von Employee ein. Wer ruft es auf?

Python ruft nur ein __init__ auf: das der Klasse des Objekts. Das der Basisklasse musst du selbst aufrufen:

def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus

super() ist „die Basisklasse dieser Klasse“, und super().__init__(name, salary) ruft ihr __init__ mit diesen Argumenten auf. Es richtet _name und _salary ein, und dann richtet Manager _bonus ein. Python erlaubt diese Zeile überall in __init__, aber schreib sie zuerst: So ist der Teil der Basisklasse fertig, bevor die Unterklasse ihn benutzt.

Wenn du sie vergisst, sagt Python nichts, bis das Feld gebraucht wird. print(maria) bricht mit AttributeError: 'Manager' object has no attribute '_name' ab: Das Feld wurde einfach nie angelegt.

Es gibt ein Objekt. Es enthält den Teil der Basisklasse und den Teil der Unterklasse, und jeder Teil wird von seinem eigenen Konstruktor eingerichtet.

def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus
maria = Manager("Maria", 3000, 500)
print(f"{maria}, Bonus {maria.bonus} €")
Schritt für Schritt, in Worten
  1. Manager(...) erzeugt ein Objekt. Vorerst ist es leer: Jedes Feld entsteht, wenn __init__ ihm einen Wert gibt.
  2. __init__ von Manager startet mit den drei Argumenten.
  3. Die Basisklasse kommt zuerst: super().__init__(name, salary) ruft __init__ von Employee auf. Python ruft es nicht für dich auf.
  4. __init__ von Employee richtet seinen Teil ein: _name und _salary.
  5. Erst dann richtet der Rumpf von Manager den Bonus ein.
  6. Es gibt ein Objekt, und jeder Teil wird von seinem eigenen Konstruktor eingerichtet.
Genauer, für Neugierige
  • __init__ wird vererbt wie jede andere Methode. Deshalb nimmt Intern ohne eigenes __init__ Name und Gehalt von Employee. Ein eigenes __init__ schreibst du, wenn die Unterklasse etwas Neues hat, und dann ersetzt es das der Basisklasse.
  • In älterem Code siehst du super(Manager, self).__init__(...) oder Employee.__init__(self, ...). Sie machen dasselbe. super() mit leeren Klammern ist die heutige Schreibweise.

Probier es aus

Welcher Konstruktor läuft zuerst?

Hier ruft die Unterklasse die Basisklasse nicht ausdrücklich auf:

Was gibt das Programm aus?

class Account:
def __init__(self):
print("Das Konto ist eröffnet")
class SavingsAccount(Account):
def __init__(self):
print("Zinsen: 3%")
savings = SavingsAccount()
Antwort

Probier es aus

Finde den Fehler

Maria soll ein Gehalt von 3000 € und einen Bonus von 500 € haben. Aber das Programm gibt aus:

Ausgabe
Maria: 500 €

In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Der Unterstrich: für die Klasse und ihre Unterklassen

Am Jahresende wird der Bonus des Managers zum Gehalt addiert. Die Methode steht in Manager, und das Gehalt ist _salary in Employee. Die Property salary liest es nur, und hier musst du es ändern.

In Java und C# gibt es dafür eine dritte Zugriffsstufe, protected: Die Klasse und ihre Unterklassen sehen es, alle anderen nicht. Python hat weder private noch protected. Der Unterstrich ist eine Abmachung genau dafür: „intern, für die Klasse und ihre Unterklassen“. Deshalb schreibt Manager direkt in self._salary, und Employee ändert sich nicht:

class Employee:
def __init__(self, name, salary):
self._name = name
self._salary = salary
# ... der Rest wie vorher
class Manager(Employee):
# ... _bonus und __init__
def merge_bonus(self):
self._salary += self._bonus
self._bonus = 0
maria = Manager("Maria", 3000, 500)
maria.merge_bonus()
print(maria)
print(f"Bonus: {maria.bonus} €")
Ausgabe
Maria: 3500 €
Bonus: 0 €

Die Abmachung ist kein Schutz: maria._salary = 0 im Programm geht auch durch, ohne Fehler. Der Unterstrich sagt Menschen „von außen nicht anfassen“, und die Unterklasse gehört „zur Familie“. Deshalb benutzt die Unterklasse ihn nur, wenn sie das Feld wirklich ändern muss, nicht um die Prüfung in raise_salary() zu umgehen.

Genauer, für Neugierige
  • Zwei Unterstriche, self.__salary, kommen private am nächsten. Python benennt das Feld still in _Employee__salary um. In Manager ist self.__salary schon _Manager__salary, und so ein Feld gibt es nicht: AttributeError: 'Manager' object has no attribute '_Manager__salary'. Das schützt vor zufälligen Namenskonflikten, nicht vor Menschen: maria._Employee__salary erreichst du trotzdem.

Probier es aus

Was fehlt?

Eine Praktikantin ist eine Mitarbeiterin. Was steht in der Lücke?

class Intern():
    pass

Die Idee

Basisklassen und ein gemeinsamer Vorfahr

In Java, C# und JavaScript hat eine Klasse genau eine Basisklasse. Python erlaubt auch mehrere: class WorkingStudent(Employee, Student): bekommt die Methoden beider. Das nennt man Mehrfachvererbung. Hier nehmen wir jeweils eine Basisklasse. Mehrere brauchen wir in der Lektion über Interfaces.

Die Kette kann lang sein. Intern erbt von Employee. Und wovon erbt Employee, wenn es kein Basisklasse in Klammern hat? Jede Klasse ohne Basisklasse erbt von object. Das ist die gemeinsame Basisklasse aller Klassen. Deshalb hat sogar eine leere Klasse Methoden:

class Ticket:
pass
a = Ticket()
b = Ticket()
print(a == a)
print(a == b)
Ausgabe
True
False

== ruft die Methode __eq__ auf, und die kommt von object. Vorerst sagt sie dir nur, ob die zwei Variablen auf dasselbe Objekt zeigen.

Auch __str__ kommt von dort. Wenn du sie nicht schreibst, gibt print(ticket) etwas wie <__main__.Ticket object at 0x7f3a2c1d5e50> aus: den Namen der Klasse und eine Adresse, die bei jedem Start anders ist. Im Modul „Klassen und Objekte“ hast du ein eigenes __str__ geschrieben. Jetzt weißt du, dass es die Version von object ersetzt. Mehr dazu in der nächsten Lektion.

Genauer, für Neugierige
  • In Python gibt es keine Klasse, von der man nicht erben kann. typing.final verspricht es, aber nur ein Werkzeug wie mypy prüft es, nicht Python selbst.
  • Bei mehreren Basisklassen sucht Python eine Methode der Reihe nach: zuerst in der Klasse, dann in den Basisklassen von links nach rechts und zuletzt in object. Die Reihenfolge siehst du in WorkingStudent.__mro__.

Übung

Unterklassen

Löse die Aufgaben in deinem Editor. Die Basisklassen sind aus dem Modul „Klassen und Objekte“. Wenn du sie nicht zur Hand hast, schreib sie neu: Sie sind kurz.

1. Familienauto. SimpleCar hat eine Geschwindigkeit und eine Methode accelerate(), die sie um 10 erhöht. Schreib FamilyCar, das von SimpleCar erbt, mit 4 Sitzen beginnt und eine Methode add_seat() hat. Erzeuge ein Familienauto, beschleunige es und füge einen Sitz hinzu:

Ausgabe
Geschwindigkeit: 10 km/h
Sitze: 5

2. Studentin. Person hat Name, Alter und introduce(). Schreib Student, das von Person erbt und einen Notendurchschnitt grade (eine Dezimalzahl) hinzufügt. Benutze den Konstruktor der Basisklasse.

Ausgabe
Ich bin Elena, 19 Jahre alt.
Notendurchschnitt: 5.5

3. Breakdance. DanceMove hat einen Namen, eine Dauer in Sekunden und perform(). Schreib BreakDanceMove mit einer Schwierigkeit von 1 bis 10 und einer Methode show_complexity().

Ausgabe
Ich tanze moonwalk für 5 Sekunden
Ich tanze windmill für 12 Sekunden
windmill: Schwierigkeit 8 von 10

4. Elektroauto. Car hat eine Marke, Kraftstoff in Litern und die gefahrenen Kilometer. refuel(liters) tankt nach, und drive(distance) verbraucht 1 Liter auf 10 km und addiert die Kilometer (reicht der Kraftstoff nicht, sagt die Methode es und fährt nicht). Schreib ElectricCar:

  • ohne Kraftstoff und mit einem Akku, der bei 100% beginnt;
  • mit einer Effizienz: wie viele km es mit 1% Akku fährt (5 beim „Tesla“);
  • charge() lädt den Akku auf 100%;
  • drive_electric(distance) verbraucht Akku statt Kraftstoff und addiert ebenfalls die Kilometer.
Auch die Unterklasse ändert die Kilometer. Wie sollte das Feld in Car heißen? Tipp: die Karte über den Unterstrich.
Ausgabe
Skoda: 150 km, Kraftstoff 35 l
Tesla: 200 km, Akku 60%
Der Akku reicht nicht für 400 km
Tesla: 600 km, Akku 20%

Weitere Aufgaben

  • Arbeiter. Worker erbt von Person und fügt eine Berufsbezeichnung und eine Methode work() hinzu:
Ausgabe
Ich bin Peter, 34 Jahre alt.
Ich arbeite als Buchhalter
  • Praktikantin mit Mentor. Füge Intern ein Feld mit dem Namen des Mentors hinzu und übergib ihn im Konstruktor, zusammen mit Name und Gehalt für die Basisklasse. Jetzt braucht Intern doch ein eigenes __init__.

Test

Teste dich

1. class Manager(Employee). Was stimmt?
2. Das Feld _name steht in Employee. Kann Manager in einer seiner Methoden self._name = "Maria" schreiben?
3. Was macht super().__init__(name, salary) im __init__ von Manager?
4. Wie viele Basisklassen kann eine Klasse in Python haben?
5. Manager hat ein eigenes __init__, das super().__init__() nicht aufruft. Was passiert?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·