Lösungen

Aufgabe: Klassenhierarchie geometrischer Figuren erweitern

Zur Darstellung von Rechtecken definieren wir eine Klasse Rect als Unterklasse von Shape.

class Rect(Shape):
    def __init__(self, top_left, width, height):
        super().__init__(top_left)
        self.set_width(width)
        self.set_height(height)
    
    def width(self):
        return self._width
    
    def height(self):
        return self._height
    
    def set_width(self, width):
        if type(width) == int:
            self._width = width
    
    def set_height(self, height):
        if type(height) == int:
            self._height = height

    def top_left(self):
        return self.location()
    
    def set_top_left(self, top_left):
        self.set_location(top_left)

Das geerbte Attribut _location interpretieren wir hier als obere linke Ecke und speichern zusätzlich Breite und Höhe in Attributen _width und _height. Wir können also mit unserer Implementierung nur achsenparallele Rechtecke darstellen. Zusätzliche zu den Zugriffsmethoden für die neuen Attribute definieren wir Methoden top_left und set_top_left zum Zugriff auf das geerbte _location-Attribut.

Da Quadrate besondere Rechtecke sind, definieren wir eine Klasse Square als Unterklasse der eben definierten Klasse Rect.

class Square(Rect):
    def __init__(self, top_left, size):
        super().__init__(top_left, size, size)
    
    def size(self):
        return self.width()
    
    def set_size(self, size):
        if type(size) == int:
            self._width = size
            self._height = size

    def set_width(self, width):
        self.set_size(width)
    
    def set_height(self, height):
        self.set_size(height)

Die __init__-Methode implementieren wir durch Rückgriff auf die entsprechende Methode der Oberklasse, wobei wir als Breite und Höhe jeweils die übergebene Kantenlänge übergeben. Zum Zugriff auf die Kantenlänge greifen wir auf die gerbten Attribute _width und _height zu, wobei wir sicherstellen, dass Breite und Höhe immer den gleichen Wert haben. Dazu überschreiben wir die schreibenden Zugriffsmethoden set_width und set_height unter Rückgriff auf die neu definierte Methode set_size.

Hausaufgabe: Klassen für arithmetische Ausdrücke definieren

Welche Implementierung von __str__ in der gezeigten Definition von __repr__ aufgerufen wird, hängt davon ab, auf welchem Objekt __repr__ aufgerufen wird. Es wird die zu diesem Objekt gehörige Implementierung von __str__ verwendet. Unterklassen von Expression können daher unterschiedliche Implementierungen von __str__ bereitstellen und die von der Klasse Expression geerbte Methode __repr__ kann diese unterschiedlichen Implementierungen verwenden.

Die Klasse Number definieren wir wie folgt.

class Number(Expression):
    def __init__(self, value):
        self._value = value
    
    def value(self):
        return self._value
    
    def __str__(self):
        return str(self._value)

Es ist sinnvoll von Expression zu erben, damit Number-Objekte in python3 ordentlich angezeigt werden, ohne dass wir erneut die Methode __repr__ überschreiben müssen.

Zur Definition der Klassen Sum und Product definieren wir zunächst eine gemeinsame Oberklasse Binary wie folgt.

class Binary(Expression):
    def __init__(self, left, right):
        self._left = left
        self._right = right
    
    def left(self):
        return self._left
    
    def right(self):
        return self._right
    
    def __str__(self):
        return "(" + str(self.left()) + self.op() + str(self.right()) + ")"

Im Konstruktor werden übergebene Argumente in Attributvariablen gespeichert. Die Methode __str__ definieren wir unter Verwendung einer Methode op, die von Unterklassen bereitgestellt werden muss. Die Methode value implementieren wir für die Klasse Binary nicht.

Die Unterklassen Sum und Product von Binary erben den Konstruktor der Oberklasse und berechnen den Wert zugehöriger Ausdrücke mit Hilfe entsprechender Methoden für die Argumente. Zusätzlich definieren sie die in der geerbten __str__ Methode verwendete Methode op.

class Sum(Binary):
    def value(self):
        return self.left().value() + self.right().value()
    
    def op(self):
        return "+"

class Product(Binary):
    def value(self):
        return self.left().value() * self.right().value()
    
    def op(self):
        return "*"

Die Definition der Hilfsklasse Binary vermeidet eine doppelte Implementierung der Textdarstellung. Da Sie weder die value noch die op Methode implementiert, sollte sie nicht direkt instantiiert werden (sondern nur indirekt über Unterklassen, die alle benötigten Methoden implementieren.)1

Hausaufgabe: Klassen für Schachfiguren definieren

Der Klasse Piece fehlt eine Methode is_allowed, die aber bei Aufrufen der Methode move benötigt wird. Die Implementation der Klasse Piece ist insofern unvollständig. Da sich die Zugregeln für die Schachfiguren unterscheiden, muss is_allowed für die Unterklassen jeweils unterschiedlich definiert werden. Daher ist es nicht sinnvoll, diese direkt in Piece zu definieren.

Eine Klasse, deren Methoden unvollständig definiert sind, ist nicht geeignet, Objekte zu instanziieren, vielmehr dient Sie als Vorlage zur Definition der allen Unterklassen gemeinsamen Attribute und Methoden 1. So vermeidet man Redundanz in den Definitionen der Unterklassen.

class Rook(Piece):

  def is_allowed(self,field):
    return self.is_orthogonal(field)

  def __str__(self):
    return "Rook (" + self._color + ") at: " + self.position()

class Bishop(Piece):

  def is_allowed(self,field):
    return self.is_diagonal(field)

  def __str__(self):
    return "Bishop (" + self._color + ") at: " + self.position()

class Queen(Piece):

  def is_allowed(self,field):
    return self.is_orthogonal(field) or self.is_diagonal(field)

  def __str__(self):
    return "Queen (" + self._color + ") at: " + self.position()

Die Definition der Klasse Piece vermeidet eine mehrfache Implementierung der Methoden, die für alle Unterklassen gleich sind, indem die Methoden col, row, is_orthogonal, is_diagonal in Piece ergänzt werden:

class Piece:

  def __init__(self, color, field):
    self._color = color
    self.set_position(field)

  def position(self):
    return self._position

  def set_position(self, field):
    self._position = field

  def move(self, field):
    if self.is_allowed(field):
      self.set_position(field)

  def row(self, pos):
    return ord(pos[1]) - 49

  def col(self, pos):
    return ord(pos[0]) - 97

  def is_orthogonal(self, field):
    return self.row(self.position()) == self.row(field) \
        or self.col(self.position()) == self.col(field)

  def is_diagonal(self, field):
    return self.row(self.position()) - self.col(self.position()) \
        == self.row(field)           - self.col(field) \
      or   self.row(self.position()) + self.col(self.position()) \
        == self.row(field)           + self.col(field)
  
  def __repr__(self):
    return str(self)

Der Aufruf von r.move(“a3”) auf einem Rook-Objekt ruft die Methode move der Überklasse auf, die ihrerseits die Methode is_valid der Rook-Klasse aufruft um schließlich mit set_position als Methode der Überklasse das Attribut _position des Rook-Objektes zu mutieren.

Mit diesem Mechanismus, der durch dynamische Bindung ermöglicht wird, lässt sich das unterschiedliche Verhalten verschiedener Piece-Unterklassen implementieren 2


  1. Das Konzept abstrakter Klassen erlaubt die Definition von Klassen, die nicht direkt instantiiert werden können. ↩︎ ↩︎

  2. Eine Methode ist polymorph, wenn sie mehrfach (in verschiedenen Klassen) mit der gleichen Signatur definiert ist, jedoch unterschiedlich implementiert ist. Beispiele sind __str__ und is_allowed in dieser Aufgabe. ↩︎