Ein Computerprogramm verarbeitet Informationen, welche zur besseren Übersicht benannt und strukturiert werden können. Angelehnt an das "echten Leben" gibt es diverse Möglichkeiten, dem Programmcode verständlicher zu gestalten.
Beispielsweise gibt es Kategorisierungen von realen Objekten, wie "Kugelschreiber" oder "Blatt Papier". Auch gibt es jeweils konkrete Ausprägungen solcher Kategorien, also, zum Beispiel einen bestimmten Kugelschreiber und ein bestimmtes Blatt Papier. Diese Objekte können dann spezielle Attribute haben, z.B. eine Farbe, eine Form, eine Größe, eine Länge, etc., welche für das jeweilige Objekt spezifisch festgelegt sind.
Darüber hinaus gibt es auch Interaktionen zwischen Objekten, also, in unserem Beispiel kann ein bestimmter Kugelschreiber auf ein Blatt Papier "schreiben". Dies ist dann eine Aktion, die ein Kugelschreiber auf ein Objekt der Kategorie "Papier" ausführen kann. All diese Elemente haben parallelen in Python:
Alle Mitspieler in einem Programm sind Objekte. Diese beinhalten die gesamte zur Verarbeitung bereitstehende Information des Programmes. Schon bekannte Vertreter sind: Zahlen, Strings, Listen, Dictionaries, Funktionen, Module, etc. So ein Objekt ist entsprechend der zum Typen des Objekt's gehörenden Datenstruktur aufgebaut. Jeder Typ hat einen Namen. Dadurch wird wird es leichter verständlich, wozu die Datenstruktur dient und was sie repräsentieren soll. Bei der Instanzierung so einer vorgegebenen Struktur wird sie mit entsprechenden Daten befüllt, die für das jeweilige konkrete Objekt spezifisch sind.
Eine "Klasse" ist eine Blaupause für Typen und definiert dessen Struktur und Funktionsweise. Bei der "Instanzierung" einer Klasse werden Attribute (das sind Variablen, die dieser Datenstruktur angehören) nach vorgegebenen Regeln mit Daten befüllt, oder die in dieser Struktur definierten Objekte übernommen. Man spricht dann von einem "Objekt", welches aus dieser Klasse instanziert wurde.
Beispielsweise kann ein Typ "Dreieck
" für das mathematische Konzept eines Dreickes stehen.
Eine Instanz davon ist ein Objekt und besteht aus den drei konkreten Eckpunkten des Dreiecks.
Die Objekte, die bereits bei der Definition der Klasse deklariert wurden, sind für alle Instanzen der Klasse identisch. Insbesondere handelt es sich hier üblicherweise um Funktionen, die fester Bestandteil dieses Types sind. Diese Funktionen werden "Methoden" genannt. Beim Aufruf so einer assoziierten Methode wird automatisch die jeweilige Instanz als erstes Argument übergeben.
In der Praxis läuft es üblicherweise so ab,
dass die Argumente bei einer Instanzierung eines Objektes vorverarbeitet werden
und dann als "Attribute" des neu erzeugten Objekts abgespeichert werden.
Die dafür zuständige Methode hat den speziellen Namen __init__(self, ...)
.
Der Zugriff auf die Attribute des Objekts geschieht mittels "Punktnotation", d.h. <Variablenname>.<Attribut>
.
Das Ziel solch einer Sammlung von Daten und Methoden in einer Klasse ist eine Modularisierung des gesamten Programms. Dadurch lassen sich diese zusammengehörenden Elemente gemeinsam verwalten und verwenden. Die Methoden sind hierbei für alle Instanzen eines Typs gleich. Daher wirken sich Änderungen an einer Methode auf alle späteren Instanzen einer Klasse aus.
Zusammenfassung:
__init__(self, ...)
nachdem ein Objekt erzeugt wurdefrom IPython.display import SVG
SVG(filename = "res/oop1.svg")
Spezialfälle:
int
realisiert und 42
ist ein Beispiel für ein Objekt dieses Typs.basestring
ist das übergeordnete Konzept, welches z.B. der Typ str
sein kann. Ein Beispiel ist "Hase"
.Den Typ eines Objekts erfährt man mittels des eingebauten Befehls type
:
type(42)
int
type("Hase")
str
Ein komplexerer Typ ist zum Beispiel das aktuelle Datum mit der aktuellen Uhrzeit.
Es wird ein Objekt des Typs "datetime.datetime
" erzeugt, indem der Konstruktor mit ensprechenden Werten befüllt wird.
Anschließend wird mit "<variable>.hour
" und "<variable>.minute
" die darin enthaltene Stunde und Minute durch die Punktnotation abgefragt.
from datetime import datetime
earlier = datetime(2014, 10, 11, 12, 13)
earlier
datetime.datetime(2014, 10, 11, 12, 13)
earlier.hour
12
earlier.minute
13
type(earlier)
datetime.datetime
Jetzt (also zu einer späteren Zeit) wird das aktuelle Datum abgefragt.
Dies geschieht mittels der .now()
Methode.
"now
" verweist nun auf das zweite "datetime
" Objekt, mit der aktuellen Uhrzeit.
now = datetime.now()
now
datetime.datetime(2015, 6, 28, 12, 2, 40, 130423)
Nun wird die Differenz gebildet, welches ein Objekt des Typs "datetime.timedelta
" ist.
timediff = now - earlier
timediff
datetime.timedelta(259, 85780, 130423)
timediff.days
259
timediff.total_seconds()
22463380.130423
type(timediff)
datetime.timedelta
Bemerkung: Ein Tipp aus der Praxis ist, Zeitabfragen prinzipiell immer auf die UTC-Zeit umzurechnen.
datetime.utcnow()
liefert hier gleich die passend umgerechnete Zeit. Warum ist das besser?
Aus einfachen "Basistypen" (str
, int
, float
, list
, ...) werden komplexeren Typen aufgebaut.
Die Beschreibung deren Struktur sind die "Klassen" (engl. "class").
Eine Klasse wird auf die Art definiert,
dass zuerst das Schlüsselwort class
mit dem Namen der Klasse steht.
Die anschließenden Klammern geben an, ob die Klasse von einer anderen abgeleitet wird --
ist das nicht der Fall, dann gibt man das Basisobjekt object
an.
Dann folgen, eingerückt, die Attribute und Methoden der Klasse.
Im folgenden Beispiel wird mittels einer Klasse der Typ Studierende
definiert,
welcher für das Konzept eines einzelnen Studierenden steht.
Die Attribute dieser Klasse sind name
, mtrnr
und stkz
.
class Studierende(object):
name = "Fritz"
mtrnr = "0105467"
stkz = 512
Die drei Attribute dieser Klasse können mittels Punktnotation abgefragt werden:
Studierende.name
'Fritz'
Studierende.stkz
512
Es ist auch jederzeit möglich, die Werte der Attribute zu ändern oder neue Attribute hinzuzufügen:
Studierende.name = "Susi"
Studierende.name
'Susi'
Studierende.lieblinsfarbe = "purpur"
Studierende.lieblinsfarbe
'purpur'
Dies ist aber nur der erste Schritt, die Daten in einem Programm zu organisieren!
Die Quintessenz einer Klasse ist jedoch, mehr als nur eine konkrete Zusammensetzung dieser Attribute und Methoden zu haben.
Man möchte gleichzeitig mehrere Studierende erzeugen können,
die zum Beispiel in den Variablen s1
, s2
, ... , abgelegt werden
und jeweils in deren Attributen die Namen, die Matrikelnummern und Studienkennzahlen abgespeichert werden.
Hierfür gibt es in Python eine besondere Methode,
die Inititalisierungsmethode __init__(self, *args, **kwargs)
,
welche zum automatischen Setzen von Attributen verwendet wird.
Sie wird auch "Konstruktor" genannt. Diese Attribute beinhalten dann z.B. die für einen bestimmten Studierenden spezifischen Daten
in einer konkreten Instanz einer Klasse --
also nicht mehr in dem allgemeinen Klassenobjekt, welches für alle Instanzen existiert.
Das "Instanzieren" einer Klasse ist der Geburtsvorgang eines Objekts dieser Klasse.
# Definition einer zweiten Version der Klasse
class Studierende2(object):
def __init__(self, name, mtrnr, stkz):
self.name = name
self.mtrnr = mtrnr
self.stkz = stkz
Instanzierung zweier Studierender:
Es werden hier insgesamt zwei Strings und vier Integer Objekte auf zwei Instanzen des Typs "Studierende2
" aufgeteilt.
ludwig = Studierende2("Ludwig Schmidt", 9100541, 515)
clara = Studierende2("Clara Bauer", 97004131, 515)
Im Hintergrund passiert folgendes:
Python erzeugt jeweils ein neues (leeres) Objekt im Speicher.
Die Methode __init__(...)
wird aufgerufen, wobei sie als erstes Argument dieses neue (leere) Objekt erhält.
Die drei Zeilen innerhalb des Konstruktors setzen nun die Attribute des Objekts
-- referenziert durch self
--
auf die übergebenen Werte.
Dies passiert jeweils für beide Instanzierungsvorgänge und die entstehenden Objekte werden in ludwig
und clara
abgelegt.
Zugriff auf Attribute:
ludwig.name
'Ludwig Schmidt'
clara.mtrnr
97004131
Typen der Studierenden:
type(ludwig)
__main__.Studierende2
type(clara)
__main__.Studierende2
An dieser Stelle eine Zwischenfrage: Was ist der Typ einer Klasse?
Beantworte diese Frage mit dem type
Befehl und versuche dies zu erklären.
type(Studierende2)
type
Von hier aus können beliebige Operationen auf Basis beider Instanzen gemacht werden.
Addition der Matrikelnummer von ludwig
mit der Studienkennzahl von clara
:
ludwig.mtrnr + clara.stkz
9101056
Zusammensetzen der Namen von Ludwig und Clara:
ludwig.name + " & " + clara.name
'Ludwig Schmidt & Clara Bauer'
Clara's Studienkennzahl multipliziert mit dem Integer 55:
clara.stkz * 55
28325
Zur besseren Verdeutlichung, hier ein anderes Beispiel:
Es wird eine Klasse Punkt
definiert, welche einen Punkt in $\mathbb{R}^2$ repräsentert.
class Punkt(object):
def __init__(self, x, y):
self.x = x
self.y = y
p1 = Punkt(4, -1)
p1.x
4
p1.y
-1
Wiederholt sich die Verarbeitung der Informationen in den Attributen,
oder möchte man die Werte in den Attributen auf Ergebnisse von Verarbeitungen setzen,
so wird diese in einer "Methode" (engl. "method") festgelegt.
Das ist im Wesentlichen eine Funktion,
welche als erstes Argument die Instanz des Objekts (engl. "Reciever") erhält,
und die restlichen Argumente je nach Definition der Methode.
Dieses erste Argument wird üblicherweise immer self
genannt.
Dies ergibt eine direkte Assoziation von Funktionen zu Instanzen,
und trägt damit zur Übersichtlichkeit bei.
Der bereits vorgestellte Konstruktor __init__
ist genau so eine Methode,
wobei nur die Bezeichnung fest vorgegeben ist und automatisch beim Instanzieren aufgerufen wird.
Im folgenden wird eine Klasse Vorlesung
definiert,
welche einen Namen, eine Lehrveranstaltungsnummer, einen Hörsaal, eine Menge von Studierenden, usw. enthält.
class Vorlesung(object):
def __init__(self, name, lvnr):
self.name = name
self.lvnr = lvnr
self.teilnehmer = set()
def add_studierenden(self, s):
self.teilnehmer.add(s)
def anzahl_teilnehmer(self):
return len(self.teilnehmer)
pp1 = Vorlesung("Programmierpraktikum 1", 25017)
pp1.add_studierenden(clara)
pp1.add_studierenden(ludwig)
pp1.anzahl_teilnehmer()
2
... und noch eine Studierende, die sich später angemeldet hat:
paula = Studierende2("Paula Power", "1004131", 515)
pp1.add_studierenden(paula)
pp1.anzahl_teilnehmer()
3
Zur besseren Verdeutlichung, hier eine Erweiterung Punkt2
von der Klasse Punkt
.
Die Methode distanz_zum_ursprung
berechnet die Distanz zum Ursprung.
class Punkt2(object):
def __init__(self, x, y):
self.x = x
self.y = y
def distanz_zum_ursprung(self):
from math import hypot
return hypot(self.x, self.y)
p2 = Punkt2(-1, 3)
p2.distanz_zum_ursprung()
3.1622776601683795
p3 = Punkt2(5, 9.1)
p3.distanz_zum_ursprung()
10.383159442096611
Neben der schon erwähnten eingebauten __init__(...)
Methode,
gibt es noch eine Reihe anderer interessanter Methoden.
Zu den wichtigeren Methoden zählen __str__()
bzw. __repr__()
,
welche aus den Attributen des Objekts eine lesbare Zeichenkette erzeugen.
Dies macht Ausgaben für den Menschen viel informativer!
# ohne __repr__, nur wenig Information:
paula
<__main__.Studierende2 at 0x2ac92fae1c18>
# wir fügen eine `__repr__` Methode hinzu und definieren eine neue Version der Klasse
class Studierende3(object):
def __init__(self, name, mtrnr, stkz):
self.name = name
self.mtrnr = mtrnr
self.stkz = stkz
def __repr__(self):
return str(self)
def __str__(self):
return "Student/in '%s' mit Mtrnr %s" % (self.name, self.mtrnr)
Das Objekt muss neu instanziert werden und paula
's __repr__
wird in der folgenden Codezelle aufgerufen,
weil es in der letzten Zeile steht.
paula = Studierende3("Paula Power", "1004131", 515)
paula
Student/in 'Paula Power' mit Mtrnr 1004131
Nebenbemerkung: __repr__
ist allgmeiner aber __str__
kommt nur bei print
oder expliziter Zeichenkettenformatierung mittels str()
zum Zug.
print(paula)
Student/in 'Paula Power' mit Mtrnr 1004131
Gibt es nur __repr__
aber kein __str__
, so wird __repr__
als Fallback verwendet.
Die +
, -
, ~
, +=
, ... Operatoren können für jede Klasse speziell definiert werden.
Beispielsweise lassen sich so mathematische Operationen für spezielle Objekte implementieren.
Siehe Liste dieser Methoden in Python2.
Als Beispiel wird Vorlesung2
implementiert, welche das Hinzufügen eines Studierenden mittels +=
Operator erlaubt.
Hierfür wird die Methode __iadd__
angegeben.
Wichtig ist (um keine Verwirrungen durch None
-Fehler auszulösen), dass das Objekt mittels self
wieder zurückgegeben wird, um der auf der linken Seite stehenden Variablen zugewiesen werden zu können.
class Vorlesung2(object):
def __init__(self, name, lvnr):
self.name = name
self.lvnr = lvnr
self.teilnehmer = set()
def __iadd__(self, s):
self.teilnehmer.add(s)
return self
def anzahl_teilnehmer(self):
return len(self.teilnehmer)
pp2 = Vorlesung2("Programmierpraktikum 2", 25017)
pp2 += clara
pp2 += ludwig
pp2 += paula
Bei dem Abruf des Attributs .teilnehmer
sieht man bereits,
dass sich die Definition von __repr__
bzw. __str__
für paula
in der Auflistung der Elemente als nützlich erweist.
pp2.teilnehmer
{<__main__.Studierende2 at 0x2ac92faee518>, Student/in 'Paula Power' mit Mtrnr 1004131, <__main__.Studierende2 at 0x2ac92faee550>}
Ein weiteres Beispiel zur besseren Verdeutlichung:
Multiplikation zweier Zahlen modulo 13,
die beide als Instanz des Typs Mod13
deklariert werden:
class Mod13(object):
def __init__(self, n):
self.n = n % 13
def __repr__(self):
return str(self.n)
def __mul__(self, other):
return Mod13(self.n * other.n)
z1 = Mod13(4)
z2 = Mod13(9)
z1, z2
(4, 9)
"handelsübliche" Multiplikation von 4 * 9
, die allerdings über die __mul__
Methode der Klasse Mod13
abgewickelt wird.
Daher ist das Ergebnis nicht 36!
z1 * z2
10
Der zurückgegebene Typ ist definitionsgemäß wieder Mod13
:
type(z1 * z2)
__main__.Mod13
Dies schließt den ersten Teil zur Einführung in Klassen & Objekte ab. Der folgende Teil beschreibt, wie komplexere Strukturen aufgebaut werden können, wie Typen von Objekten erfragt werden und wie die Hintergründe dieser Techniken in Python sind.
Das "Delegation Pattern" ist eine sehr häufig verwendete Methode, wie modularisierte Klassen aufgebaut werden. Im Konstruktor einer Klasse wird eine andere Klasse instanziert, als Attribut gespeichert und später dessen Attribute/Methoden verwendet. Dies erlaubt, problemlos größere und mächtigere Strukturen aufzubauen, während gleichzeitig -- Dank dieser Modularisierung -- die Komplexität in Schach gehalten wird.
Es folgt ein einfaches, abstraktes Beispiel, in dem eine Klasse Pipe
eine Klasse Circle
verwendet.
Wichtig ist zu erkennen, dass
Circle
in das Attribut circle
bei der Klasseninstanzierung der Pipe
-Klasse in die Variable pipe
passiert,volume
, die Methode area
des Attributes circle
aufgerufen wird.class Circle(object):
def __init__(self, r):
self.r = r
def area(self):
from math import pi
return 2 * self.r**2 * pi
class Pipe(object):
def __init__(self, diameter, height):
self.circle = Circle(diameter / 2.)
self.height = height
def volume(self):
return self.circle.area() * self.height
pipe = Pipe(4.6, 10.0)
pipe.volume()
332.38050274980003
Das Ableiten einer Klasse hat nichts mit dem mathematischen Ableiten zu tun. Es handelt sich hier um eine Technik, um schon bestehende Definitionen zu erweitern. Dies ist fundamental anders, als die schon vorgestellte "Delegation".
Beispielsweise möchten wir eine Vorlesung2
Klasse definieren,
die alles von Vorlesung
erbt und noch eine weitere Methode hinzufügt.
Erzielt wird dies durch Angeben der Elternklasse in der Definitionszeile der Klasse:
class Vorlesung3(Vorlesung):
def remove_studierenden(self, s):
self.teilnehmer.remove(s)
Instanzierung und Methoden wie add_studierenden
funktionieren wie in Vorlesung
:
ana2 = Vorlesung3("Analysis 2", 25007)
ana2.add_studierenden(paula)
ana2.anzahl_teilnehmer()
1
Neu ist hingegen, dass nun auch remove_studierenden
existiert:
ana2.remove_studierenden(paula)
ana2.anzahl_teilnehmer()
0
Allgemeiner: Die abgeleitete Kind-Klasse erbt alle Methoden und Attribute der Eltern-Klasse. Dabei können Kind-Klassen die Methoden überschreiben. Das heißt, sie definieren eine gleichlautende Methode, welche effektiv die Methode der Eltern-Klasse ersetzt.
Hierbei ist es weiterhin möglich, auf die Eltern-Methode zuzugreifen.
Hierfür gibt es das Schlüsselwort super
: super(Kind, self).methode(...)
.
Besonders häufig wird dies in der __init__
-Methode verwendet, um nach einer Initialisierung der Attribute der Kind-Klasse auch die Eltern-Klasse zu initialisieren.
Es folgt nun ein abstrakteres Kind/Eltern Klassen Beispiel, welches all dies verdeutlicht.
class Eltern(object):
def __init__(self, x):
self.x = x
print("Eltern __init__ fertig. x = %d" % self.x)
def double(self):
print("2 * x = %d" % (2*self.x))
class Kind(Eltern):
def __init__(self, x, y):
super(Kind, self).__init__(x)
self.y = y
print("Kind __init__ fertig. y = %d" % self.y)
def double(self):
print("2*x + 2*y = %d" % (2*self.x + 2*self.y))
Die Eltern-Klasse mit der double()
Methode:
e = Eltern(21)
e.double()
Eltern __init__ fertig. x = 21 2 * x = 42
Wir testen nun die Kind-Klasse mit der überschriebenen double()
Methode und der Initialisierung der Eltern-Klasse im __init__
Konstruktor.
Man sieht, bei der Ausführung des Konstruktors wird zuerst der Konstruktor der Elternklasse abgearbeitet,
und anschließend der der Kindklasse.
k = Kind(11, 12)
k.double()
Eltern __init__ fertig. x = 11 Kind __init__ fertig. y = 12 2*x + 2*y = 46
Prinzipiell gibt es daher drei Möglichkeiten, wie Klassen und Klassen-Ableitungen zur Modularisierung verwendet werden können:
Oft muss anhand des Types eines Objektes entschieden werden, was zu tun ist oder ob überhaupt ein Objekt mit dem richtigen Typ übergeben wurde.
Das wird mit der eingebauten Funktion isinstance
getestet:
Basistypen:
isinstance(9, int)
True
k
ist vom Typ Kind
, jedoch aufgrund der Ableitung der Klasse indirekt auch eine Instanz der Klasse Eltern
:
isinstance(k, Eltern)
True
e
ist Instanz der Eltern-Klasse, und daher keine Instanz der abgeleiteten Kind-Klasse:
isinstance(e, Kind)
False
... und genausowenig ein Basistyp:
isinstance(e, float)
False
Tipp: Strings müssen in Python 2 gegen den Typ basestring
getestet werden.
In Python 3 ist alles unicode und es reicht str
:
isinstance("Uµı¢ð€-string", str)
True
isinstance("ASCII STRING", str)
True
Intern sind die Attribute einer Klasse durch das bereits vorgestellte Dictionary implementiert.
Einzig die Punktnotation ist neu, und nur bei Klassen verfügbar.
Sie wird über die Methode "getattr"
der Basisklasse object
bereitgestellt.
Für spezielle Aufgaben kann sie beliebig erweitert werden.
Hier nun der Inhalt des Dictionaries der Kindes k
:
k.__dict__
{'x': 11, 'y': 12}
type(k.__dict__)
dict
Änderungen an dem Dictionary dieser Kind-Instanz ...
k.__dict__["z"] = 99
... haben sofort eine Auswirkung. Hier nun der Zugriff auf das neu erstellte Attribut z
des Objekt's k
:
k.z
99
Wo ist die Methode double()
? Wie schon erwähnt, Methoden werden in der Klasse definiert.
Klassen haben ebenfalls dieses __dict__
-Dictionary, allerdings hier eben ohne die objektspezifischen Attribute x
und y
:
Kind.__dict__
mappingproxy({'__init__': <function Kind.__init__ at 0x2ac92f85fea0>, '__module__': '__main__', '__doc__': None, 'double': <function Kind.double at 0x2ac92f85ff28>})
Um weitere strukturelle Informationen einer Klasse oder Objektinstanz abzurufen gibt es "Reflection". Dies wird in einem späteren Kapitel genauer erklärt. Für Ungeduldige: inspect py2 bzw. inspect py3
Etwas verwirrend, aber durchaus sinnvoll, ist die Tatsache, dass Klassen selbst Objekte sind.
Sie sind Instanzen des Typs type
und können auch in einem Programm erzeugt werden bzw.
lässt sich das interne Verhalten einer Klasse dadurch beeinflussen.
type(Studierende3)
type
Abseits der hier vorgestellten Art mittels class ...:
eine Klasse zu definieren,
können Klassen auch rein programmatisch definiert werden.
Hier wird nun eine Klasse ProgCls
mittels der type()
Funktion erzeugt.
Sie hat eine __init__
-Methode, welche das Attribut .x
setzt und eine Methode f()
mit einer kleinen Berechnung.
Die damit erzeugte Klasse, in der Variablen ProgCls
, kann wie gewohnt zu Objekten (hier pc1
und pc2
) instanziert werden.
ProgCls = type('ProgCls', (), {
'f' : lambda self : 3 * self.x,
'__init__': lambda self, x : setattr(self, 'x', x)
})
pc1 = ProgCls(21)
pc1.f()
63
pc2 = ProgCls(8)
pc2.f()
24
Wer mehr zu den einzelnen Themen lernen möchte: