zurück rauf weiter Englisch Index

Kapitel 15

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

Mengen von Objekten

15.1 Komposition

Bis hierher hast du schon etliche Beispiele für Komposition gesehen. Eines der ersten war die Verwendung eines Methodenaufrufs als Teil eines Ausdrucks. Ein weiteres ist die geschachtelte Struktur von Anweisungen; man kann eine if Anweisung innerhalb einer while Schleife schreiben, innerhalb einer anderen if Anweisung, und so weiter.

Nachdem du dieses Muster kennst und auch schon einiges über Listen und Objekte weißt, wirst du nicht überrascht sein, zu erfahren, dass du auch Listen von Objekten erzeugen kannst. Oder auch Objekte, die Listen (als Attribute) enthalten; du kannst Listen erzeugen, die Listen enthalten und auch Objekte, die Objekte enthalten, und so weiter.

In diesem und im nächsten Kapitel werden wir uns einige Beispiele solcher Kompositionen ansehen. Zu diesem Zweck werden wir Spielkarten im Computer modellieren, also Objekte von Typ Karte erzeugen.

15.2 Spielkarten - Objekte von Typ Karte

Wenn du mit herkömmlichen Spielkarten nicht vertraut bist, ist jetzt eine gute Gelegenheit, dir ein Paket davon zu besorgen. Andernfalls könnte es sein, dass dieses Kapitel für dich nicht viel Sinn macht.

In einem Paket sind zweiundfünfzig Karten. Jede von ihnen gehört zu einer von vier Farben und jede hat einen von dreizehn Rängen. Die Farben sind Herz, Karo, Pik, Treff in (absteigender Reihenfolge). Die Ränge sind As, 2, 3, 4, 5, 6, 7, 8, 9, 10, Bube, Dame, König. Ja nach dem, welches Kartenspiel du spielst, kann der Rang des As kleiner als 2 oder auch größer als der König sein.

Wenn wir nun Objekte erzeugen wollen, die uns Spielkarten im Computer modellieren, ist klar welche Attribute wir brauchen: rang und farbe. Es ist aber keineswegs so klar, welchen Typ diese Attribute haben sollen. Eine Möglichkeit wäre, Zeichenketten zu verwenden, mit Inhalten wie "Pik" für eine Farbe oder "Dame" für einen Rang. Ein Problem mit dieser Implementation wäre aber, dass es nicht leicht wäre Karten zu vergleichen, um herauszufinden, welche einen höheren Rang oder eine höhere Farbe hat.

Eine Alternative dazu ist die Verwendung von ganzen Zahlen zur Kodierung von Rang und Farbe. Mit Kodierung meinen wir nicht, wie manche Leute glauben, die Verschlüsselung in einen Geheimcode. Informatiker verstehen unter "Kodierung" die "Definition einer Abbildung zwischen einer Folge von Zahlen und den Elementen, die sie darstellen sollen". Zum Beispiel:

Herz -> 3
Karo -> 2
Pik -> 1
Treff -> 0

Eine offensichtliche Eigenschaft dieser Abbildung ist, dass die Farben auf Zahlen so abgebildet werden, dass man den Vergleich von Farben auf den Vergleich der zugehörigen Zahlen zurückführen kann. Die Abbildung für die Ränge ergibt sich ziemlich natürlich; numerische Ränge werden auf die entprechenden ganzen Zahlen abgebildet und für die anderen Karten definieren wir:

Bube -> 11
Dame -> 12
König -> 13

Wir haben einen Grund, warum wir eine mathematische Notation für diese Abbildungen verwenden: sie sind nicht Teil des Python Programms, das wir jetzt schreiben wollen. Sie gehören zum Programmentwurf, aber sie erscheinen nicht explizit im Code. Die Klassendefinition für den Typ Karte sieht so aus:

class Karte:
  def __init__(self, farbe=0, rang=2):
    self.farbe = farbe
    self.rang = rang

Wie gewohnt haben wir hier eine Initialisierungsmethode geschrieben, die ein optionales Argument für jedes Attribut übernimmt. Der Standardwert für die farbe ist 0, was die Farbe Treff darstellt.

Um eine Karte zu erzeugen, rufen wir den Karte Konstruktor mit den Argumenten der Karte, die wir haben wollen auf:

treffDrei = Karte(0, 3)

Im nächsten Abschnitt werden wir heraus finden, welche Karte wir da gerade erzeugt haben.

15.3 Klassenattribute und die __str__ Methode

Damit wir Kartenobjekte so ausgeben können, dass sie leicht lesbar sind, wollen wir die ganzzahligen Codes auf Wörter abbilden. Auf natürliche Weise geht das mit Listen von Zeichenketten. Wir weisen diese Listen sogenannten Klassenattributen am Anfang der Klassendefinition zu:

class Karte:
  farbListe = ["Treff", "Pik", "Karo", "Herz"]
  rangListe = ["nix", "As", "2", "3", "4", "5", "6", "7",
              "8", "9", "10", "Bube", "Dame", "König"]

  #init Methode weggelassen

  def __str__(self):
    return (self.farbListe[self.farbe] + " " +
            self.rangListe[self.rang])

Klassenattribute werden außerhalb aller Methoden definiert. Auf sie kann von allen Methoden der Klasse aus zugegriffen werden.

In der Methode __str__ können wir farbListe und rangListe benutzen, um die numerischen Werte vonfarbe und rang auf Strings abzubilden. Zum Beispiel bedeutet der Ausdruck self.farbListe[self.farbe] "benutze das Attribut farbe des Objekts self als Index für das Klassenattribut farbListe, und wähle den entsprechenden String aus."

Der Zweck des Eintrags "nix" als erstes Element in rangListe ist es, als Platzhalter für das nullte Element der Liste zu fungieren, das nicht zur Verwendung gedacht ist. Nur die Ränge 1 bis 13 sind gültig. Dieses zusätzliche Element ist nicht zwingend notwendig. Wir hätten auch, wie üblich, mit Index 0 beginnen können. Wir finden es aber weniger verwirrend, 2 mit 2 zu kodieren, 3 mit 3 und so fort.

Mit den Methoden, die wir bis jetzt haben, können wir Karten erzeugen und ausgeben:

>>> karte1 = Karte(3, 11)
>>> print karte1
Herz Bube

Klassenattribute wie farbListe werden von allen Karte Objekten gemeinsam verwendet. Der Vorteil davon ist, dass wir ein beliebiges Objekt des Typs Karte verwenden können um auf Klassenattribute zuzugreifen:

>>> karte2 = Karte(3, 3)
>>> print karte2
Herz 3
>>> print karte2.farbListe[3]
Herz

Der Nachteil ist aber, dass eine Veränderung eines Klassenattributes jede Instanz der Klasse betrifft. Wenn wir beispielsweise finden, dass "Herz Bube" - in irgend einem sonderbaren Kartenspiel - besser "Mörder Bube" heißen sollte, können wir folgendes machen:

>>> karte1.farbListe[3] = "Mörder"
>>> print karte1
Mörder Bube

Das Problem dabei ist, dass alle Herz dabei zu Mördern werden:

>>> print karte2
Mörder 3

Es ist normalerweise kein guter Einfall Klassenattribute zu verändern.

15.4 Karten vergleichen

Für einfache Datentypen haben wir die Vergleichsoperatoren (<, >, ==, etc.) zum Vergleich von Werten kennengelernt. Sie stellen fest ob ein Wert größer oder kleiner als ein anderer oder gleich einem anderen ist. Für benutzerdefinierte Datentypen können wir dieses Verhalten der eingebauten Operatoren erweitern, indem wir eine Methode namens __cmp__ bereit stellen (von compare, engl.: vergleichen). Es ist festgelegt, dass __cmp__ zwei Parameter self und other hat und 1 zurückgibt, wenn das erste Objekt größer ist als das zweite, -1 wenn das zweite Objekt größer ist und 0 wenn beide gleich groß sind.

Manche Typen sind vollständig geordnet. Das heißt, man kann zwei beliebige Elemente davon vergleichen und feststellen, welches größer ist. So sind zum Beispiel die ganzen Zahlen und die Gleitkommazahlen vollständig geordnet. Manche Mengen sind ungeordnet. Das heißt, dass man nicht auf sinnvolle Weise angeben kann, dass ein Element größer als das andere ist. So sind zum Beispiel die Früchte ungeordnet, weshalb wir nicht Äpfel mit Birnen vergleichen können.

Die Menge der Spielkarten ist teilweise geordnet. Das heißt, manchmal kann man zwei Karten vergleichen und manchmal nicht. Zum Beispiel ist es klar, dass Pik 3 höher ist als Pik 2; ebenso ist Pik 3 höher als Treff 3. Aber welche der Karten Treff 3 und Pik 2 ist höher? Eine hat einen höheren Rang, aber die andere eine höhere Farbe.

Um Spielkarten vergleichbar zu machen, muss man entscheiden, was wichtiger ist, Rang oder Farbe. Offen gesagt, die Wahl steht uns frei. Wählen wir also etwas, indem fir festlegen, dass die Farbe wichtiger ist als der Rang, weil ein neu gekauftes Paket Karten sortiert daher kommt, alle Treff beisammen, dann alle Pik und so fort.

Nach dieser Entscheidung können wir __cmp__ schreiben:

def __cmp__(self, other):
  # Prüfung der Farben
  if self.farbe > other.farbe: return 1
  if self.farbe < other.farbe: return -1
  # Farben sind gleich... Prüfung der Ränge
  if self.rang > other.rang: return 1
  if self.rang < other.rang: return -1
  # Ränge sind gleich... unentschieden
  return 0

In dieser Ordnung gelten Asse weniger als Karten mit Rang 2.

Übung: ändere __cmp__ so ab, dass Asse einen höheren Rang haben als Könige.

15.5 Pakete

Nun, da wir Objekte haben, die Karten repräsentieren, ist der nächste logische Schritt eine Klasse zu definieren, um ein Paket zu modellieren. Natürlich besteht ein Paket aus Karten, daher wird jedes Paket Objekt eine Liste von Karten als Attribut enthalten.

Es folgt nun eine Klassendefinition für die Paket Klasse. Die Initialisierungsmethode erzeugt das Attribut karten mitsamt dem Standardset von zweiundfünfzig Karten:

class Paket:
  def __init__(self):
    self.karten = []
    for farbe in range(4):
      for rang in range(1, 14):
        self.karten.append(Karte(farbe, rang))

Der einfachste Weg, das Paket aufzufüllen, verwendet eine geschachtelte Schleife. Die äußere Schleife nummeriert die Farben von 0 bis 3. Die innere Schleife nummeriert die Ränge von 1 bis 13. Weil die äußere Schleife vier mal durchlaufen wird und die innere Schleife dreizehn mal, wird der Schleifenkörper insgesamt 52 mal ausgeführt (dreizehn mal vier). Jede Iteration, also jeder Schleifendurchgang, erzeugt eine neue Instanz der Klasse Karte mit laufender Farbe und laufendem Rang und hängt diese Karte (mit der Methode append) an die karten Liste an.

Die append Methode funktioniert für Listen, aber natürlich nicht für Tupel.

15.6 (Bildschirm)ausgabe des Pakets

Wie üblich, wenn wir einen neuen Typ definieren, brauchen wir jetzt eine Methode, die uns den Inhalt eines Objekts auf dem Bildschirm darstellt. Um ein Paket auszugeben, durchlaufen wir die karten-Liste und geben jede Karte aus:

class Paket:
  ...
  def printPaket(self):
    for karte in self.karten:
      print karte

Hier und im Folgenden sollen die Punkte (...) andeuten, dass wir die anderen Methoden der Klasse weggelassen haben.

Als Alternative zu printPaket, könnten wir eine Methode __str__ für die Paket Klasse schreiben. Der Vorteil der __str__ Methode ist, dass sie flexibler ist. Anstatt nur den Inhalt des Objekts auszugeben, erzeugt sie nämlich eine Stringdarstellung, die andere Teile des Programms gegebenenfalls manipulieren können, bevor sie ausgegeben wird oder auch für spätere Verwendung abspeichern.

Hier ist eine Version von __str__ die eine Stringdarstellung von Paket zurückgibt. Als kleines Schmankerl ordnet sie die Karten in Kaskadenform an, wobei jede Karte um ein Leerzeichen mehr eingerückt wird, als die vorhergehende.

class Paket:
  ...
  def __str__(self):
    s = ""
    for i in range(len(self.karten)):
      s = s + " "*i + str(self.karten[i]) + "\n"
    return s

An diesem Beispiel können wir einige Besonderheiten beobachten:

Erstens: Anstatt die Liste self.karten zu durchlaufen und jede Karte einer Variablen zuzuweisen, benutzen wir hier i als Schleifenvariable und als Index für Elemente der Kartenliste.

Zweitens: Wir benutzen den Multiplikationsoperator für Strings um jede Karte ein Leerzeichen mehr einzurücken als die letzte. Der Ausdruck " "*i liefert eine Anzahl von Leerzeichen gleich den laufenden Wert des Index i.

Drittens: Anstatt die print Anweisung zu verwenden um die Karten auszugeben, benutzen wir die str Funktion. Ein Objekt als Argument an str zu übergeben ist gleichwertig mit dem Aufruf der __str__ Methode für dieses Objekt.

Viertens: wir verwenden die Variable s als eine Akkumulator-Variable. Anfangs ist s der Leerstring. Bei jedem Schleifendurchgang wird ein neuer String erzeugt und mit dem alten Wert von s verkettet um den neuen Wert zu erhalten. Wenn die Schleife abgearbeitet ist, enthält s die vollständige Stringdarstellung des Pakets, die so aussieht:

>>> paket = Paket()
>>> print paket
Treff As
Treff 2
  Treff 3
   Treff 4
    Treff 5
     Treff 6
      Treff 7
       Treff 8
        Treff 9
         Treff 10
          Treff Bube
           Treff Dame
            Treff König
             Pik As
          ...

Und so fort. Obwohl das Ergebnis 52 Zeilen aufweist, ist es nur eine lange Zeichenkette, die 52 newline-Zeichen '\n' enthält.

15.7 Das Paket mischen

Wenn ein Paket perfekt gemischt ist, dann kann jede Karte gleich wahrscheinlich an jeder Stelle des Pakets liegen und jede Stelle im Paket enthält mit gleicher Wahrscheinlichkeit jede Karte.

Um das Paket zu mischen, werden wir die randrange Funktion aus dem random Module verwenden. Mit zwei ganzzahligen Argumenten a und b aufgerufen, gibt randrange ein zufällige ganze Zahl im Bereich a <= x < b zurück. Da die obere Schranke streng kleiner als b ist, können wir die Länge einer Liste als zweiten Parameter verwenden und bekommen damit garantiert einen legalen Index. Der folgende Ausdruck liefert beispielsweise den Index einer Zufallskarte aus einem Paket:

random.randrange(0, len(self.karten))

Ein einfacher Weg, ein Paket zu mischen, ist die Karten zu durchlaufen und dabei jede Karte mit einer zufällig ausgewählten zu vertauschen. Dabei ist es durchaus möglich, dass eine Karte mit sich selbst vertauscht wird, aber das passt schon. Würden wir diese Möglichkeit ausschließen, so wäre in der Tat nach dem Mischen die Reihenfolge der Karten nicht ganz zufällig:

class Paket:
  ...
  def mischen(self):
    import random
    nKarten = len(self.karten)
    for i in range(nKarten):
      j = random.randrange(i, nKarten)
      self.karten[i], self.karten[j] = self.karten[j], self.karten[i]

Anstatt anzunehmen, dass zweiundfünfzig Karten in dem Paket sind, ermitteln wir die aktuelle Länge der Liste und speichern sie in nCards.

Für jede Karte im Paket wählen wir eine Zufallskarte aus den Karten aus, die bis jetzt noch nicht gemischt worden sind. Dann vertauschen wir die laufende Karte (i) mit der ausgewählten Karte (j). Für das Vertauschen der Karten verwenden wir eine Tupel-Wertzuweisung, wie im Abschnitt 9.2:

self.karten[i], self.karten[j] = self.karten[j], self.karten[i]

Übung: Schreibe diese Codezeile neu, ohne Tupel-Wertzuweisung zu benutzen.

15.8 Entfernen von Karten und Geben

Eine weitere nützliche Methode der Paket Klasse ist entferneKarte. Sie übernimmt eine Karte als Argument, entfernt sie und gibt wahr (1) zurück, wenn die Karte im Paket war und falsch (0) andernfalls:

class Paket:
  ...
  def entferneKarte(self, karte):
    if karte in self.karten:
      self.karten.remove(karte)
      return 1
    else:
      return 0

Der in Operator gibt wahr zurück, wenn der erste Operand im zweiten enthalten ist, der eine Liste oder ein Tupel sein muss. Wenn der erste Operand ein Objekt ist, benutzt Python die __cmp__ Methode des Objekts um die Gleichheit mit den Elementen der Liste zu prüfen. Weil __cmp__ in der Karte Klasse auf tiefe Gleichheit prüft, überprüft auch die removeCard Methode auf tiefe Gleichheit.

Um die Karten zu geben, d. h. die Karten an die Spieler zu verteilen, benötigen wir eine Methode, die die oberste Karte entfernt und zurückgibt. Die Listen-Methode pop ermöglicht etwas derartiges auf bequeme Weise:

class Paket:
  ...
  def popKarte(self):
    return self.karten.pop()

Tatsächlich entfernt pop die letzte Karte in der Liste, sodass wir in Wahrheit die Karten des Pakets von unten ausgehend austeilen.

Eine weitere Operation, die wir ziemlich sicher brauchen werden, ist die boole'sche Funktion istLeer, die wahr zurückgibt, wenn das Paket keine Karten (mehr) enthält.

class Paket:
  ...
  def istLeer(self):
    return (len(self.karten) == 0)

15.9 Glossar

kodieren
Eine Menge von Werten durch eine andere Menge von Werten darstellen, indem man eine Abbildung zwischen diesen beiden Mengen konstruiert.
encode
To represent one set of values using another set of values by constructing a mapping between them.
Klassenattribut
Eine Variable, die innerhalb einer Klassendefinition aber außerhalb aller Methoden definiert wird. Auf Klassenattribute kann von jeder Methode der Klasse aus zugegriffen werden. Sie werden von allen Instanzen einer Klasse gemeinsam benutzt.
class attribute
A variable that is defined inside a class definition but outside any method. Class attributes are accessible from any method in the class and are shared by all instances of the class.
Akkumulator-Variable
Eine Variable, die in einer Schleife dazu benutzt wird, eine Folge von Werten zu akkumulieren, beispielsweise durch Verkettung mit einem String oder durch Addition zu einer laufenden Summe.
accumulator
A variable used in a loop to accumulate a series of values, such as by concatenating them onto a string or adding them to a running sum.


zurück rauf weiter Englisch Index