EINFÜHRUNG |
Du hast bereits Bekanntschaft mit wichtigen Konzepten der Objektorientierten Programmierung gemacht und gemerkt, dass du ohne OOP in Python kaum Computergames schreiben kannst. Es ist daher wichtig, dass du etwas systematischer die Begriffe der OOP und ihre Implementierung in Python kennen lernst [mehr...
In Python werden Daten, also auch Zahlen, ausschliesslich als Objekte behandelt, auch wenn du dir dessen nicht immer bewusst bist]
| .
INSTANZVARIABLEN |
Tiere eignen sich hervorragend, also Objekte modelliert zu werden. Du definierst zuerst eine Klasse Animal, die das entsprechende Tierbild im Hintergrund des Gameboard darstellt. Bei der Erzeugung eines Objekts dieser Klasse übergibst du daher dem Konstruktor den Dateipfad auf das Tierbild, damit die Methode showMe() soll das Bild anzeigen kann. Sie verwendet dabei Zeichnungsmethoden der Klasse GGBackground. Der Konstruktor, der den Dateipfad erhält, muss ihn als Initialisierungswert in einer Variablen speichern, damit alle Methoden darauf zugreifen können. Eine solche Variable ist ein Attribut oder eine Instanzvariable der Klasse. In Python erhalten Instanzvariablen den Prefix self und werden bei der ersten Zuweisung eines Werts erzeugt. Wie du bereits weist, hat der Konstruktor den speziellen Namen __init__ (mit zwei vor- und nachgestellten Underlines). Der Konstruktor sowie alle Methoden müssen self als ersten Parameter aufweisen, was man gerne vergisst.
from gamegrid import * # ---------------- class Animal ---------------- class Animal(): def __init__(self, imgPath): self.imagePath = imgPath # Instance variable def showMe(self, x, y): # Method definition bg.drawImage(self.imagePath, x, y) def pressCallback(e): myAnimal = Animal("sprites/animal.gif") # Object creation myAnimal.showMe(e.getX(), e.getY()) # Method call makeGameGrid(600, 600, 1, False, mousePressed = pressCallback) setBgColor(Color.green) show() doRun() bg = getBg()
|
MEMO |
Die Eigenschaften oder Attribute eines Objekts werden als Instanzvariablen definiert. Sie haben für jedes Objekt der Klasse individuelle Werte. Der Zugriff auf Instanzvariablen innerhalb der Klasse erfolgt durch Vorstellen von self. Von ausserhalb der Klasse kann man durch Voranstellen des Instansnamens darauf zugreifen, wie z. B. myInstance.attribut. Einer Klasse stehen aber auch die Variablen und Funktionen des Programm-Hauptteils zur Verfügung, beispielsweise alle Methoden der Klasse GameGrid und mit bg der Background des Spielfensters. Die Methoden können sogar eine Variable des Hauptteils verändern, falls sie in der Methode als global deklariert wird. Wenn das Objekt keine Initialisierungen benötigt, so kann die Definition des Konstruktors auch weggelassen werden. Statt das Spritebild dem Konstruktor zu übergeben, verwendest du im folgenden Programm die Variable imagePath und kannst damit auf den Konstruktor verzichten. from gamegrid import * import random # ---------------- class Animal ---------------- class Animal(): def showMe(self, x, y): bg.drawImage(imagePath, x, y) def pressCallback(e): myAnimal = Animal() myAnimal.showMe(e.getX(), e.getY()) imagePath = "sprites/animal.gif" makeGameGrid(600, 600, 1, False, mousePressed = pressCallback) setBgColor(Color.green) show() doRun() bg = getBg() |
VERERBUNG, METHODEN HINZUFÜGEN |
Durch Klassenableitung oder Vererbung erstellst du eine Klassenhierarchie und kannst damit einer bestehenden Klasse zusätzliche Eigenschaften und Verhalten hinzufügen. Objekte der abgeleiteten Klasse sind automatisch auch Objekte der übergeordneten Klasse (auch Ober-, Basis- oder Superklasse genannt) und können daher
from gamegrid import * from java.awt import Point # ---------------- class Animal ---------------- class Animal(): def __init__(self, imgPath): self.imagePath = imgPath def showMe(self, x, y): bg.drawImage(self.imagePath, x, y) # ---------------- class Pet ---------------- class Pet(Animal): # Derived from Animal def __init__(self, imgPath, name): self.imagePath = imgPath self.name = name def tell(self, x, y): # Additional method bg.drawText(self.name, Point(x, y)) makeGameGrid(600, 600, 1, False) setBgColor(Color.green) show() doRun() bg = getBg() bg.setPaintColor(Color.black) for i in range(5): myPet = Pet("sprites/pet.gif", "Trixi") myPet.showMe(50 + 100 * i, 100) myPet.tell(72 + 100 * i, 145)
|
MEMO |
Wie du siehst, kannst du myPet.showMe() aufrufen, obschon showMe() in der Klasse Pet gar nicht definiert ist, denn ein Haustier ist-auch-ein Tier. Man nennt die Beziehung von Pet und Animal daher eine is-a-Relation. Da die Instanzvariable imagePath im Konstruktor von Animal definiert ist, kannst du die Zeile self.imagePath = imgPath im Konstruktor von Pet durch Animal.__init__(self, imgPath) ersetzen, wodurch die Basisklasse Animal initialisiert wird. Die Basisklassen werden bei den abgeleiteten Klassen in eine Klammer hinter den Klassennamen gesetzt. In Python kann man eine Klasse auch aus mehreren Basisklassen ableiteten (Multiple Inheritance). |
KLASSENHIERARCHIE, METHODEN ÜBERSCHREIBEN |
from gamegrid import * # ---------------- class Animal ---------------- class Animal(): def __init__(self, imgPath): self.imagePath = imgPath def showMe(self, x, y): bg.drawImage(self.imagePath, x, y) # ---------------- class Pet ---------------- class Pet(Animal): def __init__(self, imgPath, name): Animal.__init__(self, imgPath) self.name = name def tell(self, x, y): bg.drawText(self.name, Point(x, y)) # ---------------- class Dog ---------------- class Dog(Pet): def __init__(self, imgPath, name): Pet.__init__(self, imgPath, name) def tell(self, x, y): # Overriding bg.setPaintColor(Color.blue) bg.drawText(self.name + " tells 'Waoh'", Point(x, y)) # ---------------- class Cat ---------------- class Cat(Pet): def __init__(self, imgPath, name): Pet.__init__(self, imgPath, name) def tell(self, x, y): # Overriding bg.setPaintColor(Color.gray) bg.drawText(self.name + " tells 'Meow'", Point(x, y)) makeGameGrid(600, 600, 1, False) setBgColor(Color.green) show() doRun() bg = getBg() alex = Dog("sprites/dog.gif", "Alex") alex.showMe(100, 100) alex.tell(200, 130) # Overriden method is called rex = Dog("sprites/dog.gif", "Rex") rex.showMe(100, 300) rex.tell(200, 330) # Overriden method is called xara = Cat("sprites/cat.gif", "Xara") xara.showMe(100, 500) xara.tell(200, 530) # Overriden method is called
|
MEMO |
Durch das Überschreiben von Methoden kann man in der abgeleiteten Klassen das Verhalten der Basisklasse verändern. Beim Aufruf von Methoden derselben Klasse oder der Basisklasse muss self vorgestellt werden. In der Parameterliste wird self aber nicht übergeben. Manchmal möchte man in einer überschriebenen Methode die gleichlautende Methode der Basisklasse verwenden. Um diese aufzurufen, muss man den Klassennamen der Basisklasse voranstellen und in der Parameterliste self ebenfalls übergeben [mehr... Vergisst man diese Regel, ergäbe sich ein rekursiver Aufruf]. Diese Regel gilt auch für den Konstruktor: Wird im Konstruktor der abgeleiteten Klasse der Konstruktor der Basisklasse verwendet, so muss dieser durch Vorstellen des Klassennamens der Basisklasse und mit dem Parameter self aufgerufen werden. Beispielsweise: class BaseClass: def __init__(self, a): self.a = a class ChildClass(BaseClass): def __init__(self, a, b): # die Initialisierung der Instanzvariablen 'a' # erfolgt durch den Konstruktor der Basisklasse BaseClass.__init__(self, a) self.b = b |
TYPENBEZOGENER METHODENAUFRUF: POLYMORPHISMUS |
Eine etwas schwieriger zu verstehende, aber besonders wichtige Eigenschaft von objektorientierten Programmiersprachen ist der Polymorphismus Darunter versteht man den Aufruf von überschriebenen Methoden, wobei der Aufruf automatisch der Klassenzugehörigkeit angepasst wird. An einem einfachen Beispiel erlebst du, was damit gemeint ist. Du verwendest mit den vorher definierten Klassen eine Liste animals animals = [Dog(), Dog(), Cat()] in der sich zwei Hunde und eine Katze befinden. Beim Durchlaufen der Liste und Aufruf von tell() mit
from gamegrid import * from soundsystem import * # ---------------- class Animal ---------------- class Animal(): def __init__(self, imgPath): self.imagePath = imgPath def showMe(self, x, y): bg.drawImage(self.imagePath, x, y) # ---------------- class Pet ---------------- class Pet(Animal): def __init__(self, imgPath, name): Animal.__init__(self, imgPath) self.name = name def tell(self, x, y): bg.drawText(self.name, Point(x, y)) # ---------------- class Dog ---------------- class Dog(Pet): def __init__(self, imgPath, name): Pet.__init__(self, imgPath, name) def tell(self, x, y): # Overridden Pet.tell(self, x, y) openSoundPlayer("wav/dog.wav") play() # ---------------- class Cat ---------------- class Cat(Pet): def __init__(self, imgPath, name): Pet.__init__(self, imgPath, name) def tell(self, x, y): # Overridden Pet.tell(self, x, y) openSoundPlayer("wav/cat.wav") play() makeGameGrid(600, 600, 1, False) setBgColor(Color.green) show() doRun() bg = getBg() animals = [Dog("sprites/dog.gif", "Alex"), Dog("sprites/dog.gif", "Rex"), Cat("sprites/cat.gif", "Xara")] y = 100 for animal in animals: animal.showMe(100, y) animal.tell(200, y + 30) # Which tell()???? show() y = y + 200 delay(1000)
|
MEMO |
Der Polymorphismus sorgt dafür, dass bei überschriebenen Methoden die Klassenzughörigkeit entscheidet, welche Methode aufgerufen wird. Da in Python die Zugehörigkeit zu Klassen sowieso erst zu Laufzeit festgelegt wird, ist der Polymorphismus eine Selbstverständlichkeit. Diese dynamische Datenbindung von Python nennt sich auch Ententest oder Duck-Typing, gemäss dem Zitat, das James Whitcomb Riley (1849 - 1916) zugeschrieben wird: "Wenn ich einen Vogel sehe, der wie eine Ente läuft, wie eine Ente schwimmt und wie eine Ente schnattert, dann nenne ich diesen Vogel eine Ente." Es gibt Fälle, wo eine überschriebene Methode in der Basisklasse zwar definiert ist, aber nichts bewirken soll. Dies erreicht man entweder mit einem sofortigen return oder mit der leeren Anweisung pass. |
AUFGABEN |
|
ZUSATZSTOFF |
STATISCHE VARIABLEN UND STATISCHE METHODEN |
Klassen können auch dazu verwendet werden, zusammengehörende Variablen oder Funktionen zu gruppieren und damit den Code übersichtlicher zu machen. Beispielsweise kannst du die wichtigsten physikalischen Konstanten in der Klasse Physics zusammenfassen. Man nennt Variablen, die im Klassenkopf definiert werden, statische Variablen und ruft sie durch Vorstellen des Klassennamens auf. Es ist also im Gegensatz zu Instanzvariablen nicht nötig, eine Instanz der Klasse zu erzeugen. import math # ---------------- class Physics ---------------- class Physics(): # Avagadro constant [mol-1] N_AVAGADRO = 6.0221419947e23 # Boltzmann constant [J K-1] K_BOLTZMANN = 1.380650324e-23 # Planck constant [J s] H_PLANCK = 6.6260687652e-34; # Speed of light in vacuo [m s-1] C_LIGHT = 2.99792458e8 # Molar gas constant [K-1 mol-1] R_GAS = 8.31447215 # Faraday constant [C mol-1] F_FARADAY = 9.6485341539e4; # Absolute zero [Celsius] T_ABS = -273.15 # Charge on the electron [C] Q_ELECTRON = -1.60217646263e-19 # Electrical permittivity of free space [F m-1] EPSILON_0 = 8.854187817e-12 # Magnetic permeability of free space [ 4p10-7 H m-1 (N A-2)] MU_0 = math.pi*4.0e-7 c = 1 / math.sqrt(Physics.EPSILON_0 * Physics.MU_0) print("Speed of light (calulated): %s m/s" %c) print("Speed of light (table): %s m/s" %Physics.C_LIGHT) Eine Sammlung von zusammengehörenden Funktionen kannst du ebenfalls gruppieren, indem du sie als statische Methoden in einer aussagekräftig bezeichneten Klasse definierst. Diese Methoden kannst du dann durch Vorstellen des Klassennamens direkt verwenden, ohne dass du eine Instanz der Klasse erstellen musst. Um eine Methode statisch zu machen, muss man vor die Definition @staticmethod schreiben. # ---------------- class OhmsLaw ---------------- class OhmsLaw(): @staticmethod def U(R, I): return R * I @staticmethod def I(U, R): return U / R @staticmethod def R(U, I): return U / I r = 10 i = 1.5 u = OhmsLaw.U(r, i) print("Voltage = %s V" %u) |
MEMO |
Statische Variable (im Unterschied zu Instanzvariablen auch Klassenvariablen genannt) gehören zu der Klasse als Ganzes und haben im Gegensatz zu Instanzvariablen für alle Objekte der Klasse den gleichen Wert. Sie können mit vorgestelltem Klassennamen gelesen und verändert werden. Eine typische Anwendung von statischen Variablen ist ein Instanzenzähler, also eine Variable, welche die Anzahl erzeugter Objekte der betreffenden Klasse zählt. Zusammengehörende Funktionen können als statische Methoden einer sinnvoll bezeichneten Klasse gruppiert werden. Bei der Definition muss die Zeile @staticmethod (function decorator genannt) vorgestellt werden |