Diskrete Modellierung Wintersemester 2016/17 Martin Mundhenk Uni Jena, Institut für Informatik 14. November 2016 3.3 Entwurf von Datentypen Durch das Erzeugen von Datentypen entwickelt der Programmierer eine eigene Sprache, mit der über die Daten gesprochen“ werden kann. ” Deshalb muss man beim Entwickeln einer Idee für ein Programm das Ziel haben, die benötigten Datentypen zu verstehen und zu designen. In diesem Abschnitt werden wir (nochmal) betrachten, welche Möglichkeiten man hat, und wie man beim Design einerseits die Implementierung des Datentyps und andererseits deren Benutzung im Auge behalten muss. Das werden wir am Beispiel des Datentyps Vector machen. Vektoren und der Datentyp Vector Mit Vektoren will man z.B. im dreidimensionalen Raum eine Kraft darstellen, die in eine bestimmte Richtung wirkt. Die Größe der Kraft wird durch einen Pfeil einer bestimmten Länge in die gegebene Richtung dargestellt. Zur Beschreibung des Vektors genügt die Angabe der Koordinaten seines Endpunktes (wenn der Startpunkt der Nullpunkt ist). Dieses Konzept kann man sich in beliebigen Dimensionen vorstellen und wird ausführlich in der Linearen Algebra untersucht. 3.3.2 Wir benutzen x zur Bezeichnung eines Vektors und px0 , x1 , . . . , xn´1 q zur Bezeichnung seiner Werte. Beispiel: x “ p0, 3, 6.4, ´2.5q, y “ p17, 4q . Damit ist klar, welche Daten für einen Vektor gespeichert werden müssen. Variante 1: Speicherung eines Vektors als Array. Der Konstruktor wird mit einem Array aufgerufen und nimmt es für die Daten. # Definition der Klasse Vector class Vector: def __init__(self,a): self._koordinaten = a self._n = len(a) def __str__(self): return str(self._koordinaten) # test.py benutzt Datentyp Vector t = [1,2,3] v = Vector(t) print v t[1] = 999 print v # python test.py # [1, 2, 3] # [1, 999, 3] Problem: Daten sind nicht gekapselt und können (versehentlich) von außen verändert werden! Variante 2: Speicherung eines Vektors als Array. Der Konstruktor wird mit einem Array aufgerufen. Er legt eine Kopie des Arrays zum Speichern der Daten an. # Definition der Klasse Vector class Vector: def __init__(self,a): self._koordinaten = a[:] self._n = len(a) def __str__(self): return str(self._koordinaten) # test.py benutzt Datentyp Vector t = [1,2,3] v = Vector(t) print v t[1] = 999 print v v._koordinaten[1] = 999 print v # python test.py # [1, 2, 3] # [1, 2, 3] # [1, 999, 3] Die Daten des Objekts können jetzt nur noch mutwillig“ verändert werden ” (gegen die Konvention). 3.3.4 Es besteht auch die Möglichkeit, Datentypen mit unveränderbaren Daten zu definieren. Dazu kann man den eingebauten Datentyp tuple benutzen. Er ist ähnlich wie array, aber man kann die Einträge nicht ändern. Variante 3: Speicherung eines Vektors als Array. Der Konstruktor wird mit einem Array aufgerufen. Er legt ein tuple zum Speichern der Daten an. # Definition der Klasse Vector class Vector: def __init__(self,a): self._koordinaten = tuple(a) self._n = len(a) def __str__(self): return str(self._koordinaten) # # # # # test.py benutzt Datentyp Vector t = [1,2,3] v = Vector(t) print v t[1] = 999 print v v._koordinaten[1] = 999 print v python test.py [1, 2, 3] [1, 2, 3] Traceback (most recent call last): File "vector3.py", line 21, in <module> if __name__=='__main__': test() File "vector3.py", line 18, in test v._koordinaten[1] = 999 TypeError: 'tuple' object does not support item assignment Einschub: Der eingebaute Datentyp tuple Teil der API: Operation tuple(a) Beschreibung ein Tupel mit den gleichen Einträgen wie Array a (a,b,c,...) ein Tupel mit den Einträgen a, b, c, . . . t[i] der i-te Eintrag von Tupel t len(t) die Anzahl der Einträge im Tupel t for v in t: Iteration über die Einträge im Tupel t Weitere Beispiele: t = (1,2,3) (x,y,z) = t tuple unpacking tuple eignet sich zur Rückgabe mehrfacher Funktionswerte . . . 3.3.6 Bisher gesehene Entwurfs-Prinzipien und -Entscheidungen Prinzip: Kapselung von Daten in Objekten § verbessert die Modularität § verbessert die Klarheit des Client-Programms § verringert die Gefahr von Fehlern Entscheidung: Daten im Objekt sind veränderbar bzw. unveränderbar 3.3.7 Die Vektor-Operationen § Addition: x ` y “ px0 ` y0 , x1 ` y1 , . . . , xn´1 ` yn´1 q § Skalar-Produkt: αx “ pαx0 , αx1 , . . . , αxn´1 q § § Produkt: x ¨ y “ x0 ¨ y0 ` x1 ¨ y1 ` . . . ` xn´1 ¨ yn´1 a Größe: |x| “ x0 2 ` x1 2 ` . . . ` xn´1 2 § Richtung: x{|x| “ px0 {|x|, x1 {|x|, . . . , xn´1 {|x|q 3.3.8 Die API für Vector Operation Vector(a) spezielle Methode init (self,a) Beschreibung ein neuer Vector mit Koordinaten aus dem Array a[] x[i] getitem (self,i) i-te Koordinate von x x + y add (self,other) Summe von x und y Skalar-Produkt von float alpha und x x.skalar(alpha) x * y mul (self,other) Produkt von x und y Einheitsvektor mit der gleichen Richtung wie x x.richtung() abs(x) abs (self) Größe von x len(x) len (self) Länge von x str(x) str (self) string-Darstellung von x 3.3.9 Prinzipien zum Entwurf von APIs § Die API soll nicht zu groß werden. (Ausnahmen sind häufig benutzte eingebaute Datentypen wie z.B. str.) § Die API soll nicht zu schwer zum Implementieren sein. § Die Programme, die einen Datentyp benutzen, sollen einfach werden. § Die API soll unabhängig von den tatsächlichen Daten sein. Polymorphismus und Overloading Beim Implementieren von Methoden oder Funktionen denken wir häufug, dass sie mit Objekten bestimmter Typen aufgerufen werden. Entgegengesetzt wollen wir aber manchmal auch, dass sie mit Objekten unterschiedlicher Typen benutzt werden können. Solche Methoden (bzw. Funktionen) nennt man polymorph. Overloading ist eine spezielle Art von Polymorphismus. Es erlaubt z.B., dass gleiche Operatoren abhängig vom Datentyp der Operanden unterschiedliche Implementierungen haben. class A: def __init__(self,i): self._i = i class B: def __init__(self,i): self._i = i def op(self): print "Klasse A" def op(self): print "Klasse B" # test.py x = A(5) x.op() x = B(5) x.op() # python test.py # Klasse A # Klasse B Spezielle Methoden Ein weiteres Beispiel für Overloading sind die speziellen Methoden. Sie erlauben, z.B. arithmetische Operatoren statt Methodenaufrufen zu verwenden. class Vector: def __init__(self,a): self._koordinaten = tuple(a) self._n = len(a) def __add__(self,other): ergebnis = stdarray.create1D(self._n,0) for i in range(self._n): ergebnis[i] = self._koordinaten[i] + other._koordinaten[i] return Vector(ergebnis) # test.py benutzt Vector x = Vector([1,2,3]) y = Vector([4,5,6]) z = x + y print x, y, z # python test.py # (1, 2, 3) (4, 5, 6) (5, 7, 9) 3.3.12 Spezielle Methoden für arithmetische Operatoren 3.3.13 Spezielle Methoden für Gleichheit Gleichheit von Referenzen is Die Operatoren is bzw. is not prüfen, ob zwei Referenzen gleich sind. >>> a >>> b >>> c >>> a False >>> a True >>> d >>> a False = [1,2] = [1,2] = a is b is c = (1,2) is d is und is not sind auf alle Datentypen anwendbar. 3.3.14 Gleichheit der Werte des Objekts == kann auf Objekte mit Wert vom gleichen Datentyp angewendet werden. Es kann durch die spezielle Methode eq () definiert werden. Ist sie nicht definiert, dann ersetzt Python es durch is . class Charge1: def __init__(self, x0, y0, q0): self._xkoord = x0 self._ykoord = y0 self._ladung = q0 # test.py benutzt Charge1 c1 = Charge1(1,2,3) c2 = Charge1(1,2,3) print c1==c2 # test.py # False class Charge2: def __init__(self, x0, y0, q0): ... def __eq__(self,other): if self._xkoord != other._xkoord: return False if self._ykoord != other._ykoord: return False if self._ladung != other._ladung: return False return True # test.py benutzt Charge2 c1 = Charge2(1,2,3) c2 = Charge2(1,2,3) print c1==c2 # test.py # True 3.3.15 Hashing Die eingebaute hash()-Funktion gibt für jedes Objekt ein int zurück – den sog. Hashcode. Sie kann durch die spezielle Methode hash() überladen werden. Wir nennen ein Objekt hashbar, falls: § Die Gleichheitsoperation == ist auf das Objekt anwendbar. § Mittels == gleiche Objekte haben den gleichen Hashcode. § Der Hashcode eines Objekts ändert sich nie. class Charge3: def __init__(self, x0, y0, q0): self._xkoord = x0 self._ykoord = y0 self._ladung = q0 def __eq__(self,other): if self._xkoord != other._xkoord: return False if self._ykoord != other._ykoord: return False if self._ladung != other._ladung: return False return True def __hash__(self): a = (self._xkoord, self._ykoord, self._ladung) return hash(a) # test.py benutzt Charge3 c1 = Charge3(1,2,3) c2 = Charge3(1,2,3) print c1 is c2 print hash(c1) print hash(c2) # python test.py # False # -378539185 # -378539185 3.3.17 Spezielle Methoden für weitere Vergleichsperatoren 3.3.18 Spezielle Methoden für eingebaute Funktionen Fast alle Operatoren – z.B. += – können überladen werden. Es gibt weitere spezielle Methoden, die wir später noch benutzen werden. 3.3.19 Die Implementierung von Vector import stdarray, math class Vector: def __init__(self,a): self._koordinaten = tuple(a) self._n = len(a) def __add__(self,other): ergebnis = stdarray.create1D(self._n,0) for i in range(self._n): ergebnis[i] = self._koordinaten[i] + other._koordinaten[i] return Vector(ergebnis) def skalar(self,alpha): ergebnis = stdarray.create1D(self._n,0) for i in range(self._n): ergebnis[i] = alpha * self._koordinaten[i] return Vector(ergebnis) def __mul__(self,other): ergebnis = 0 for i in range(self._n): ergebnis += self._koordinaten[i] * other._koordinaten[i] return ergebnis 3.3.20 def richtung(self): return self.skalar(1.0/abs(self)) def __abs__(self): return math.sqrt(self*self) def __len__(self): return self._n def __getitem__(self,i): return self._koordinaten[i] def __str__(self): return str(self._koordinaten) def test(): print abs(Vector([1,2,3])) ... if __name__=='__main__': test() 3.3.21 Vererbung Python unterstützt die Definition von Beziehungen zwischen Klassen. Man kann (neue) Klassen als Unterklassen bereits definierter (Ober-)Klassen definieren. Die Unterklassen erben Instanzen-Variablen und Methoden ihrer Oberklassen. Dieses Konzept ist sehr umfangreich und wir begnügen uns mit einem (etwas murksigen) Beispiel auf der nächsten Seite. 3.3.22 class Bruch: # Klasse für ungekürzte rationale Zahlen def __init__(self, zaehler, nenner): self._z = zaehler self._n = nenner def __str__(self): return str(self._z) + " / " + str(self._n) class gBruch(Bruch): # Unterklasse von Bruch für gekürzte Brüche def _ggT(self): a = self._z b = self._n while a%b != 0: (a,b) = (b,a%b) return b def _kuerzen(self): g = self._ggT() self._z = self._z / g self._n = self._n / g def __str__(self): self._kuerzen() return Bruch.__str__(self) # test.py benutzt Burch und gBruch a = Bruch(27,6) print a a = gBruch(27,6) print a # python test.py # 27 / 6 # 9 / 2 Zusammenfassung Jede Programmiersprache bietet andere Möglichkeiten zum Entwurf von Datentypen. Wir haben verschiedene Prinzipien gesehen, die stets beachtet werden sollten. Beim Entwurf eines modularen Programms werden die Aufgaben, die das Programm lösen sollen, in Einzelteile zerlegt. Beim Entwurf von Datentypen werden die Daten, mit denen das Programm arbeiten soll, und die typische Benutzung der Daten zusammengesetzt. Diese beiden unterschiedlichen Aufgaben müssen beim Programmieren zusammen gelöst werden. 3.3.24