Objekte

Hinweis: Dieser Zusatzblock war ursprünglich Teil des Kurses. Objekte sind ein sehr nützliches und verbreitetes Software-Konzept. Das Wissen wie man selbst Objekten erstellt, wird in den nächsten Kursen (PHY125 und PHY231) aber nicht benötigt. Dieser Block ist deshalb als Ergänzung gedacht.

Objekte

Wir wollen die Vertiefungsaufgabe "Vektorrechnung" des Input-Blocks noch einmal lösen. Dieses Mal definieren wir selbst Vektor-Objekte.

Hinweis: Im Allgemeinen ist das eine dumme Idee. Wir sollten besser die bestehenden Arrays und Matrizen von Pylab verwenden. Für unser Beispiel machen wir jetzt aber eine Ausnahme.

Aus dem Tutorial weisst du bereits, dass ein Objekt ein Software-Konzept ist. Wie reale Objekte hat auch ein Software-Objekt Eigenschaften, gespeichert in Variablen, und Fähigkeiten, gegeben durch Methoden. Variablen und Methoden werden oft auch Attribute genannt. Ein Objekt kombiniert also Daten (ein Zustand) mit dazugehörigen Algorithmen. Das ist zum Beispiel bei der sauberen Organisation von Messdaten sehr nützlich.

Ein Vektor in unserem Sinn hat nun 3 Komponenten und zum Beispiel eine Methode, um seinen Betrag zu berechnen. Das kannst du wie folgt implementieren:

import math class Vektor(object): def __init__(self, x=0, y=0, z=0): self.x = x self.y = y self.z = z def abs(self): b = math.sqrt(self.x*self.x+self.y*self.y+self.z*self.z) return b

Das Beispiel enthält viel Neues auf einmal. Wir schauen uns das gleich im Einzelnen an. Zuerst wollen wir nun unsere Klasse aber ausprobieren. Speichere den Code oben als vektor.py in deinem Arbeitsverzeichnis und setze dieses Verzeichnis dann als Arbeitsverzeichnis deiner Console in Spyder.

vektor.py können wir nun wie jedes Modul laden und dann mit der Klasse arbeiten.

# Vektor aus vektor.py laden from vektor import Vektor # eine Instanz 'a' der Klasse 'Vektor' erstellen a = Vektor(0,3,4) # die Methode 'abs' der Instanz 'a' aufrufen a.abs() # die Variable 'x' der Instanz 'a' ausgeben a.x

Hinweis: Wenn du versuchst nur a auszugeben, bekommst du eine nicht sehr informative Meldung. Grund dafür ist, dass Python nicht weiss wie es dein Objekt in einen String umwandeln soll.

Die Kommentare oben enthalten ein paar Begriffe, die eine Erklärung brauchen. Eine Instanz ist ein konkretes Objekt: dieser Vektor, jene Grafik, die Liste mit dem Namen 'l'. Klassen sind Baupläne für Objekte. Sie definieren die Attribute jeder Instanz dieser Klasse. Wie bei einem Bauplan legt die Klassendefinition fest, dass wir drei Speicherplätze für Zahlen brauchen und wie aus diesen dann der Betrag berechnet werden kann.

Was machen nun die verschiedenen Zeilen in vektor.py?

import math
Wir benötigen Funktionen des math-Moduls, also laden wir dieses.
class Vektor(object):
Das Keyword class markiert den Beginn einer Klassendefinition (analog zu def bei Funktionen). Danach folgt der Name (Vektor) und in Klammern die Grundklasse von der wir erben. (Vererbung werden genauer anschauen, bei uns steht dort immer (object)).
def __init__(self, [...] ):
Hier definieren wir die erste Funktion, die zu dieser Klasse gehört. Funktionen einer Klasse heissen auch Methoden. Die __init__-Methode wird aufgerufen, wenn ein neue Instanz dieser Klasse erzeugt wird. Jede Methode einer Klasse muss als erstes Argument self haben. Dieses wird beim Aufruf (a.abs()) automatisch übergeben.
x=0, y=0, z=0
Damit wir die Komponenten unseres Vektor festlegen können, akzeptiert die init-Methode drei zusätzliche Argumente. Wir haben hier für jedes Argument den Standardwert =0 festgelegt.
self.x = x
Die Variable x aus dem Argument der Methode existiert nur innerhalb der Methoden. Wir müssen diesen Wert deshalb dem Attribut x unseres Objekts zuordnen. Indem wir in der init-Methode self.x etwas zuweisen, fügen wir dieses Attribut auch gleich zu unserem Bauplan hinzu. (Dass self.x und x den gleichen Buchstaben verwenden, ist nicht nötig, macht den Code aber lesbarer.)
def abs(self):
Ist die Definition unserer abs-Methode.

Probiere die Vektor-Klasse weiter aus. Erstelle neue Vektor-Instanzen oder verändere den Code und schaue was passiert.

Achtung: Wenn du die Klasse veränderst, solltest du die Python-Session neu starten. So stellst du sicher, dass alle deine Objekte die richtige Version haben.

Lasse auch pylint über das Beispiel laufen. Auch hier sind die Fehlermeldungen ähnlich wie im Input-Block. Dem Beispiel fehlt die Dokumentation. Dieses Mal aber nicht nur für das Modul, sonder auch für die Klasse und die einzelnen Methoden. Ausserdem ist pylint wieder nicht mit den Namen der Variablen zufrieden. Variablen müssen mindestens drei Zeichen lang sein. Das ist normalerweise auch sinnvoll. Hier folgen wir aber lieber der aus der Mathematik bekannten Namenskonvention. Deaktiviere also wieder C0103

Aufgabe 1

Ergänze die Vektor-Klasse um eine Methode, die den Vektor mit einer Zahl multipliziert (skaliert). Implementiere zudem eine Methode um den Vektor zu normieren. Verwende dafür die abs- und skalieren-Methoden.

Lösung anzeigen

class Vektor(object): [...die __init__ und abs Metoden...] def scale(self, a): self.x = self.x*a self.y = self.y*a self.z = self.z*a def norm(self): b = self.abs() self.scale(1./b)

Aufgabe 2

Erstelle eine Klasse für ein Auto-Objekt. Das Auto soll zwei Variablen haben, die Sitzplätze repräsentieren (Fahrer und Beifahrer). Die Sitze können besetzt oder frei sind (True, False). Ausserdem hat das Auto eine Geschwindigkeit und kann beschleunigen und bremsen. (Teste deine Klasse in der Console.)

Lösung anzeigen

class Auto(object): def __init__(self): self.driver = False self.passenger = False self.speed = 0 def speedup(self): self.speed += 1 def slowdown(self): self.speed -= 1

Interfaces

Die Lösung zur letzten Aufgabe hat ein Problem. Unser Auto kann verschiedene komische Dinge.

from auto import Auto car = Auto() car.speedup() car.speed car.driver

Wie du siehst kann unser Auto ohne Fahrer fahren und der Fahrer kann auch ins fahrende Auto einsteigen. Eine sauber programmierte Klasse sollte unerwünschte Effekte möglichst verhindern. Um das zu erreichen, sollte deine Klasse ein kleine Gruppe von Attributen anbieten, die der Benutzer verwenden soll. Diese Attribute bilden zusammen das Interface der Klasse. Das Interface stellt sicher, dass keine unerwünschte Effekte auftreten (solange sich der Benutzer ans Interface hält). Was hinter der Oberfläche des Interface geschieht, sollte für den Benutzer völlig irrelevant sein.

Arbeitest du konsequent mit durchdachten Interfaces, so kannst du im inneren einer Klasse alles anpassen. Solange sich das Interface nicht verändert, musst du am Programm das Objekte dieser Klasse verwendet nichts ändern.

Um das Ein- und Aussteigen bei fahrendem Auto zu verhindern, kannst du set-Methoden verwenden

def set_driver(self, state): if (self.speed == 0): self.driver = state else: print("Das ist keine gute Idee, du fährst!")

Aufgabe 3

Passe deine Auto-Klasse so an, dass die beiden beschriebenen Probleme verhindert werden. Verhindere zudem, dass dein Auto negative Geschwindigkeiten bekommt.

Lösung anzeigen

class Auto(object): def __init__(self): self.driver = False self.passenger = False self.speed = 0 def set_driver(self, state): if (self.speed == 0): self.driver = state else: print("Das ist keine gute Idee, du fährst!") def set_passenger(self, state): if (self.speed == 0): self.passenger = state else: print("Das Auto fährt du Spinner!") def speedup(self): if (self.driver): self.speed += 1 def slowdown(self): if (self.driver) and (self.speed > 0): self.speed -= 1

Private Attribute

Auch bei unserer neuen Auto-Klasse, kann ein Benutzer ein unerwünschte Änderungen am Auto vornehmen: self.speed = -5. In C++ oder Java würdest du deshalb speed als private deklarieren. Damit könnte nur noch das Objekt selbst auf dieses Attribut zugreifen. In Python kannst du das nicht. Python erwartet, dass sich die Benutzer freiwillig an die Regeln halten. Um ein Attribut zu kennzeichnen, das nicht direkt angesprochen werden soll, verwendet Python einen Unterstrich _.

def __init__(self): self._driver = False self._passenger = False self._speed = 0

Wenn du dich nun an die Regeln hältst, kannst du die Geschwindigkeit deines Autos nicht mehr ausgeben lassen. Dafür brauchst du zusätzliche get-Methoden.

def get_driver(self): return self._driver

Aufgabe 4

Statte dein Auto mit den nötigen get-Methoden aus.

Lösung anzeigen

class Auto(object): def __init__(self): self._driver = False self._passenger = False self._speed = 0 def set_driver(self, state): if (self._speed == 0): self._driver = state else: print("Das ist keine gute Idee, du fährst!") def get_driver(self): return self._driver def set_passenger(self, state): if (self._speed == 0): self._passenger = state else: print("Das Auto fährt du Spinner!") def get_passenger(self): return self._passenger def speedup(self): if (self._driver): self._speed += 1 def slowdown(self): if (self._driver) and (self._speed > 0): self._speed -= 1 def get_speed(self): return self._speed

Funktionen

Soviel zu Autos. Wir wenden uns nun wieder unseren Vektoren zu. Schliesslich wollen wir immer noch das Skalarprodukt, den Winkel und die Fläche bestimmen.

Wie die Pylab-Arrays könnten wir nun ebenfalls eine dot-Methode für unsere Vektoren definieren. Stattdessen implementieren wir aber eine normale Funktion, die zwei Vektoren als Argumente akzeptiert.

def skalarprodukt(a, b): return a.x*b.x+a.y*b.y+a.z*b.z

In deiner Pyhon-Session kannst du nun Skalarprodukte berechnen

from vektor import Vektor, skalarprodukt a = Vektor(0,3,4) b = Vektor(3,2,1) skalarprodukt(a,b)

Aufgabe 5

Füge nun noch eine Funktionen für das Vektorprodukt zu vektor.py hinzu. Als Rückgabewert hat diese Funktion wieder einen Vektor.

Lösung anzeigen

def vektorprodukt(a, b): z = a.x*b.y-a.y*b.x x = a.y*b.z-a.z*b.y y = a.z*b.x-a.x*b.y return Vektor(x,y,z)

Programme und Module

Wie du siehst, haben wir mit vektor.py ein Modul erstellt. Wir können vektor.py oder ein Teil davon wie gewohnt importieren. Eigentlich wollten wir aber ein Programm schreiben, das eine Aufgabe löst. Du kannst dies erledigen, indem du den nötigen Code ans Ende von vektor.py setzt. Kopiere als Beispiel folgende Zeilen dort hin und führe die Datei dann aus..

a = Vektor(0,3,4) b = Vektor(3,2,1) print(skalarprodukt(a,b))

Bestens, klappt alles. Leider hat das aber einen unschönen Nebeneffekt. Starte eine neue Python-Session und importiere vektor.py.

Der Code wird nun auch ausgeführt, wenn du die Datei als Modul verenden möchtest. Das lässt sich verhindern, indem du den Code wie folgt verpackst.

if __name__ == "__main__": a = Vektor(0,3,4) b = Vektor(3,2,1) print(skalarprodukt(a,b))

So führt Python die Befehle nur aus falls das Programm als Hauptprogramm ausgeführt wird.

Aufgabe 6

Erstelle nun eine weitere Funktion in vektor.py, welche die "Vektorrechnung"-Aufgabe löst. Rufe diese Funktion aus dem if __name__ == "__main__": Block auf.

Kontrolliere, ob dein Programm funktioniert, indem du spezielle Vektoren wie (0,1,0) und (1,0,0) eintippst.

Lösung anzeigen

def aufgabe(): print("Vektor a:") try: x = float(input(" x: ")) y = float(input(" y: ")) z = float(input(" z: ")) except ValueError: return a = Vektor(x,y,z) print("Vektor b:") try: x = float(input(" x: ")) y = float(input(" y: ")) z = float(input(" z: ")) except ValueError: return b = Vektor(x,y,z) sprod = skalarprodukt(a, b) print("skalarp.:", sprod) print("winkel.: ", math.acos(sprod/(a.betrag()*b.betrag()))) print("flaeche: ", vektorprodukt(a, b).betrag())

DRY - Don't Repeat Yourself

Die Lösung zum letzten Beispiel hat einen Schönheitsfehler. Die ersten 18 Zeilen enthalten zwei Mal beinahe des gleiche. Falls wir neu nur noch Integer-Komponenten möchten, müssen wir das an sechs Orten ändern. Das sind mindestens drei zu viel. Statt den Code zum Einlesen eines Vektors zwei Mal zu kopieren, sollte dieser Code als eigene Funktion definiert werden. aufgabe() kann diese Funktion dann zweimal aufrufen.

def mach_vektor(name): print("Vektor", name, ":") try: x = float(input(" x: ")) y = float(input(" y: ")) z = float(input(" z: ")) except ValueError: return False return Vektor(x,y,z) def aufgabe(): a = mach_vektor("a") b = mach_vektor("b") if not (a and b): return [...]

Aufgaben zur Vertiefung

Autos

Erstelle auch für deine Auto-Klasse eine einfache Demo-Funktion um die Klasse auszuprobieren. (Analog zur Aufgaben-Funktion in Vektor.) Damit du etwas interessantes siehst, kannst du deine Auto-Methoden mit den passenden Geräuschen ausstatten.

Objekte zu Klassen

Erstelle eine Klasse für ein anderes "Ding", analog zum Auto-Beispiel. Versehe auch diese jeweils mit einer Demo-Funktion. Mögliche Objekte sind: Haus, Ente oder wohl nützlicher Wasserstand-Messungen eines Monats. Überlege dir zuerst auf Papier, welche Attribute und Methoden deine Klasse braucht.

Bruchrechnen

Schreibe ein Modul um mit Brüchen zu rechnen. Dafür brauchst du sicher eine Bruch-Klasse, die Nenner und Zähler (zwei Ganzzahlen) eines Bruchs speichert. Ausserdem soll dein Modul folgende Dinge können:

Du kannst diese Funktionalität sowohl als Methoden deiner Klasse oder als Funktionen deines Moduls implementieren. Entscheide selbst, wo was besser passt und welche Argumente und Rückgabewerte du brauchst.

Dokumentiere dein Modul auch mit Beispielen und teste diese mit doctest