3.8    Objektorientierte Programmierung

Bei der objektorientierten Programmierung (OOP) geht es darum, große Programme in überschaubare Teile zu zerlegen. Diese Teile werden als Klassen bezeichnet. Eine Klasse besteht aus Eigenschaften (Attribute) und Methoden. Bei diesen Methoden handelt es sich um selbst erstellte Python-Funktionen, die innerhalb einer Klasse definiert werden. Während der Programmausführung werden aus diesen Klassen Objekte erzeugt. Mithilfe dieser Objekte werden dann die Berechnungen durchgeführt. Das Design der Programmiersprache Python beruht auf den Prinzipien des objektorientierten Paradigmas.

Dass die Programmiersprache Python sich aus Klassen zusammensetzt, erkennen Sie an der Ermittlung des Typs einer Variablen: Wenn Sie in der Python-Konsole type(variable) eingeben, erhalten Sie immer als Antwort class .... Die drei Punkte stehen für den Namen einer Klasse. Dem Anwender bleiben die Details des objektorientierten Designs der Programmiersprache Python verborgen.

Selbst geschriebene Python-Programme müssen aber nicht dem objektorientierten Paradigma gehorchen. Das beweisen die bisher vorgestellten Programmbeispiele.

Damit Sie sich besser vorstellen können, was unter OOP zu verstehen ist, zeige ich Ihnen anhand einiger Beispiele, wie objektorientierte Programme prinzipiell aufgebaut sind. Damit Sie das objektorientierte Paradigma besser verstehen, sollten Sie sich auf die Begriffe Datenkapselung, Konstruktor und Vererbung konzentrieren.

3.8.1    Definition einer Klasse

Jedes objektorientierte Design beginnt mit den Definitionen der im Programm vorkommenden Klassen.

Klasse

Eine Klasse vereinigt Daten (Eigenschaften) und Methoden (Funktionen). Klassen sind die kleinsten Einheiten eines objektorientierten Programms. Eine Klassendefinition beschreibt, wie Objekte aufgebaut sind und welche Operationen auf sie ausführbar sind.

Nach der UML-Notation (Unified Modeling Language) werden Klassen als Klassendiagramme dargestellt. Abbildung 3.5 zeigt ein Klassendiagramm für die Berechnung des Umfangs und Flächeninhalts eines Kreises.

Klassendiagramm für die Klasse »Kreis«

Abbildung 3.5     Klassendiagramm für die Klasse »Kreis«

Eine Klasse wird durch ein Rechteck symbolisiert, das in drei Teile aufgeteilt ist. Im oberen Teil steht der Name der Klasse. Er sollte immer mit einem Großbuchstaben beginnen, so verlangt es die Konvention.

Im mittleren Teil stehen die Eigenschaften der Objekte, die Bestandteile der Klasse sind. Bei einem Kreis sind das die Kreiszahl π und der Radius. Das Minuszeichen bedeutet, dass diese Eigenschaften vor einem unkontrollierten äußeren Zugriff geschützt werden müssen. In der Sprache der OOP heißt das: Sie sind private. Dieses Prinzip wird als Datenkapselung bezeichnet. Bezogen auf die Kreisberechnung bedeutet das, dass die Kreiszahl π während der Programmausführung nicht durch eine neue Zuweisung geändert werden darf. In Python gibt es das Schlüsselwort private nicht, stattdessen wird eine private Variable durch zwei Unterstriche markiert: __pi.

Datenkapselung

Unter Datenkapselung versteht man die Verhinderung des unkontrollierten Zugriffs auf die Eigenschaften (Attribute) einer Klasse.

Im unteren Teil stehen die Methoden, die die Berechnungen durchführen sollen. Methoden sind die selbst erstellten Python-Funktionen.

Methode

Die innerhalb einer Klasse definierten Python-Funktionen werden als Methoden bezeichnet.

Das Pluszeichen symbolisiert, dass die Methoden öffentlich (public) sein sollen, d. h., auf sie kann außerhalb der Klassendefinition zugegriffen werden.

Ein objektorientiertes Programm besteht immer aus einem Definitionsteil, das sind die Klassendefinitionen, und aus einem Ausführungsteil. Im Ausführungsteil werden die Objekte durch eine Zuweisung erzeugt:

objName = Klasse(parameterliste)

Auf der linken Seite des Zuweisungsoperators steht ein frei wählbarer Bezeichner. Auf der rechten Seite des Zuweisungsoperators steht der Name der Klasse mit der in runde Klammern eingeschlossenen Liste der Parameter.

Auf die Methoden einer Klasse wird mit Punktoperator zugegriffen:

objName.methode()

Objekt

Ein Objekt ist ein symbolisch adressierter Speicherbereich, in dem alle Daten und Methoden der Klassendefinition hinterlegt sind. Es ist ein konkretes Exemplar einer Klasse. Es hat einen Namen, und mit diesem Namen kann auf die Methoden einer Klasse zugegriffen werden. Von einer Klasse können beliebig viele Objekte erzeugt werden.

Wenn ein Objekt erzeugt wird, dann wird der sogenannte Konstruktor

def __init__(self, parameterliste): 

aufgerufen.

Die zwei Unterstriche markieren Python-interne Spezialfunktionen. In diesem Fall geht es um die Initialisierung der Eigenschaften (deshalb init).

Konstruktor

Ein Konstruktor ist eine spezielle Methode, die beim Erzeugen eines Objekts aufgerufen wird. Er sorgt für die Initialisierung der Eigenschaften. Ein Konstruktor wird mit dem Namen der Klasse aufgerufen.

Listing 3.19 zeigt den prinzipiellen Aufbau eines OOP-Programms. Das Beispiel wurde bewusst einfach gehalten, damit die Begriffe der OOP besser verständlich werden.

01  #19_oop_kreis.py
02 class Kreis:
03 from math import pi
04 __pi=pi #Klassenvariable
05 #Konstruktor
06 def __init__(self,radius):
07 self.__r=radius #Instanzenvariable
08 #Methode für Umfangsberechnung
09 def umfang(self):
10 return 2*self.__pi*self.__r
11 #Methode für Flächenberechnung
12 def flaeche(self):
13 return self.__r**2*self.__pi
14 #Hauptprogramm
15 r=10 #Radius in m
16 objK=Kreis(r) #Konstruktor
17 #Kreis.__pi=6.28
18 print("Kreis")
19 print("Umfang:",objK.umfang(),"m")
20 print("Fläche:",objK.flaeche(),"m^2")

Listing 3.19     OOP-Programm für die Berechnung eines Kreises

Ausgabe

Kreis
Umfang: 62.83185307179586 m
Fläche: 314.1592653589793 m^2

Analyse

Eine Klassendefinition beginnt mit dem Schlüsselwort class, danach folgt der Name der Klasse Kreis. Der hinter dem Klassennamen stehende Doppelpunkt ist zwingend notwendig (Zeile 02).

In Zeile 04 wird die Klassenvariable __pi definiert. Sie hat eine globale Gültigkeit innerhalb der Klasse, sie kann also von allen Methoden der Klasse benutzt werden. Die zwei Unterstriche bewirken, dass der Wert der Kreiszahl von außen nicht geändert werden kann.

In Zeile 06 wird der Konstruktor __init__(self,radius) definiert. Wenn die Methoden flaeche() oder umfang() in dem Hauptprogramm aufgerufen werden, dann wird diese Funktion intern aufgerufen, und es wird ihr der Radius des Kreises als Argument übergeben (Zeile 16). Der Bezeichner self ist frei wählbar. Dieser Name hat sich aber als Konvention durchgesetzt. In Zeile 07 wird der Instanzenvariablen self.__r der Wert der Variablen radius zugewiesen. Diese Instanzenvariable kann jetzt in allen Methoden der Klasse benutzt werden.

In den Zeilen 09 bis 13 werden die Methoden umfang(self) und flaeche(self) definiert. Sie benötigen jeweils nur den einen Parameter self. Mit self kann über den Punktoperator auf die Klassen- und Instanzenvariablen zugegriffen werden (Zeilen 10 und 13).

In Zeile 16 erzeugt der Konstruktor Kreis(r) das Objekt objK. Objekte werden also durch Zuweisungen erzeugt. Als Argument wird der Radius r übergeben.

In den Zeilen 19 und 20 werden die Methoden flaeche() und umfang() innerhalb der print-Funktion aufgerufen. Der Zugriff auf die Methoden einer Klasse erfolgt immer über den Punktoperator mit der allgemeinen Syntax objektName.methodenname().

Wenn Sie den Kommentar in Zeile 17 entfernen und das Programm neu starten, ändert sich das Ergebnis nicht. Wenn Sie dagegen einen oder beide Unterstriche vor der Konstanten pi entfernen, ändert sich das Ergebnis. Das Programm erfüllt also nicht mehr das Prinzip der Datenkapselung.

Wenn Sie Zeile 02 bis 13 in eine Datei mit dem Namen kreis.py speichern, können Sie die Klasse Kreis für die Berechnungen in anderen Programmen wiederverwenden. Mit der Anweisung from kreis import * werden die Methoden dieser Klasse importiert.

3.8.2    Vererbung

Bei dem Konzept der Vererbung geht es darum, einmal erstellte Klassen wiederzuverwenden. Das reduziert nicht nur den Programmieraufwand, sondern verhindert auch aufwendige Fehlersuchen (Debugging).

Am Beispiel einer Zylinderberechnung soll eine Vererbungsbeziehung veranschaulicht werden. Für das Volumen eines Zylinders gilt:

formula

Wenn die Kreisfläche πr2 schon mit der Klasse Kreis berechnet wurde, kann die Klasse Zylinder dieses Ergebnis verwenden: Es braucht nur noch mit der Höhe h des Zylinders multipliziert zu werden.

Für die Oberfläche eines Zylinders gilt:

formula

Hier können die Kreisfläche πr2 und der Kreisumfang 2πr, die in der Klasse Kreis berechnet wurden, wiederverwendet werden.

Vererbung

Eine Basisklasse stellt anderen Klassen (abgeleitete Klassen) ihre Eigenschaften und Methoden zur Verfügung.

Implementiert wird die Vererbungsbeziehung zwischen Zylinder und Kreis mit der Notation Zylinder(Kreis). Die Basisklasse wird also in runde Klammern eingeschlossen.

Listing 3.20 zeigt, wie die Vererbungsbeziehung zwischen Zylinder und Kreis in Python programmiert werden kann. Das Programm berechnet die Oberfläche und das Volumen eines Zylinders nur, wenn Sie die Programmzeilen 02 bis 13 aus Listing 3.19 in die Datei kreis.py gespeichert haben.

01  #20_vererbung.py
02 from kreis import *
03 #Zylinder erbt von Kreis
04 class Zylinder(Kreis):
05 def __init__(self,radius,hoehe):
06 super().__init__(radius)
07 self.r=radius
08 self.h=hoehe
09 #Methode für Oberflächenberechnung
10 def oberflaeche(self):
11 return super().umfang()*self.h+2*super().flaeche()
12 #Methode für Volumenberechnung
13 def volumen(self):
14 return super().flaeche()*self.h
15 #Hauptprogramm
16 r=1 #Radius in m
17 h=5 #Höhe in m
18 objZ=Zylinder(r,h)
19 print("Zylinder")
20 print("Oberfläche:",objZ.oberflaeche(),"m^2")
21 print("Volumen: ",objZ.volumen(),"m^3")
22 #Typabfrage
23 print(type(Kreis))
24 print(type(Zylinder))
25 print(type(Kreis.flaeche))
26 print(type(Zylinder.volumen))
27 print(type(objZ.volumen))

Listing 3.20     Die Klasse »Zylinder« erbt von der Klasse »Kreis«.

Ausgabe

Zylinder
Oberfläche: 37.69911184307752 m^2
Volumen: 15.707963267948966 m^3
<class 'type'>
<class 'type'>
<class 'function'>
<class 'function'>
<class 'method'>

Analyse

In Zeile 02 werden die Methoden der Klasse Kreis importiert.

In Zeile 04 erbt die Klasse Zylinder alle Eigenschaften und Methoden der Basisklasse Kreis. Der Name der Basisklasse steht von runden Klammern umrahmt hinter dem Namen der abgeleiteten Klasse Zylinder.

Mit der eingebauten Python-Funktion super() kann auf alle Eigenschaften (Zeile 06) und Methoden (Zeilen 11 und 14) der Basisklasse Kreis zugegriffen werden.

In Zeile 18 erzeugt die Methode Zylinder() das Objekt objZ. Wichtig ist hier, sich zu verdeutlichen, dass es sich bei dieser Methode um den Konstruktor der Klasse Zylinder handelt.

In den Zeilen 20 und 21 werden die Ergebnisse für das Volumen und die Oberfläche ausgegeben.

Die Ausgaben in den Zeilen 23 bis 27 dienen zur Erfassung der Typen: Ist der Ausdruck eine Klasse, eine Funktion oder eine Methode? Mit der eingebauten Python-Funktion type(ausdruck) können Sie später NumPy-, Matplotlib-, SciPy- und SymPy-Programme ausführlicher analysieren.

Wenn der Ausdruck eine Klasse ist, wird <class 'type'> ausgegeben. Mit der Anweisung type(klassenname.funktionsname) gibt die print-Funktion <class 'function'> aus. Dieses Ergebnis ist überraschend: Denn die selbst erstellten Python-Funktionen umfang() und flaeche() wurden innerhalb der Klasse Kreis definiert. Sie müssten deshalb eigentlich Methoden sein.

Wenn dagegen mit einem Objekt auf eine Funktion zugegriffen wird, ermittelt type(), dass es sich bei der Funktion um eine Methode handelt. Mit der Anweisung type(objektname.funktionsname) ermittelt die type-Funktion <class 'method'>.