Datenkapselung – public vs. private

Schützen von Attributen

Bisher haben wir die Methode aendere_preis_um(self, aenderung)noch gar nicht verwendet. Diese Methode wurde mit einer if-Abfrage so programmiert, dass der Preis eines Autos durch eine Änderung nicht negativ werden kann. Dies sehen wir, wenn wir die Methode in Autotest.py verwenden:

# Datei Auto_Test.py
from Auto  import *

mein_erstes_auto = Auto(1985, "DeLorean", 54000, False, 60000)

mein_erstes_auto.aendere_preis_um(-50000) 
print(mein_erstes_auto.preis) # Ausgabe: 10000

mein_erstes_auto.aendere_preis_um(-20000) # Ausgabe: Die Änderung ist nicht zulässig!
print(mein_erstes_auto.preis) # Ausgabe: 10000

Vielleicht wäre es geschickt, die Methode mache_probefahrt(self, kilometer) ebenfalls etwas sicherer zu gestalten. Momentan können wir nämlich einfach eine negative Zahl angeben und so den Kilometerstand zurückdrehen:

from Auto  import *

mein_erstes_auto = Auto(1985, "DeLorean", 54000, False, 60000)

mein_erstes_auto.mache_probefahrt(-100)
print(mein_erstes_auto.kilometerstand) # Ausgabe: 53900

Auch hier können wir eine if-Abfrage verwenden:

# Datei Auto.py
class Auto:

    def __init__(self, baujahr, marke, kilometerstand, automatik, preis):
        self.baujahr = baujahr
        self.marke = marke
        self.kilometerstand = kilometerstand
        self.automatik = automatik
        self.preis = preis

    def mache_probefahrt(self, kilometer):
        if kilometer >= 0:
            self.kilometerstand = self.kilometerstand + kilometer
        else:
            print("Betrugsversuch!")

    def aendere_preis_um(self, aenderung):
        if self.preis+aenderung > 0:
            self.preis = self.preis + aenderung
        else:
            print("Die Änderung ist nicht zulässig!")

    def __str__(self):
        return str(self.baujahr) + " " + str(self.kilometerstand)

Wollen wir nun den Kilometerstand zurückdrehen, erscheint eine eindeutige Meldung:

# Datei Auto_Test.py
from Auto  import *

mein_erstes_auto = Auto(1985, "DeLorean", 54000, False, 60000)

mein_erstes_auto.mache_probefahrt(-100) # Betrugsversuch!
print(mein_erstes_auto) # Ausgabe: 1985 54000

mein_erstes_auto.mache_probefahrt(100)
print(mein_erstes_auto) # Ausgabe: 1985 54100

Es scheint, als sei der Kilometerstand damit vor Manipulationen geschützt. Das ist er jedoch nicht. Anstatt nämlich mithilfe der Methode mache_probefahrt(self, kilometer) den Kilometerstand zu verändern, können wir einfach direkt auf das Attribut kilometerstand zugreifen und seinen Wert verändern:

# Datei Auto_Test.py
from Auto  import *

mein_erstes_auto = Auto(1985, "DeLorean", 54000, False, 60000)

mein_erstes_auto.kilometerstand = 50000
print(mein_erstes_auto) # Ausgabe: 1985 50000

Hier wird der Kilometerstand einfach auf 50000 km zurückgedreht. Die anderen Attribute sind ebenso frei zugänglich, so dass man auch bei diesen die Werte unkontrolliert verändern könnte.

Um einen solch willkürlichen Zugriff auf die Attribute einer Klasse zu unterbinden, können wir diese schützen, indem man sie als private Attribute deklarieren. Dies geschieht mithilfe eines doppelten Unterstrichs:

# Datei Auto.py
class Auto:

    def __init__(self, baujahr, marke, kilometerstand, automatik, preis):
        self.__baujahr = baujahr
        self.__marke = marke
        self.__kilometerstand = kilometerstand
        self.__automatik = automatik
        self.__preis = preis

    def mache_probefahrt(self, kilometer):
        if kilometer >= 0:
            self.__kilometerstand = self.__kilometerstand + kilometer
        else:
            print("Betrugsversuch!")

    def aendere_preis_um(self, aenderung):
        if self.__preis+aenderung > 0:
            self.__preis = self.__preis + aenderung
        else:
            print("Die Änderung ist nicht zulässig!")

    def __str__(self):
        return str(self.__baujahr) + " " + str(self.__kilometerstand)

Nun läuft der Test so ab:

# Datei Auto_Test.py
from Auto  import *

mein_erstes_auto = Auto(1985, "DeLorean", 54000, False, 60000)

mein_erstes_auto.kilometerstand = 50000
print(mein_erstes_auto) # Ausgabe: 1985 50000

Der Preis wurde also nicht verändert.

Es ist nun vielleicht überraschend, dass gar keine Fehlermeldung auftauchte, als wir versuchten, den Kilometerstand zu ändern. Schließlich sollte das Attribut von außen doch nicht mehr sichtbar sein. Dennoch wird führt die Anweisung mein_erstes_auto.kilometerstand = 50000 zu keinem Fehler.

Das liegt daran, dass Python aufgrund dieser Anweisung unserem Objekt noch ein weiteres öffentliches Attribut kilometerstand hinzufügt! Davon können wir uns z.B. so überzeugen:

# Datei Auto_Test.py
from Auto  import *

mein_erstes_auto = Auto(1985, "DeLorean", 54000, False, 60000)

mein_erstes_auto.kilometerstand = 50000
print(mein_erstes_auto) # Ausgabe: 1985 50000
print(mein_erstes_auto.kilometerstand) # Ausgabe: 50000

Das birgt natürlich die Gefahr von Verwechslungen.

Außerdem können wir nun den Kilometerstand nur noch mittels der Ausgabe durch die str-Methode erfahren, wie man hier sieht:

# Datei Auto_Test.py
from Auto  import *

mein_erstes_auto = Auto(1985, "DeLorean", 54000, False, 60000)

print(mein_erstes_auto.kilometerstand) # Ausgabe: AttributeError: 'Auto' object has no attribute 'kilometerstand'

Möchten wir aber den Stand konkret als int-Wert erhalten, ist dies nun recht ungünstig.

Im Folgenden sehen wir, wie wir diese Schwierigkeiten lösen können.

Getter

Um das Auslesen (aber nicht das Verändern) des Wertes eines Attributes zu ermöglichen, müssen wir die sogenannten Getter (oder auch get-Methoden) ergänzen. Hier ist diese erst einmal nur exemplarisch für den Kilometerstand hinzugefügt:

# Datei Auto.py
class Auto:

    def __init__(self, baujahr, marke, kilometerstand, automatik, preis):
        self.__baujahr = baujahr
        self.__marke = marke
        self.__kilometerstand = kilometerstand
        self.__automatik = automatik
        self.__preis = preis

    def mache_probefahrt(self, kilometer):
        if kilometer >= 0:
            self.__kilometerstand = self.__kilometerstand + kilometer
        else:
            print("Betrugsversuch!")

    def aendere_preis_um(self, aenderung):
        if self.__preis+aenderung > 0:
            self.__preis = self.__preis + aenderung
        else:
            print("Die Änderung ist nicht zulässig!")

    def __str__(self):
        return str(self.__baujahr) + " " + str(self.__kilometerstand)

    def get_kilometerstand(self):
        return self.__kilometerstand

Die Getter sind immer nach dem gleichen Muster aufgebaut. Die einzige Anweisung, die von einem Getter ausgeführt wird, ist das Zurückgeben des Attributwertes mit return.

Nun können wir die Werte der Attribute von außen auslesen. Die Getter bilden dabei eine Einbahnstraße: Auslesen ist erlaubt, Verändern nicht. Hier ein Beispiel zur Verwendung eines Getters:

# Datei Auto_Test.py
from Auto  import *

mein_erstes_auto = Auto(1985, "DeLorean", 54000, False, 60000)

print(mein_erstes_auto.get_kilometerstand()) # Ausgabe: 54000

Setter

Neben den get-Methoden können oft auch set-Methoden nützlich sein. Diese sind – ähnlich wie aendere_preis_um – dazu da, den Wert einen Attributs kontrolliert zu verändern. Allerdings gibt man dort dann immer direkt den neuen gewünschten Wert an. Hier ein Beispiel für unsere Klasse Auto:

# Datei Auto.py
class Auto:

    def __init__(self, baujahr, marke, kilometerstand, automatik, preis):
        self.__baujahr = baujahr
        self.__marke = marke
        self.__kilometerstand = kilometerstand
        self.__automatik = automatik
        self.__preis = preis

    def mache_probefahrt(self, kilometer):
        if kilometer >= 0:
            self.__kilometerstand = self.__kilometerstand + kilometer
        else:
            print("Betrugsversuch!")

    def aendere_preis_um(self, aenderung):
        if self.__preis+aenderung > 0:
            self.__preis = self.__preis + aenderung
        else:
            print("Die Änderung ist nicht zulässig!")

    def __str__(self):
        return str(self.__baujahr) + " " + str(self.__kilometerstand)

    def get_kilometerstand(self):
        return self.__kilometerstand

    def set_kilometerstand(self, stand):
        if stand > self.__kilometerstand:
            self.__kilometerstand = stand

Eine Anwendung sieht dann so aus:

# Datei Auto_Test.py
from Auto import *

mein_erstes_auto = Auto(1985, "DeLorean", 54000, False, 60000)

print(mein_erstes_auto.get_kilometerstand()) # Ausgabe: 54000

mein_erstes_auto.set_kilometerstand(55000)
print(mein_erstes_auto.get_kilometerstand()) # Ausgabe: 55000

Properties

Im Grunde könnten wir nun so mit unseren privaten Attributen und den zugehörigen Gettern und Settern arbeiten. In Python ist es jedoch üblich, die Arbeit ein wenig komfortabler zu gestalten, damit man von außen doch wieder auf die Verwendung von get bzw. set verzichten kann, obwohl innerhalb einer Klasse mit diesen gearbeitet wird.

Dazu verwendet man Properties, hier ganz unten für unser Attribut kilometerstand ergänzt:

# Datei Auto.py
class Auto:

    def __init__(self, baujahr, marke, kilometerstand, automatik, preis):
        self.__baujahr = baujahr
        self.__marke = marke
        self.__kilometerstand = kilometerstand
        self.__automatik = automatik
        self.__preis = preis

    def mache_probefahrt(self, kilometer):
        if kilometer >= 0:
            self.__kilometerstand = self.__kilometerstand + kilometer
        else:
            print("Betrugsversuch!")

    def aendere_preis_um(self, aenderung):
        if self.__preis+aenderung > 0:
            self.__preis = self.__preis + aenderung
        else:
            print("Die Änderung ist nicht zulässig!")

    def __str__(self):
        return str(self.__baujahr) + " " + str(self.__kilometerstand)

    def get_kilometerstand(self):
        return self.__kilometerstand

    def set_kilometerstand(self, stand):
        if stand > self.__kilometerstand:
            self.__kilometerstand = stand

    kilometerstand = property(get_kilometerstand, set_kilometerstand)

Nun kann man von außen scheinbar ohne Umweg auf das Attribut zugreifen. In Wahrheit erden dabei aber der Getter bzw. Setter aufgerufen:

# Datei Auto_Test.py
from Auto import *

mein_erstes_auto = Auto(1985, "DeLorean", 54000, False, 60000)

print(mein_erstes_auto.kilometerstand) # Ausgabe: 54000

mein_erstes_auto.kilometerstand= 55000
print(mein_erstes_auto.kilometerstand) # Ausgabe: 55000

mein_erstes_auto.kilometerstand= 50000
print(mein_erstes_auto.kilometerstand) # Ausgabe: 55000

Der Stand wird nicht wider auf 50000 verringert, woran man sieht, dass tatsächlich der Setter verwendet wurde.

In dieser Version könnten wir nun entweder direkt den Attributnamen verwenden oder aber auch Getter und Setter aufrufen. In Python möchte man aber immer gerne nur einen eindeutigen Weg zur Verfügung stellen, um eine bestimmte Aktion durchzuführen. Daher sollten wir nun in einem letzten Schritt noch Getter und Setter zu privaten Methoden machen:

# Datei Auto.py
class Auto:

    def __init__(self, baujahr, marke, kilometerstand, automatik, preis):
        self.__baujahr = baujahr
        self.__marke = marke
        self.__kilometerstand = kilometerstand
        self.__automatik = automatik
        self.__preis = preis

    def mache_probefahrt(self, kilometer):
        if kilometer >= 0:
            self.__kilometerstand = self.__kilometerstand + kilometer
        else:
            print("Betrugsversuch!")

    def aendere_preis_um(self, aenderung):
        if self.__preis+aenderung > 0:
            self.__preis = self.__preis + aenderung
        else:
            print("Die Änderung ist nicht zulässig!")

    def __str__(self):
        return str(self.__baujahr) + " " + str(self.__kilometerstand)

    def __get_kilometerstand(self):
        return self.__kilometerstand

    def __set_kilometerstand(self, stand):
        if stand > self.__kilometerstand:
            self.__kilometerstand = stand

    kilometerstand = property(__get_kilometerstand, __set_kilometerstand)

Mit den übrigen Attributen sollten wir nun analog verfahren. Das bietet sich hier als kleine Übung an ;)