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
.
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
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
Das Konzept abstrakter Klassen erlaubt die Definition von Klassen, die nicht direkt instantiiert werden können. ↩︎ ↩︎
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. ↩︎