Entwurf von Datentypen

Werbung
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
Herunterladen