Rationale Zahlen als Objekte

Als Beispiel definieren wir eine Klasse Bruch von Objekten, die rationale Zahlen darstellen.

class Bruch:
    def __init__(self, zaehler, nenner):
        self.zaehler = zaehler
        self.nenner = nenner

Das Schlüsselwort class leitet die Klassendefinition ein und ist gefolgt vom Namen der Klasse. Innerhalb der Klasse definieren wir eine, Konstruktor genannte, Methode __init__, die bei der Erzeugung von Objekten der Klasse ausgeführt wird. Die Methode hat hier drei Parameter self, zaehler und nenner Der erste Parameter muss bei der Erzeugung nicht angegeben werden sondern verweist automatisch auf das neu erzeugte Objekt. Die restlichen Parameter müssen bei der Erzeugung angegeben werden. In unserem Beispiel speichern wir deren Werte in den Attributvariablen zaehler und nenner auf die wir mit der Punkt-Schreibweise des Objektes in self zugreifen können. Attribute speichern den Zustand konstruierter Objekte und sind überall innerhalb der Klassendefinition (und in Python sogar außerhalb dieser) sichtbar.

Um Objekte der Klasse Bruch zu erzeugen, rufen wir Bruch als Funktion auf und übergeben die vom Konstruktor erwarteten Argumente für Zähler und Nenner. Auch diese Funktion nennen wir Konstruktor. Sie erzeugt eine neue Instanz der zugehörigen Klasse und ruft dann die Methode __init__ auf.

Bisher haben wir Objekte meist ohne Verwendung expliziter Konstruktoren erzeugt. Für Zahlen, Wahrheitswerte, Zeichenketten und Arrays bietet Python spezielle Syntax, die es erlaubt, Objekte kompakter zu initialisieren. Zum Beispiel bei Arrays können wir allerdings auch explizite Konstruktoren verwenden, wie die folgenden Aufrufe zeigen.

>>> nums = [1,2,3]
>>> nums2 = list(nums)
>>> nums is nums2
False
>>> nums == nums2
True

Man kann von Arrays also eine Kopie anlegen, indem man sie bei der Konstruktion eines neuen Objektes als Parameter an den list Konstruktor übergibt.

Doch nun zurück zu der selbst definierten Klasse für Brüche.

>>> Bruch(3,4)
<__main__.Bruch object at 0x7fe84aed0970>
>>> Bruch(8,6)
<__main__.Bruch object at 0x7fe84ae1c310>

Durch Übergabe von Zähler und Nenner an den Konstruktor Bruch wird jeweils ein neues Objekt erzeugt, das entsprechende Werte in den Attributen zaehler und nenner speichert. In Python werden die erzeugten Objekte standardmäßig durch Angabe des Klassennamens und der Speicheradresse angezeigt. Wir können eine alternative Darstellung definieren, indem wir eine Methode __str__ definieren, die von der str-Funktion automatisch verwendet wird.

Dazu fügen wir innerhalb der Klassendefinition folgendes ein.

    def __str__(self):
        return str(self.zaehler) + "/" + str(self.nenner)

Auch diese Methode hat (wie alle Methoden) einen ersten Parameter (hier self), der beim Aufruf automatisch auf das Objekt verweist, auf dem die Methode aufgerufen wird. Dadurch ist es möglich, in Methoden auf Attributvariablen (und andere Methoden) zuzugreifen.

Wir erzeugen erneut ein Bruch-Objekt und beobachten, wie es nun angezeigt wird.

>>> drei4tel = Bruch(3,4)
>>> drei4tel
<__main__.Bruch object at 0x7f2c8fcea970>
>>> str(drei4tel)
'3/4'
>>> print(drei4tel)
3/4

Wie wir sehen, wandelt auch print das Argument automatisch mit Hilfe der __str__-Methode in einen String um, wenn diese vorhanden ist. Indem wir zusätzlich eine Methode __repr__ definieren, die __str__ aufruft, können wir beeinflussen, wie Bruch-Objekte in der interaktiven Python-Umgebung angezeigt werden.

    def __repr__(self):
        return self.__str__()

Hier wird beim Aufruf von __str__ der erste Parameter automatisch mit dem Objekt in self initialisiert, auf dem wir die Methode aufrufen.

Nun werden Brüche auch ohne expliziten Aufruf von str oder print in unserer eigenen Darstellung angezeigt.

>>> acht6tel = Bruch(8,6)
>>> acht6tel
8/6

Es wäre schön, wenn Brüche automatisch gekürzt würden. Dazu können wir Zähler und Nenner im Konstruktor durch deren größten gemeinsamen Teiler teilen. Zur Berechnung dessen verwenden wir den Algorithmus von Euklid.

Wir ersetzen also den Konstruktor __init__ wie hier gezeigt und fügen die Methode ggT hinzu.

    def __init__(self, zaehler, nenner):
        gcd = self.ggT(zaehler, nenner)

        self.zaehler = zaehler // gcd
        self.nenner = nenner // gcd
    
    def ggT(self, a, b):
        while b != 0:
            x = b
            b = a % x
            a = x
        
        return a

Nun werden alle erzeugten Brüche intern gekürzt dargestellt also auch so angezeigt.

>>> Bruch(8,6)
4/3

Als nächstes wollen wir eine Methode zum Multiplizieren von Brüchen definieren. Diese Methode soll das Ergebnis als neues Objekt zurück liefern und die multiplizierten Objekte nicht verändern.

Zur Definition der Multiplikation definieren wir eine Methode mit dem Namen mal. Deren Implementierung erzeugt ein neues Objekt der Klasse Bruch und greift sowohl auf die eigenen Attribute als auch auf diejenigen des übergebenen Argumentes zu.

    def mal(self, other):
        return Bruch(
            self.zaehler * other.zaehler,
            self.nenner * other.nenner
        )

Nun können wir Brüche wie folgt multiplizieren.

>>> drei4tel = Bruch(3,4)
>>> acht6tel = Bruch(8,6)
>>> drei4tel.mal(acht6tel)
1/1

Wir wir sehen, wird das Ergebnis dabei automatisch gekürzt. Zur Implementierung des kürzenden Konstruktors haben wir eine Methode ggT definiert, die wir auch außerhalb der Klassendefinition auf Bruch-Objekten aufrufen können.

>>> drei4tel.ggT(24,16)
8

Wir hatten nicht beabsichtigt, Brüchen die ggT-Funktion als nach außen sichtbare Methode hinzuzufügen. Wir wollten diese lediglich im Konstruktor verwenden, um Brüche zu kürzen.

In Python können die Namen von Attributen und Methoden mit einem Unterstrich beginnen. Dadurch wird per Konvention signalisiert, dass diese Namen außerhalb der Klassendefinition nicht verwendet werden sollen.1 In anderen Sprachen kann die Sichtbarkeit von Attribute und Methoden gesteuert werden, um die Verwendung außerhalb der Klassendefinition zu verhindern.

Auch auf Attributvariablen können wir in Python von außen zugreifen, wie das folgende Beispiel zeigt.

>>> drei4tel.nenner = 3
>>> drei4tel
3/3

Um unsere Absicht zu kommunizieren, dass Brüche nicht mutierbar sein sollen, können wir die Attributvariablen mit einem Unterstrich am Anfang benennen. Der Zugriff von außen wird dadurch zwar nicht verhindert, aber solche Zugriffe sind zumindest am Unterstrich besser als (per Konvention) unzulässig zu erkennen. Lesender Zugriff von außen sollte jedoch weiterhin erlaubt sein. Dazu definieren wir Methoden, die die Werte von Zaehler und Nenner zurückliefern.

Hier ist noch einmal die komplette Definition der Bruch-Klasse inklusive Verwendung von Unterstrichen um sogenannte private Bestandteile zu kennzeichnen.

class Bruch:
    def __init__(self, zaehler, nenner):
        gcd = self._ggT(zaehler, nenner)

        self._zaehler = zaehler // gcd
        self._nenner = nenner // gcd
    
    def _ggT(self, a, b):
        while b != 0:
            x = b
            b = a % x
            a = x
        
        return a

    def zaehler(self):
        return self._zaehler
    
    def nenner(self):
        return self._nenner

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

    def __str__(self):
        return str(self._zaehler) + "/" + str(self._nenner)

    def __repr__(self):
        return self.__str__()

  1. Bei Verwendung eines doppelten Unterstriches wird in Python eine Umbenennung vorgenommen, die eine Verwendung von außen zwar nicht verhindert, aber versehentliche Verwendung unwahrscheinlicher macht. ↩︎