zurück rauf weiter Englisch Index

Kapitel 16

Rohübersetzung -- bitte um Rückmeldungen über Fehler und Unklarheiten an Gregor Lingl

Vererbung

16.1 Vererbung

Die Spracheigenschaft, die am häufigsten mit objektorientierter Programmierung in Verbindung gebracht wird, ist Vererbung. In Sprachen mit Vererbung besteht die Möglichkeit, eine neue Klasse zu definieren, die eine abgeänderte Version einer bereits existierenden Klasse ist.

Der wichtigste Vorteil von Vererbung ist die Möglichkeit, einer Klasse neue Methoden hinzuzufügen, ohne die existierende Klasse zu verändern. Die Bezeichnung "Vererbung" drückt aus, dass die neue Klasse alle Methoden der existierenden Klasse erbt. In Erweiterterung dieser Methapher, nennt man die existierende Klasse die Eltern-Klasse. Die neue Klasse wird manchmal Kind-Klasse genannt oder auch "Unterklasse".

Vererbung ist eine mächtige Spracheigenschaft. Manche Programme, die ohne Vererbung kompliziert wären, können mit ihr einfach und knapp geschrieben werden. Außerdem kann Vererbung die Wiederverwendbarkeit von Code unterstützen, da man das Verhalten von Eltern-Klassen an neue Anforderungen anpassen kann, ohne sie verändern zu müssen. In manchen Fällen spiegelt die Vererbungsstruktur die natürliche Struktur des Problems wider, sodass das Programm leichter verständlich wird.

Auf der anderen Seite kann ein Programm durch Vererbung auch schwerer lesbar werden. Wenn eine Methode aufgerufen wird, ist es manchmal nicht klar, wo ihre Definition steht. Der relevante Code kann sogar über mehrere Module verstreut sein. Außerdem können viele Dinge, die man mit Vererbung machen kann, ebenso elegant (oder sogar eleganter) ohne sie gemacht werden. Wenn die natürliche Struktur eines Problems nicht die Verwendung von Vererbung nahe legt, kann es sein, dass dieser Programmierstil mehr Schaden anrichtet als Nutzen bringt.

In diesem Kapitel werden wir vorführen, wie Vererbung in einem Programm genutzt werden kann, das das Kartenspiel "Old Maid" simuliert. Eines unserer Ziele wird dabei sein, Code zu schreiben, der weiter verwendet werden könnte, um andere Kartenspiele zu implementieren.

16.2 Ein Blatt

Für nahezu jedes Kartenspiel benötigen wir eine Darstellung eines sogenannten Blatts, also jener Zusammenstellung von Karten, die sich in der Hand eines Spieler befinden. Ein Blatt ist natürlich einem Paket ähnlich. Beide bestehen aus Karten, beide benötigen Operationen wie beispielsweise Entfernen von Karten. Vielleicht ist es auch vorteilhaft ein Paket wie auch ein Blatt mischen zu können.

Ein Blatt ist aber auch verschieden von einem Paket. Je nach dem, welches Spiel gespielt wird, möchten wir mit einem Blatt bestimmte Operationen vornehmen, die für ein Paket keinen Sinn haben. Für das Poker-Spiel möchten wir zum Beispiel unser Blatt klassifizieren (Straße, Flush, usw.) oder es mit einem anderen Blatt vergleichen. In Bridge möchten wir vielleicht die Punktezahl für ein Blatt berechnen um eine Ansage zu machen.

Diese Situation legt die Verwendung von Vererbung nahe. Wenn die Klasse Blatt als Unterklasse von Paket konzipiert ist, wird sie alle Methoden von Paket erben und wir können dann noch neue Methoden hinzufügen.

In der Klassendefinition folgt auf den Namen der Klasse der Name der Erltern-Klasse, eingeschlossen in runde Klammern:

class Blatt(Paket):
  pass

Diese Anweisung zeigt an, dass die neue Blatt Klasse von der existierenden Paket Klasse erbt.

Der Blatt Konstruktor initialisiert die Attribute für das Blatt, nämlich name und karten. Der String name bezeichnet das Blatt eindeutig, vielleicht durch den Namen des Spielers, der das Blatt hält. Der Name ist ein Parameter mit dem Leerstring als Standardwert und also mit optionalem Argument. karten ist die Liste von Karten, aus denen das Blatt besteht, initialisiert mit der leeren Liste.

class Blatt(Paket):
  def __init__(self, name=""):
    self.karten = []
    self.name = name

Für fast alle Kartenspiele ist es nötig, Karten vom Blatt zu entfernen und zum Blatt hinzuzufügen. Für die Möglichkeit des Entfernens haben wir schon gesorgt, denn Blatt erbt entferneKarte von Paket. Aber gibKarteDazu müssen wir noch schreiben:

class Blatt(Paket):
  ...
  def gibKarteDazu(self,karte) :
    self.karten.append(karte)

Wieder bedeuten die Punkte, dass wir andere Methode weggelassen haben. Die Listen-Methode append fügt die neue Karte an das Ende der Kartenliste an.

16.3 Die Karten Geben

Jetzt, da wir die Blatt Klasse haben, möchten wir die Karten des Pakets an die Spieler austeilen, sodass jeder Spieler sein Blatt bekommt. Es ist nicht unmittelbar klar, ob diese Methode geben in die Klasse Blatt oder in die Klasse Paket gehört. Aber weil die Karten eines Pakets gegeben werden und zwar im Allgemeinen an an mehrere Blatt Objekte, ist es wohl natürlicher, sie in die Klasse Paket zu schreiben.

geben sollte einigermaßen allgemein sein, denn verschiedene Spiele werden daran verschiedene Anforderungen haben. Vielleicht müssen wir das ganze Paket auf einmal ausgeben, oder wir wollen nur eine Karte an jedes Blatt geben, und so weiter.

geben hat zwei Parameter. Einen für eine Blatt-Liste (oder ein Blatt-Tupel), den anderen für die Gesamtanzahl an Karten, die zu geben sind. Wenn nicht genügend Karten im Paket sind, gibt die Methode alle vorhandenen Karten und stoppt dann.

class Paket :
  ...
  def geben(self, blattListe, nKarten=999):
    nblatt = len(blattListe)
    for i in range(nKarten):
      if self.istLeer(): break      # break wenn keine Karten mehr da sind
      karte = self.popKarte()       # nimm die "oberste" Karte
      blatt = blattListe[i % nblatt]    # wer ist der nächste?
      blatt.gibKarteDazu(karte)         # gib die Karte zu Blatt

Das zweite Argument, nKarten, ist optional; der Standardwert ist eine große Zahl, was im Endeffekt bedeutet, dass alle Karten des Pakets gegeben werden.

Die Schleifenvariable i läuft von 0 bis nKarten-1. Bei jedem Schleifendurchgang wird dem Paket eine Karte mittels der Methode popKarte entnommen und der Variablen karte zugewiesen.

Der Modulo-Operator (%) gestattet uns Karten im Kreis zu verteilen (immer eine Karte auf einmal für jedes Blatt). Wenn i so groß wie die Anzahl der Spieler (oder ein Vielfaches der Anzahl der Spieler), also der Elemente der Blatt-Liste ist, dann setzt das Geben beim Beginn der Blatt-Liste fort, weil der Index i % nblatt dann gleich 0 ist.

16.4 Ausgabe eines Blatts

Um den Inhalt eines Blatts auszugeben, können wir vorteilhafterweise die von Paket geerbten Methoden printDeck und __str__ verwenden. Zum Beispiel so:

>>> paket = Paket()
>>> paket.mischen()
>>> blatt = Blatt("Franz")
>>> paket.geben([blatt], 5)
>>> print blatt
Das Blatt von Franz enthält
Herz 2
Herz 3
  Herz 4
   Karo As
    Treff 9

Das ist kein großartiges Blatt, aber es könnte sich auf einen straight flush ausgehen.

Obwohl es vorteilhaft ist, die existierenden Methoden zu erben, ist doch noch eine zusätzliche Information in einem Blatt Objekt, die wir bei der Ausgabe eines Blatts mit ausgeben wollen. Um das zu erreichen, können wir auch für die Klasse Blatt eine __str__ Methode bereit stellen, die die entsprechende Methode der Paket-Klasse überschreibt.

class Blatt(Paket)
  ...
  def __str__(self):
    s = "Das Blatt von " + self.name + ":"
    if self.istLeer():
      s = s + " ist leer.\n"
    else:
      s = s + ":\n"
    return s + Paket.__str__(self)

Zunächst ist s ein String der den Namen des Blatts (oder seines Besitzers) enthält. Wenn das Blatt leer ist, hängt das Programm die Worte ist leer an und gibt s zurück.

Andernfalls hängt das Programm einen Doppelpunkt an und dann eine Stringdarstellung, die berechnet wird, indem die __str__ Methode der Paket Klasse mit dem Argument self, das ist eben das Blatt, aufgerufen wird.

Es mag zunächst sonderbar erscheinen, self, das auf das aktuelle Blatt verweist, einer Paket Methode als Argument zu übergeben, aber nur bis man sich daran erinnert, dass ein Blatt eine Art von Paket ist. Blatt-Objekte können alles, was Paket-Objekte können. Daher ist es ganz legal ein Blatt als erstes Argument für eine Paket - Methode zu verwenden.

Allgemein gesprochen ist es stets legal, die Instanz einer Unterklasse an der Stelle einer Instanz der Eltern-Klasse zu verwenden.

16.5 Die KartenSpiel Klasse

Die KartenSpiel Klasse sorgt für einige grundlegende Aufgaben, die allen Spielen gemeinsam sind, wie zum Beispiel das Erzeugen und das Mischen des Kartenpakets:

class KartenSpiel:
  def __init__(self):
    self.paket = Paket()
    self.paket.mischen()

Hier ist es erstmals der Fall, dass die Initialisierungsmethode nennenswerte Berechnungen ausführt und nicht nur Attribute mit Werten initialisiert.

Um spezielle Kartenspiele zu implementieren, können wir von KartenSpiel erben und spezielle Züge des neuen Spiels hinzufügen. Als Beispiel werden wir eine Simulation des Kartenspiels "Old Maid" programmieren.

Beim Kartenspiel "Old Maid" ist es das Ziel der Spielerin, die Karten, die in ihrem Blatt sind, los zu werden. Sie tut dies, indem sie entsprechend Rang und Farbe aus den Karten "passende Paare" bildet und ablegt. Zum Beispiel passt Treff 4 zu Pik 4, weil beide Farben schwarz sind. Der Herz Bube passt zum Karo Buben, weil beide rot sind.

Beim Beginn des Spiels wird die Treff Dame aus dem Paket entfernt, sodass die Pik Dame keine passende Partnerkarte hat. Die einundfünfzig verbleibenden Karten werden an die Spieler im Kreis herum verteilt. Nach dem Geben bilden alle Spieler so viele passende Paare, wie möglich und legen diese ab.

Wenn keine weiteren Paare mehr gebildet werden können, beginnt das Spiel. Der Reihe nach zieht jeder Spieler eine Karte (zufällig, ohne sie anzusehen) aus dem Blatt seines nächsten linken Nachbarn, der noch Karten hat. Wenn die gezogene Karte mit einer Karte seines Blatts ein passendes Paar bildet, wird dieses Paar abgelegt. Am Ende des Spiels sind alle möglichen passenden Paare abgelegt und es verbleibt nur mehr die Pik Dame in der Hand des Verlierers.

In unserer Computer-Simulation des "Old Maid" Spieles, wird jedes Blatt vom Computer gespielt. Leider gehen dabei einige wichtige Eigenheiten eines realen Spieles verloren. In einem Spiel mit richtigen Karten wird der Spieler mit der "Old Maid" - der Pik Dame - einige Anstrengungen unternehmen, damit seine rechte Nachbarin diese Karte zieht. Vielleicht wird er diese Karte aus seinem Blatt ein bißchen deutlicher in den Vordergrund schieben, oder vielleicht auch das gerade Gegenteil davon, oder sogar dasselbe doppelt gemoppelt versuchen.

Der Computer zieht dagegen eine Karte des Nachbarn einfach mittels Zufallsgenerator.

16.6 Die OldMaidBlatt Klasse

Ein Blatt, mit dem man "Old Maid" spielt, braucht einige Fähigkeiten, die über die allgemeinen Fähigkeiten eines Blattes hinausgehen. Daher werden wir eine neue Klasse OldMaidBlatt definieren, die eine Unterklasse von Blatt ist und eine zusätzliche Methode entfernePaare hat:

class OldMaidBlatt(Blatt):
  def entfernePaare(self):
    partnerFarbe = {0:1, 1:0, 2:3, 3:2}
    anzahl = 0
    blattKarten = self.karten[:]
    for karte in blattKarten:
      partner = Karte(partnerFarbe[karte.farbe], karte.rang)
      if partner in self.karten:
        self.karten.remove(karte)
        self.karten.remove(partner)
        print "Im Blatt %s ist das Paar %s, %s" % (self.name,karte,partner)
        anzahl = anzahl + 1
    return anzahl

Wir beginnen damit, in einem Dictionary festzulegen welche Farbe zu welcher anderen Farbe passt: Treff zu Pik (0 zu 1), Pik zu Treff (1 zu 0), und so weiter. Eine Zählvariable, die die Anzahl der Paare im Blatt angeben soll, setzen wir auf 0.

Dann erzeugen wir eine Kopie der Kartenliste. Wir wollen die Karten dieser Kopie in einer Schleife durchlaufen, um jeweils passende Paare aus der originalen Kartenliste des Blatts, self.karten zu entfernen. Dabei wird diese natürlich geändert. Daher wollen wir nicht sie (sondern ihre Kopie) für die Kontrolle der Schleifendurchläufe verwenden. Python kann ziemlich durcheinander kommen, wenn es eine Liste durchläuft, die geändert wird.

Für jede Karte im Blatt erzeugen wir die passende Partnerkarte. Diese hat denselben Rang und Karten eines Paares müssen entweder beide schwarz oder beide rot sein. Die Partnerfarbe liefert uns das Dictionary partnerFarbe. Wenn die Partnerkarte auch in dem Blatt ist, werden beide Karten der Paares entfernt.

Das folgende Beispiel zeigt, wie entfernePaare verwendet wird:

>>> spiel = KartenSpiel()
>>> blatt = OldMaidBlatt("Franz")
>>> spiel.paket.geben([blatt], 13)
>>> print blatt
Das Blatt von Franz:
Pik 10
Pik 6
  Karo König
   Pik 3
    Treff 5
     Pik Bube
      Karo Bube
       Treff 3
        Treff 10
         Karo 6
          Treff Dame
           Herz König
            Treff 8

>>> blatt.entfernePaare()
Im Blatt Franz ist das Paar Pik 10, Treff 10
Im Blatt Franz ist das Paar Karo König, Herz König
Im Blatt Franz ist das Paar Pik 3, Treff 3
3
>>> print blatt
Das Blatt von Franz:
Pik 6
Treff 5
  Pik Bube
   Karo Bube
    Karo 6
     Treff Dame
      Treff 8

Beachte, dass in der OldMaidBlatt Klasse keine __init__ Methode definiert wird. Wir erben sie von Blatt.

16.7 Die OldMaidSpiel Klasse

Nun können wir unsere Aufmerksamkeit auf das Spiel selbst lenken. OldMaidSpiel ist eine Unterklasse von KartenSpiel mit einer neuen Methode, genannt spielen, mit einem Parameter für den beim Aufruf als Argument eine Liste von Spielern eingesetzt wird.

Weil __init__ von KartenSpiel geerbt wird, enthält ein neues OldMaidSpiel Objekt ein neues gemischtes Paket:

class OldMaidSpiel(KartenSpiel):
  def spielen(self, namen):
    # entferne die Treff Dame
    self.paket.entferneKarte(Karte(0,12))

    # erzeuge fuer jeden Spieler ein Blatt
    self.blattListe = []
    for name in namen :
      self.blattListe.append(OldMaidBlatt(name))

    # die Karten geben
    self.paket.geben(self.blattListe)
    print "---------- Die Karten sind gegeben!"
    self.printBlattListe()

    # entferne anfangs vorhandene Paare
    paare = self.entferneAllePaare()
    print "---------- Paare abgelegt - das Spiel beginnt!"
    self.printBlattListe()

    # Spiel läuft bis alle 25 Paare abgelegt sind
    amZug = 0
    numBlattListe = len(self.blattListe)
    while matches < 25:
      paare = paare + self.einSpielerSpielt(amZug)
      amZug = (amZug + 1) % numBlattListe

    print "---------- Game is Over"
    self.printBlattListe()

Einige der Schritte, die in diesem Spiel vorkommen, sind dabei in eigene Methoden ausgelagert worde. entferneAllePaare durchläuft die Blattliste und ruft entfernePaare für jedes Blatt auf:

class OldMaidSpiel(KartenSpiel):
  ...
  def entferneAllePaare(self):
    anzahl = 0
    for blatt in self.blattListe:
      anzahl = anzahl + blatt.entfernePaare()
    return anzahl

anzahl ist eine Akkumulator-Variable, die die Anzahlen der in jedem Blatt auftretenden Paare aufsummiert. Am Ende wird diese Summe zurückgegeben.

Übung: schreibe printBlattListe als eine Methode, die self.blattListe durchläuft und jedes Blatt ausgibt.

Wenn die Gesamtzahl der Paare fünfundzwanzig erreicht hat, dann sind fünfzig Karten von den Spielern abgelegt worden. Das heißt, es ist nur mehr eine Karte übrig und das Spiel ist beendet.

Die Variable amZug enthält jeweils die Nummer des Spielers, der gerade am Zug ist. Das Spiel beginnt mit dem Spieler 0 und bei jedem Zug wird diese Variable um eins erhöht. Wenn der Wert von amZug numBlattListe erreicht, sorgt der Modulo-Operator dafür, dass er auf Null zurückgesetzt wird.

Die Methode einSpielerSpielt hat einen Parameter. Für ihn wird die Nummer des Spielers, der an der Reihe ist als Argument eingesetzt. Der Rückgabewert ist die Anzahl der Paare, die von diesem Spieler bei diesem Spielzug abgelegt werden.

class OldMaidSpiel(KartenSpiel):
  ...
  def einSpielerSpielt(self, i):
    if self.blattListe[i].istLeer():
      return 0
    nachbar = self.findeNachbarn(i)
    gezogeneKarte = self.blattListe[nachbar].popKarte()
    self.blattListe[i].gibKarteDazu(gezogeneKarte)
    print "Blatt", self.blattListe[i].name, "hat", gezogeneKarte, "gezogen."
    anzahl = self.blattListe[i].entfernePaare()
    self.blattListe[i].mischen()
    return anzahl

Wenn das Blatt eines Spielers oder einer Spielerin leer ist, dann ist er oder sie aus dem Spiel ausgeschieden, daher tut er oder sie gar nichts mehr und gibt 0 zurück.

Andernfalls muss er den/die erste SpielerIn links von ihm finden, der oder die Karten hat, ein Karte aus seinem/ihrem Blatt ziehen und prüfen ob ein neues Paar vorliegt. Bevor sein Nachbar das Spiel fortsetzt, mischt er oder sie sein Blatt, damit die Wahl des nächsten Spielers zufällig ist.

Die Methode findeNachbarn beginnt die Suche mit dem Spieler umittelbar links und fährt im Kreis fort, bis sie einen Spieler findet, der Karten hat:

class OldMaidSpiel(KartenSpiel):
  ...
  def findeNachbarn(self, i):
    numBlattListe = len(self.blattListe)
    for naechste in range(1,numBlattListe):
      nachbar = (i - naechste) % numBlattListe   ## original: i + naechste
      if not self.BlattListe[nachbar].istLeer():
        return nachbar

Würde die Funktion findeNachbarn jemals den ganzen Spielerkreis absuchen, ohne Karten zu finden, so würde sie None zurückgeben und einen Fehler irgendwo im Programmablauf verursachen. Glücklicherweise können wir zeigen, dass das nicht geschehen kann (so lange das Programmende korrekt ermittelt wird.)

Wir haben die printBlattListe Methode weggelassen. Die kannst du dir selber schreiben.

Die folgende Ausgabe ist von einer gekappten Form des Spiels, wobei nur die obersten fünfzehn Karten (Zehner und höhere Karten) an die Spieler ausgegeben werden. Mit diesem kleinen Paket hört das Spiel bereits auf, wenn 7 Paare gefunden wurden (an Stelle von fünfundzwanzig).

>>> import karten
>>> spiel = karten.OldMaidSpiel()
>>> spiel.spielen(["Anton", "Franz", "Karl"])
---------- Die Karten sind gegeben!
Das Blatt von Anton:
Pik 10
Treff 10
  Herz Bube
   Karo 10
    Herz König

Das Blatt von Franz:
Pik König
Karo Dame
  Treff Bube
   Karo Bube
    Treff König

Das Blatt von Karl:
Pik Bube
Herz Dame
  Herz 10
   Pik Dame
    Karo König

Im Blatt Anton ist das Paar Pik 10, Treff 10
Im Blatt Franz ist das Paar Pik König, Treff König
---------- Paare abgelegt - das Spiel beginnt!
Das Blatt von Anton:
Herz Bube
Karo 10
  Herz König

Das Blatt von Franz:
Karo Dame
Treff Bube
  Karo Bube

Das Blatt von Karl:
Pik Bube
Herz Dame
  Herz 10
   Pik Dame
    Karo König

Blatt Anton hat Karo König gezogen.
Im Blatt Anton ist das Paar Herz König, Karo König
Blatt Franz hat Karo 10 gezogen.
Blatt Karl hat Karo Bube gezogen.
Blatt Anton hat Pik Dame gezogen.
Blatt Franz hat Pik Dame gezogen.
Blatt Karl hat Karo 10 gezogen.
Im Blatt Karl ist das Paar Herz 10, Karo 10
Blatt Anton hat Pik Bube gezogen.
Blatt Franz hat Pik Bube gezogen.
Im Blatt Franz ist das Paar Treff Bube, Pik Bube
Blatt Karl hat Pik Dame gezogen.
Blatt Anton hat Herz Dame gezogen.
Blatt Franz hat Herz Bube gezogen.
Blatt Karl hat Herz Bube gezogen.
Im Blatt Karl ist das Paar Karo Bube, Herz Bube
Blatt Anton hat Pik Dame gezogen.
Blatt Franz hat Herz Dame gezogen.
Im Blatt Franz ist das Paar Karo Dame, Herz Dame
---------- Das Spiel ist aus!
Das Blatt von Anton:
Pik Dame

Das Blatt von Franz ist leer.

Das Blatt von Karl ist leer.


Also verlor Anton.

16.8 Glossar

Vererbung
Die Möglichkeit, eine neue Klasse zu definieren, die eine abgeänderte Version einer vorher definierten Klasse ist.
inheritance
The ability to define a new class that is a modified version of a previously defined class.
Eltern-Klasse
Die Klasse, von der eine Kind-Klasse (Unterklasse) erbt.
parent class
The class from which a child class inherits.
Kind-Klasse
Eine neue Klasse, die dadurch erzeugt wird, dass sie von einer existierenden Klasse erbt. Wird auch "Unterllasse" genannt.
child class
A new class created by inheriting from an existing class; also called a "subclass."


zurück rauf weiter Englisch Index