Lösungen

Lösungen

Aufgabe: Bruch-Klasse erweitern

Hier ist eine neue Version des Konstruktors. Er gibt eine Fehlermeldung aus, wenn ein Bruch konstruiert wird, dessen Nenner Null ist.

    def __init__(self, zaehler, nenner):
        self._zaehler = zaehler
        self._nenner = nenner

        if nenner == 0:
            print("Der Nenner ist Null!")
            return
        
        gcd = self._ggT(zaehler, nenner)

        self._zaehler = zaehler // gcd
        self._nenner = nenner // gcd

Der folgende Aufruf demonstriert den Effekt dieser Änderung.

>>> Bruch(7,0)
Der Nenner ist Null!
7/0

Hier sind Definitionen der verbleibenden Grundrechenarten inklusive dazu verwendeter Hilfsmethoden.

    def negativ(self):
        return Bruch(-self._zaehler, self._nenner)
    
    def minus(self, other):
        return self.plus(other.negativ())

    def kehrwert(self):
        return Bruch(self._nenner, self._zaehler)
    
    def durch(self, other):
        return self.mal(other.kehrwert())

Zur Definition der Subtraktion definieren wir eine Negationsmethode negativ. Zur Definition der Division definieren wir eine Methode kehrwert. Beide Methoden sind unabhängig von unserer Verwendung nützlich, weshalb wir ihren Namen keinen Unterstrich voranstellen.

Da Brüche gekürzt dargestellt werden, können wir die Vergleichsmethode wie folgt definieren.

    def ist_gleich(self, other):
        return self._zaehler == other._zaehler and self._nenner == other._nenner

Bonusaufgabe: Klasse für Komplexe Zahlen definieren

Als Namen für die Klasse komplexer Zahlen wählen wir Komplex.

class Komplex

Wir definieren einen Konstruktor, der Real- und Imaginärteil als Parameter erwartet und als entsprechende (private) Attribute speichert.

    def __init__(self, real, imag):
        self._real = real
        self._imag = imag

Für diese Attribute definieren wir lesende Zugriffsmethoden.

    def real(self):
        return self._real
    
    def imag(self):
        return self._imag

Als Zeichenketten-Darstellung für komplexe Zahlen wählen wir die Form a+bi mit Vereinfachungen für einige Sonderfälle. Zum Beispiel Stellen wir die Zahl 1+0i als 1 dar, die Zahl 0-i als -i und so weiter.

    def __repr__(self):
        return str(self)

    def __str__(self):
        if self._imag == 0:
            return str(self._real)
        
        if self._real == 0:
            return self._imag_str()
        
        if self._imag < 0:
            return str(self._real) + self._imag_str()
        
        return str(self._real) + "+" + self._imag_str()

Die Hilfsmethode _imag_str gibt die Zeichenkettendarstellung des Imaginärteils zurück und soll nicht von außen verwendet werden.

    def _imag_str(self):
        if self._imag == 1:
            return "i"
        
        if self._imag == -1:
            return "-i"
        
        return str(self._imag) + "i"

Wir können nun die Zahl i wie folgt erzeugen und anzeigen lassen.

>>> Komplex.new(0,1)
i

Zur Addition zweier komplexer Zahlen addieren wir deren Real- und Imaginärteile getrennt voneinander. Ist das Argument keine komplexe Zahl interpretieren wir es als reele Zahl und erzeugen eine entsprechende komplexe Zahl vor der Addition.

    def plus(self, other):
        if type(other) == Komplex:
            return Komplex(
                self._real + other._real,
                self._imag + other._imag
            )
        else:
            return self.plus(Komplex(other, 0))

Durch diesen Trick ist es möglich, komplexe Zahlen mit reellen zu addieren - zumindest, wenn das erste Argument eine komplexe Zahl ist:

>>> i = Komplex.new(0,1)
>>> i.plus(1)
1+i

Zur Subtraktion komplexer Zahlen definieren wir zunächst die Negation und verwenden dann die Addition zum Subtrahieren.

    def negativ(self):
        return Komplex(-self._real, -self._imag)
    
    def minus(self, other):
        if type(other) == Komplex:
            return self.plus(other.negativ())
        else:
            return self.plus(Komplex(-other, 0))

Multiplikation und Division implementieren wir auf Basis von Absolutbetrag und Winkel im Bogenmaß (Radiant), die mit Hilfe vordefinierter mathematischer Funktionen aus Real- und Imaginärteil berechnet werden können, die wir mit import math importieren.

    def abs(self):
        return math.sqrt(self._real ** 2 + self._imag ** 2)
    
    def rad(self):
        return math.atan2(self._imag, self._real)

Zur Multiplikation unterscheiden wir wieder ob das Argument eine komplexe Zahl ist. Falls nicht, interpretieren wir das Argument als reele Zahl und multiplizieren Real- und Imaginärteil getrennt voneinander mit dieser. Ansonsten berechnen wir das Ergebnis in Polarkoordinaten und erzeugen aus diesen das Ergebnis.

    def mal(self, other):
        if type(other) == Komplex:
            return polar(self.abs() * other.abs(), self.rad() + other.rad())
        else:
            return Komplex(self._real * other, self._imag * other)

Zur Division verfahren wir analog und die Definition der Klasse Komplex ist beendet.

    def durch(self, other):
        if type(other) == Komplex:
            return polar(self.abs() / other.abs(), self.rad() - other.rad())
        else:
            return Komplex(self._real / other, self._imag / other)

Die hier verwendete Funktion polar zur Konstruktion einer komplexen Zahl aus Polarkoordinaten definieren wir (außerhalb der Klassendefinition) wie folgt.

def polar(abs, rad):
    return Komplex(math.cos(rad), math.sin(rad)).mal(abs)

Hierbei wird wieder die Multiplikation auf komplexen Zahlen verwendet, um mit dem Absolutbetrag zu multiplizieren. Unsere Multiplikations-Methode und die polar-Funktion rufen sich also gegenseitig auf, allerdings nicht endlos, da der Absolutbetrag eine reelle Zahl ist.

Hier sind einige Beispielaufrufe zum Testen der Implementierung.

>>> i = Komplex(0,1)
>>> i.mal(i)
-1.0+1.2246467991473532e-16i
>>> i.durch(i)
1.0
>>> i.plus(1).mal(i.minus(1)).durch(2)
-1.0000000000000002+1.2246467991473535e-16i
>>> i.plus(2).mal(i.plus(3))
5.000000000000001+5.0i
>>> i.plus(2).durch(i.plus(3))
0.7+0.09999999999999999i

Hausaufgabe: Konto-Klasse erweitern

Die Methode zum Einzahlen erweitern wir wie folgt, um die Einzahlung negativer Beträge zu verhindern.

    def einzahlen(self, betrag):
        if 0 <= betrag:
            self._guthaben = self._guthaben + betrag
            return True
        else:
            return False

Wir geben statt self nun einen Wahrheitswert zurück, an dem aufrufender Programmcode erkennen kann, ob die Einzahlung erfolgreich war.

Beim Abheben testen wir zusätzlich, ob der auszuzahlende Betrag vom Guthaben gedeckt ist und geben wieder einen Wahrheitswert zurück.

    def abheben(self, betrag):
        if 0 <= betrag and betrag <= self._guthaben:
            self._guthaben = self._guthaben - betrag
            return True
        else:
            return False

Da Ein- und Auszahlungen jetzt fehlschlagen können, müssen wir die Überweisungs-Methode so anpassen, dass sie eine Einzahlung nur genau dann vornimmt, wenn auch die Auszahlung erfolgreich war. Dazu testen wir, ob die Auszahlung erfolgreich war, bevor wir die Einzahlung veranlassen.

    def ueberweisen(self, other, betrag):
        if self.abheben(betrag):
            return other.einzahlen(betrag)
        else:
            return False

Die Einzahlung ist hier immer erfolgreich, da sie nur bei negativem Betrag fehl schlägt. In diesem Fall wäre aber schon die Auszahlung fehlgeschlagen und die Einzahlung garnicht veranlasst worden.

Um Verzinsung zu implementieren fügen wir dem Konstruktor ein Argument für den Zinssatz hinzu, der in einem Attribut gespeichert wird.

    def __init__(self, zinssatz):
        self._guthaben = 0.0
        self._zinssatz = zinssatz

Auf dieses Attribut können wir nun in der Methode zum Verzinsen zugreifen.

    def verzinsen(self):
        self._guthaben = self._guthaben * (1 + self._zinssatz)
        return self

Aufgabe: Stack-Klasse implementieren

class Stack:
    def __init__(self):
        self._elems = []
    
    def is_empty(self):
        return len(self._elems) == 0
    
    def top(self):
        return self._elems[len(self._elems)-1]
    
    def push(self, elem):
        self._elems.append(elem)
        return self
    
    def pop(self):
        return self._elems.pop()

    def __repr__(self):
        return str(self)

    def __str__(self):
        result = "Stack:"
        for i in range(0, len(self._elems)):
            result = result + " " + str(self._elems[i])
        return result

Aufgabe: Queue-Klasse implementieren

class Queue:
    def __init__(self):
        self._elems = []
    
    def is_empty(self):
        return len(self._elems) == 0
    
    def first(self):
        return self._elems[0]
    
    def enqueue(self, elem):
        self._elems.append(elem)
        return self
    
    def dequeue(self):
        self._elems.pop(0)
        return self

    def __repr__(self):
        return str(self)

    def __str__(self):
        result = "Queue:"
        for i in range(0, len(self._elems)):
            result = result + " " + str(self._elems[i])
        return result