===== Objektorientierte Programmierung ===== ==== 1. Grundlagen ==== === 1.1 Konzept === Wenn wir bisher ein Programm geschrieben haben, war der Ansatz "prozedural" oder "imperativ", d.h. wir haben uns überlegt, aus welchen Einzelschritten ein Algorithmus besteht und haben dann ein Programm erstellt, welches diese Schritte durchgeführt hat. Mithilfe von Funktionen kann man ein solches Programm strukturieren und Redundanz vermeiden. Die **objektorientierte Programmierung** geht von einem anderen Ansatz aus: Ein Programm besteht nicht aus einem Ablauf von Anweisungen und Funktionen, sondern aus **Objekten**. Diese Objekte haben bestimmte Eigenschaften und Methoden und sie stehen miteinander in Verbindung. Oftmals wird das Programm dann durch Events gesteuert (z.B. Tastaturanschläge, Mausklicks etc.). === 1.2 Begriffe === |< 100% 170px >| | ** Klasse vs. Objekt ** | Eine **Klasse** ist ein **allgemeiner Bauplan** für konkrete **Objekte**. So kann man z.B. eine Klasse Auto definieren, aus welcher man die beiden Objekte //Ferrari// oder //VW// ableiten kann. Die Objekte (oder auch Instanzen genannt) sind dann konkrete Exemplare dieser Klasse. | | **Eigenschaften** | Die Objekte besitzen bestimmte **Eigenschaften** (oder **Attribute** genannt), dies sind im Prinzip die Variablen, welche die konkreten Objekte beschreiben. Z.B. ''Kilometerstand'', ''Tankfüllung'', ''MaxSpeed'', ''VerprauchPro100km'' | | **Methoden** | Die **Methoden** entsprechen den Funktionen in der üblichen Programmierung. Sie operieren auf den Objekten/Klassen und auf deren Eigenschaften. So könnte man beispielsweise eine Methode ''fahren(km)'' haben, oder eine Methode ''auftanken(liter)'' | ** Definition einer Klasse und eines Objektes in Python ** class Auto(): # eine Klasse Auto definieren pass Ferrari = Auto() # ein neues Auto-Objekt erzeugen ** Instanzattribute vs. Klassenattribute und Instanzmethoden vs. Klassenmethoden ** Man muss nun unterscheiden, ob eine Eigenschaft zu einem konkreten Objekt gehört (Instanzattribut, Instanzvariable), oder ob sie für die gesamte Klasse gleich bleibt (Klassenattribut, Klassenvariable). Bei der Klasse ''Auto'' könnte man z.B. das Klassenattribut ''Anzahl_Raeder'' haben. Da dies für alle Objekte der Klasse ''Auto'' gleich sein wird (4) und nicht vom konkreten Objekt abhängt, muss es nur einmal unter der KLasse ''Auto'' gespeichert werden. Die Attribute ''Kilometerstand'',''Tankfuellung'', ''MaxSpeed'', ''Verbrauch'' etc. wären **Instanzattribute**, sie gehören zu einer bestimmten Instanz und beschreiben ein konkretes Objekt. Genauso ist es bei den Methoden (Funktionen): **Instanzmethoden** verwenden Instanzvariablen und sind deshalb an ein konkretes Objekt gebunden. **Klassenmethoden** hingegen hängen nicht vom konkreten Objekt ab, sondern nur von der Klasse und dürfen somit auch nicht auf die Instanzattribute zugreifen. Man kann sich z.B. eine Hilfsfunktion ''steuerwert'' vorstellen, welche den Startwert des Autos und das aktuelle Alter des Autos als Parameter übernimmt und dann den aktuellen Steuerwert berechnet. Dies könnte man als Klassenmethode implementieren. Man könnte natürlich auch die Instanzvariablen ''startwert'' und ''alter'' einführen, auf welche die Funktion ''steuerwert'' zugreift - dann wäre die Methode ''steuerwert'' eine Instanzmethode. === 1.3 Diagramm der Klasse Auto mit den Instanzen Ferrari und VW === {{:ef:diegramm.png?600|}} |< 100% 150px >| | ** Konstruktor ** | Wenn man aus einer Klasse ein konkretes Objekt (d.h. eine Instanz) erstellt, dann wird eine spezielle Methode aufgerufen, der Konstruktor. In Python ist dies die ''__init__()''-Methode. Im Konstruktor werden die Objektvariablen für das konkrete Objekt gesetzt und andere Dinge erledigt, die bei der Erstellung eines Objektes gemacht werden müssen. | | **self** | Wenn man Instanzmethoden definiert, muss man jeweils zusätzlich den Parameter **self** übergeben. Dies ist eine Referenz auf das aktuell bearbeitete Objekt. Mit ''self.tankfuellung'' kann man dann z.B. auf die Tankfüllung des aktuell betrachteten Autos zugreifen. \\ Wenn man also eine Instanzmethode definiert, hat diese immer einen Parameter mehr (self), als wenn man sie aufruft (!). | | **Vererbung** | Es ist möglich, dass man eine "Kindklasse" definiert, welche alles von der Oberklasse (d.h. von der Vater- bzw. Mutterklasse) erbt. So könnte z.B. die Klasse "Säugetier" eine Unterklasse der Klasse "Tier" sein. Ruft man z.B. eine Methode eines Säugetier-Objektes auf, so wird diese auch funktionieren, wenn sie eigentlich in der Klasse "Tier" definiert wurde (sie wurde vererbt). Will man in der Kindklasse (z.B. bei der Definition des Konstruktors) die Methode der Vaterklasse aufrufen, so kann man ''super()'' verwenden, um auf die Vaterklasse zuzugreifen. | | **Datenkapselung** | Definiert man Instanzvariablen mit "normalen" Namen, so kann man von aussen darauf zugreifen, die Variablen sind "public". Definiert man Instanzvariablen mit einem anführenden Unterstrich, so zeigt man, dass diese eigentlich nicht für den Gebrauch von aussen gedacht sind (dies ist nur eine Konvention). Die Variablen sind "protected" (es besteht aber keinerlei Schutz). \\ Definiert man Instanzvariablen mit doppelten Underscores, so weiss Python, dass diese nur innerhalb des Objektes verwendet werden sollen. Diese sind "private" und man kann von aussen nicht darauf zugreifen (bzw. nur mit einem Trick). Man muss **Getter-** und **Setter-**Methoden schreiben, um diese Variablen von aussen zu manipulieren. | ==== 2. Umsetzung in Python ==== === 2.1 Eine Klasse Auto === class Auto(): anzahl_Raeder = 4 def __init__(self, ps, km, tank, verbrauch, maxspeed,alter, neupreis): self.PS = ps self.Kilometerstand = km self.Tankfuellung = tank self.MaxSpeed = maxspeed self.Verbrauch = verbrauch self.Alter = alter self.Neupreis = neupreis def tanken(self, liter): self.Tankfuellung = self.Tankfuellung + liter def fahren(self, km): self.Kilometerstand += km self.Tankfuellung = self.Tankfuellung - km * self.Verbrauch/100 def steuerWert2(self): return self.Neupreis*0.8**self.Alter @classmethod def steuerwert(cls, neupreis, alter): return neupreis* (0.8**alter) Ferrari = Auto(550,0,40,14,300,0,200000) VW = Auto(103, 87987, 38, 7, 145,4,26000) Ferrari.fahren(30) print(Ferrari.Kilometerstand) print("VW Steuerwert:" + str(Auto.steuerwert(26000, 4))) print(VW.steuerWert2()) === 2.2 Eine Klasse Haus in Minecraft mit einer Elternklsse 'Objekt'. Die Position wird vererbt === class Objekt(): yPos = (-1)*60 # kleiner Trick, um Minecraft zu überlisten (-60) def __init__(self, x, z): # jedes Objekt hat eine x- und z-Position self.xPos = x self.zPos = z def baue(self): # wird in der Kindklasse überschrieben pass class Haus(Objekt): # Haus erbt von Objekt def __init__(self, x, z, b, h): super().__init__(x, z) # Konstruktor der Elternklasse aufrufen self.__breite = b # private Variablen self.__hoehe = h def getBreite(self): # Getter-Methode return self.__breite def setBreite(self, b): # Setter-Methode if b > 30: player.say("zu breit, sorry") else: self.__breite = b def baue(self): # Die Methode 'baue' wird überschrieben blocks.fill(CONCRETE,world(self.xPos, -61, self.zPos), world(self.xPos+self.__breite, -61+self.__hoehe, self.zPos+self.__breite),FillOperation.HOLLOW) for xPos1 in range(self.xPos+2, self.xPos+self.__breite, 3): for hoehe1 in range(-61+3, -61+self.__hoehe, 4): blocks.place(GLASS, world(xPos1, hoehe1, self.zPos)) blocks.place(GLASS, world(xPos1, hoehe1, self.zPos+self.__breite)) for zPos1 in range(self.zPos+2, self.zPos+self.__breite, 3): for hoehe1 in range(-61+3, -61+self.__hoehe, 4): blocks.place(GLASS, world(self.xPos, hoehe1, zPos1)) blocks.place(GLASS, world(self.xPos+self.__breite, hoehe1, zPos1)) # Loch für Türe blocks.place(AIR, world(self.xPos+2, -60, self.zPos)) blocks.place(AIR, world(self.xPos+2, -59, self.zPos)) # Türe blocks.place(blocks.block_with_data(CRIMSON_DOOR, 4),world(self.xPos+2, -60, self.zPos)) def loesche(self): blocks.fill(AIR,world(self.xPos, -61, self.zPos), world(self.xPos+self.__breite, -60+self.__hoehe, self.zPos+self.__breite)) blocks.fill(GRASS, world(self.xPos, self.yPos-2, self.zPos),world(self.xPos+self.__breite, self.yPos-1, self.zPos+self.__hoehe)) Hauser = [] Haus1 = Haus(200,200,10,6) Haus1.baue() player.say(Haus1.xPos) #xPos ist in der Elternklasse definiert, wurde aber vererbt. # player.say(Haus1.__breite) geht nicht, da breite private ist player.say(Haus1.getBreite()) # die Breite mit der Getter-Methode holen **Auftrag 1** - Betrachte die Programme von oben - Welche Klassen bzw. Objekte wurden definiert/erstellt? - Gibt es Klassenvariablen und Klassenmethoden? Wenn "ja", welche? - Gibt es Instanzvariablen und Instanzmethoden? Welche? - Warum wurde wohl die y-Position bei 2.2 als Klassenvariable definiert? - Warum wurden wohl ''xPos'' und ''yPos'' nicht privat definiert (''__xPos'', ''__yPos'')? - Erkläre die Vererbung am Beispiel Haus. - Erkläre am konkreten Programm 2.1 die Methode ''super()''. - Wozu ist Datenkapselung gut? Erkläre, wie sie im gegebenen Programm implementiert wurde. - Was bedeuten die Begriffen **overriding (überschreiben)** und **overloading (überladen)** in der Objektorientierten Programmierung. Wird eines dieser Konzepte hier verwendet? **Auftrag 2**\\ Installiere die Umgebung processing.org - ich empfehle hier die ältere Version 3.5.4 herunterzuladen: [[https://processing.org/releases]] und füge rechts oben den Modus "Python für Processing 3" hinzu.\\ * Betrachte das Car-Beispiel und vesuche es zu verstehen. * Erweitere das Car-Beispiel. * Füge ein Element hinzu, so dass erkennbar ist, wo vorne/hinten ist (z.B. Lichter) * Ändere das Programm, so dass man ein Auto mit den Pfeiltasten steuern kann. * Versuche das Programm zu ändern, indem du eine Vaterklasse "Fahrzeug" definierst. Überlege dir, welche Eigenschaften und Methoden in die Klasse Fahrzeug und welche in die Klasse Auto gehören. Definiere dann zwei Unterklassen "Car" und "Bicycle", welche beide von Fahrzeug erben. * Verwende das Prinzip "overriding" oder "overloading" in deinem Beispiel Car+Bicycle === 2.3 Ein Processing-Beispiel: Autoklasse=== def setup(): global car1 size(500,500) background(255) # Hintergrundfarbe (weiss) car1 = Car(200,200, 60, 100, 45, [250,0,0]) # car2 = Car(20,100, 0, [250,200,0]) car1.drawCar() # car2.drawCar() def drawBackground(): background(255) # Hintergrundfarbe (weiss) stroke(200) # Linienfarbe (grau) for i in range(0,500,20): line(0,i,500,i) line(i,0,i,500) def draw(): clear() drawBackground() car1.drive(4) car1.drawCar() class Car(object): def __init__(self, xpos, ypos, car_width, car_length, direction, color1): self.x = xpos self.y = ypos self.dir = radians(direction) self.col = color1 # list of RGB-Values self.w = car_width self.l = car_length def drawCar(self): v1 = PVector.fromAngle(self.dir).div(2) v2 = v1.copy() v2.rotate(3.14159265/2) vm = PVector(self.x, self.y) fill(self.col[0],self.col[1],self.col[2]) beginShape() vertex(vm.x-self.l*v1.x-self.w*v2.x,vm.y-self.l*v1.y-self.w*v2.y) vertex(vm.x-self.l*v1.x+self.w*v2.x,vm.y-self.l*v1.y+self.w*v2.y) vertex(vm.x+self.l*v1.x+self.w*v2.x,vm.y+self.l*v1.y+self.w*v2.y) vertex(vm.x+self.l*v1.x-self.w*v2.x,vm.y+self.l*v1.y-self.w*v2.y) endShape() def drive(self, dist): v1 = PVector.fromAngle(self.dir)*dist self.x += v1.x self.y += v1.y def turn(self, angle): self.dir += radians(angle) [[https://www.python-kurs.eu/python_OOP.php| Objektorientierte Programmierung auf python-kurs.eu]] [[https://www.youtube.com/watch?v=JeznW_7DlB0|OOP Tech with Tim]]