Der Python-Kurs: Vererbung
Willemers Informatik-Ecke
Methoden Python-Kurs Module

Erbschaftsangelegenheiten

Klassen haben oft Überschneidungen. Kunden und Angestellte sind beides Personen. Wenn wir Angestellte in der Klasse Angestellter und Kunden in der Klasse Kunde modellieren, gibt es einige gemeinsame Felder, wie etwa Namen und Adresse, die sie gemeinsam haben.

Anstatt den Code nun doppelt zu schreiben, was ein guter Programmierer immer vermeiden wird, kann man die Gemeinsamkeiten wie Namen und Adresse in einer Klasse Person zusammenfassen und diese als Basisklasse zu den Klassen Angestellter und Kunde machen.

Dementsprechend ist eine Vererbung immer eine IST-EIN-Beziehung. Ein Kunde IST EINE (spezielle) Person. Ein Angestellter IST EINE Person. Ein Angestellter ist aber kein Kunde. Während ein Kunde immer eine Person ist, ist nicht jede Person ein Kunde.

Beispiel mit Kunden und Angestellten

Beispielsweise kann eine Klasse Person den Namen, Adresse, Telefonnummer und weiteres enthalten. Dann gibt es aber auch speziellere Personen, die zusätzliche Eigenschaften hat. Ein Angestellter ist bei einer Firma angestellt, ein Kunde hat eine Kundennummer.

Nun können die Klassen Angestellter und Kunde die Basisklasse Person erweitern. Damit erben sie von der Klasse Person alle Attribute und Methoden und müssen sie nicht explizit noch einmal implementieren.

Der Programmierer der abgeleiteten Klassen fügt nur noch spezielle Elemente der neuen Klasse hinzu oder überschreibt Methoden, die sich in seiner Klasse anders verhalten sollen als in der Basisklasse.

class Person:
    def __init__(self):
        self.name = "Hugo"
        self.adr = "Holzweg 1"

class Angestellter(Person):
    def __init__(self):
        Person.__init__(self) # Basisklasse initialisieren
        self.firma = "Klug KG"

class Kunde(Person):
    def __init__(self):
        Person.__init__(self)
        self.kunr = 1234

ich = Angestellter()
print(ich.name)

Syntaktisch wird so eine erweiternde Klasse dadurch gekennzeichnet, dass in der Klammer hinter dem Klassenname die Basisklasse aufgeführt wird. Im Beispiel sieht man also, dass die Klasse Kunde die Klasse Person erweitert.

Darüber hinaus muss der Konstruktor der erweiternden Klasse aber auch den Konstruktor der Basisklasse aufrufen, damit diese überhaupt angelegt wird.

Wird nun ein Objekt der Klasse Angestellter angelegt, enthält sie automatisch auch die Elemente der Klasse Person. Man kann also über das Objekt der Klasse Angestellter auf das Attribut name der Klasse Person zugreifen.

Methoden vererben

Genau wie die Attribute werden auch die Methoden vererbt. Eine Methode, die in der Basisklasse definiert ist, kann auch über ein Objekt einer abgeleiteten Klasse aufgerufen werden.

class Person:
    def __init__(self):
        self.name = "Hugo"
    def getName(self):
        return self.name

class Kunde(Person):
    def __init__(self):
        Person.__init__(self)
        self.kunr = 1234

du = Kunde()
print(du.getName())  # liefert die Ausgabe Hugo

Die abgeleitete Klasse kann die geerbte Methode aber auch selbst definieren und damit überschreiben. Dann wird diese aufgerufen.

class Person:
    def __init__(self):
        self.name = "Hugo"
    def getName(self):
        return self.name

class Kunde(Person):
    def __init__(self):
        Person.__init__(self)
        self.kunr = 1234
    def getName(self):
        return "Kunde"
    
du = Kunde()
print(du.getName() # liefert die Ausgabe Kunde

Die überschriebene Methde getName verdeckt also die Methode getName der Basisklasse. Soll die Methode der Basisklasse auch aufgerufen werden, muss sie unter Angabe des Klassennamens aufgerufen werden.

class Person:
    def __init__(self):
        self.name = "Hugo"
    def getName(self):
        return self.name

class Kunde(Person):
    def __init__(self):
        Person.__init__(self)
        self.kunr = 1234
    def getName(self):
        return "Kunde: " + Person.getName(self)
    
du = Kunde()
print(du.getName()) # liefert die Ausgabe Kunde: Hugo

Konstruktorparameter

Erhält die Basisklasse einen Parameter, muss dieser bei der Initialisierung bedient werden. Das gilt auch bei der Vererbung. Der Konstruktor der Klasse Person erhält den Parameter name. Nun muss bei den beiden erweiternden Klassen jeweils der Auruf von __init__ mit einem Parameter versehen werden.

Die Klasse Angestellter lässt sich den Parameter durch den eigenen Konstruktor geben und fordert bei der Gelegenheit gerade noch als Parameter den Firmennamen ein. Der Name wird also an die Basisklasse durchgereicht.

Die Klasse Kunde besetzt den Namen konstant mit einem festen Wert und kann so ihren Konstruktor ohne Parameter belassen.

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

class Angestellter(Person):
    def __init__(self, name, fa):
        Person.__init__(self, name)
        self.firma = fa

class Kunde(Person):
    def __init__(self):
        Person.__init__(self, "Kunde")
        self.kunr = 1234

ich = Angestellter("Hugo", "Klug AG")
print(ich.name)
du = Kunde()
print(du.name)
Das Objekt ich hat neben dem Namen aus Person auch Zugriff auf das Attribut firma von Angestellter, aber natürlich nicht auf kunr von Kunde. Beim Objekt du ist es umgekehrt.

Polymorphie

Polymorphie ist ein großes Thema in streng typisierten, kompilierenden Programmiersprachen wie Java oder C++. Ein reiner Python-Programmierer wird die Problematik kaum verstehen, weil sie sich mehr oder weniger von selbst auflöst.

Polymorpie bedeutet, dass ein Objekt, bzw. dessen Referenz weiß, zu welcher erweiterten Klasse es gehört, obwohl es über eine Referenz der Basisklasse aufgerufen wird.

Das ist reichlich abstrakt, wird aber am Beispiel hoffentlich klarer. Man erzeugt eine Liste vom Typ Person. Dann kann man dort Objekte von Person, Kunde und Angestellter hineinstecken. Polymorphie bedeutet, dass bei einem Aufruf eines ursprünglich als Kunde angelegten Objekt bei einem Aufruf der Methode getName nicht die Methode des Typs der Liste, also Person aufgerufen wird, sondern die Methode von Kunde. Das Objekt vergisst also nicht, von welcher Klasse es ist. Ähnliches geschieht bei Parametern, die vom Typ der Basisklasse sind.

Python hat an dieser Stelle kein Problem, weil die Parametervariable ihren Typ durch die Initialisierung erhält und so automatisch der Typ vorliegt, über den die Methode aufgerufen wird. Da man in einer Pythonliste beliebige Variablen einstellen kann, ergibt sich auch hier kein überraschender Effekt.

Als Beispiel ist hier eine solche Liste angelegt.

class Person:
    def __init__(self):
        self.name = "Hugo"
    def getName(self):
        return self.name

class Angestellter(Person):
    def __init__(self):
        Person.__init__(self)
        self.firma = "Klug AG"

class Kunde(Person):
    def __init__(self):
        Person.__init__(self)
        self.kunr = 1234
    def getName(self):
        return "Kunde: " + Person.getName(self)
    
liste = [Kunde(), Person(), Angestellter()]
for person in liste:
    print(person.getName())

Bei der Ausführung ensteht die wenig überraschende Ausgabe:

Kunde: Hugo
Hugo
Hugo

Abstrakte Basisklassen

In unserem Beispiel mit Personen, Kunden und Angestellten sind alle drei Klassen nicht wirklich abstrakt. Bei anderen Erbfolgen kann das anders sein.

Beispielsweise könnte in einem Warenhaus eine Klasse Ware als Basisklasse definiert sein, die den Preis und die Artikelnummer enthält. Verkauft werden allerdings Lebensmittel, Bekleidung und Bücher, die sich durch eigenständige Eigenschaften wie Mindesthaltbarkeitsdatum, Größe und ISBN deutlich unterscheiden. Der Begriff Ware ist also abstrakt, wenn die Software vermeiden will, dass ein Objekt von Ware angelegt wird, sondern nur von abgeleiteten Klassen.

Von Haus aus bietet Python keine abstrakten Klassen an. Aber mithilfe des Moduls abc (für Abstract Base Class) können auch in Python abstrakte Klassen definiert werden.

Dazu erweitert die Abstrakte Klasse die Klasse ABCMeta und die abstrakten Methoden werden durch @abstractmethod markiert. Der Versuch, ein Objekt der Klasse Ware anzulegen, führt sofort zu einer Exception.

from abc import ABCMeta, abstractmethod
class Ware(metaclass=ABCMeta):
    @abstractmethod
    def tuwas(self):
        pass

class Bekleidung(Ware):
    def tuwas(self):
        print("tuwas")

hose = Bekleidung()
#ware = Ware() # entkommentiert führt zu Exception
Man kann zumindest abstrakte Methoden aber auch recht einfach mit Bordmitteln erreichen, in dem man bei Aufruf der Methode eine Exception auslöst. Diese zwingt dazu, die Methode in einer erweiterten Klasse zu überschreiben.

class Ware():
    def tuwas(self):
        raise NotImplementedError("abstrakt")

class Bekleidung(Ware):
    def tuwas(self):
        print("tuwas")

hose = Bekleidung()
ware = Ware() # führt zu keiner Exception
#ware.tuwas()  # hier gibt es eine Exception

Mehrfachvererbung

Python erlaubt im Gegensatz zum Beispiel zu Java Mehrfachvererbung. Man könnte Beispielsweise eine Klasse Testkaeufer definieren, die sowohl alle Bedingungen eines Angestellten als auch die eines Kunden erfüllt. Syntaktisch wird dies sehr einfach definiert, indem die Basisklassen kommasepariert nacheinander in der Klassenklammer aufgeführt werden.

class TestKaeufer(Kunde, Angestellter):

Mehrfachvererbungen werden erst dann kritisch, wenn Elemente gleichen Namens enthalten sind. Es muss dann klar geregelt sein, welche der Eigenschaften verwendet wird. Und hier verwendet Python einfach die Basisklasse, die zuerst in der Klammer genannt wird.

class Person:
    def __init__(self):
        self.name = "Hugo"
        self.adr = "Holzweg 1"

class Angestellter(Person):
    def __init__(self):
        Person.__init__(self) # Basisklasse initialisieren
        self.firma = "Klug KG"
    def werbinich(self):
        print("Angestellter")

class Kunde(Person):
    def __init__(self):
        Person.__init__(self)
        self.kunr = 1234
    def werbinich(self):
        print("Kunde")        

class TestKaeufer(Kunde, Angestellter):
    pass

ich = TestKaeufer()
ich.werbinich()
Hier erscheint auf dem Bildschirm das Wort Kunde. Tauschen Sie Kunde und Angestellter in der Klammer von TestKaeufer aus, wird auf dem Bildschirm das Wort Angestellter erscheinen.

Übungsaufgaben

In einem Warenhaus gibt es vielfältigste Waren. Allen gemeinsam ist, dass man sie verkaufen will. Darum haben alle Waren einen Preis. Damit man sie unterscheiden kann, haben sie eine Artikelnummer und eine Bezeichnung.

Die folgenden Aufgaben bauen aufeinander auf.

Die Klasse Ware

Erstellen Sie eine Klasse Ware mit den Attributen preis, bez und artnr

Neben diesen Attributen haben Waren Methoden. Zur Preisbestimmung kann es die Funktion getVerkaufspreis() und für die Inventur die Funktion getZeitwert() geben. Natürlich will die Inventur-Routine alle Waren bewerten und keine Fallunterscheidung zwischen Leberwurst und Unterwäsche machen.

Spezielle Ware Lebensmittel

Ein wichtiger Warenbestandteil sind Lebensmittel. Sie haben die gleichen Eigenschaften wie alle Waren, aber zusätzlich ein Mindesthaltbarkeitsdatum. Erstellen Sie also eine Klasse Lebensmittel.

Die Klasse soll die Basisklasse Ware erweitern, also alle von ihr erben. Das Datum können Sie durch ein Tupel oder vielleicht noch besser durch eine eigene Klasse Datum realisieren.

Spezielle Ware Bekleidung

Alle Bekleidungsartikel sind natürlich ebenfalls Waren mit all deren Eigenschaften. Sie haben aber auch eine Größe. Dafür haben sie kein Mindesthaltbarkeitsdatum. Im Gegenteil: Wenn man sie nur lang genug auf Lager hält, werden sie irgendwann wieder modern.