Kapselung
Kurz gesagt: Kapselung bedeutet, dass die Daten eines Objekts nur über seine Methoden geändert werden: in Python mit der Abmachung _name und mit Properties über @property.
Problem
Minus 500 €
In der letzten Lektion haben wir die Prüfung beim Abheben in eine Methode gesteckt. Das Feld ist aber offen, und jeder kann direkt hineinschreiben:
account = Account("Maria", 100)account.withdraw(500)account.balance = -500print(account.balance)-500Die Methode hat das Abheben von 500 € richtig abgelehnt. Die nächste Zeile hat den Kontostand einfach von Hand gesetzt. Die Prüfung ist da, aber man kann sie umgehen. Wir brauchen eine Möglichkeit zu sagen: „Nur das Konto fasst den Kontostand an.“
Die Idee
_balance: intern für die Klasse
Schreib einen Unterstrich vor den Namen des Feldes: _balance.
class Account: def __init__(self, owner, balance): self._owner = owner self._balance = balance
# ...Python hat kein private. Der Unterstrich ist eine Abmachung unter Programmierern: „Das ist intern für die Klasse, von außen nicht anfassen.“ Python hält niemanden auf. Die Zeile
account._balance = -500läuft, und der Kontostand wird -500. Aber jeder, der sie liest, sieht, dass die Regel gebrochen ist (PyCharm zum Beispiel unterstreicht sie). Code außerhalb der Klasse benutzt nur die Methoden der Klasse.
Die Klasse versteckt ihre Daten und zeigt sie nur über ihre eigenen Methoden. Das nennt man Kapselung. Java und C# haben dafür Schlüsselwörter, die Zugriffsmodifizierer (private, public). In Python übernimmt der Unterstrich diese Rolle.
Genauer, für Neugierige
- Mit zwei Unterstrichen (
self.__balance) benennt Python das Feld in_Account__balanceum. Von außen gibt esaccount.__balancenicht mehr, aberaccount._Account__balancekommt trotzdem hin. Das schützt vor einem zufälligen Namenskonflikt in Unterklassen, nicht davor, dass jemand das Feld absichtlich anfasst. Meist reicht ein Unterstrich. - Die Abmachung gilt auch für Methoden:
_check()ist eine Methode, die nur für die Klasse selbst gedacht ist.
Die Idee
Lesen, ohne anzufassen
Von außen sollten wir den Kontostand jetzt nicht einmal lesen. Deshalb gibt die Klasse eine Methode, die ihn nur zurückgibt:
class Account: def __init__(self, owner, balance): self._owner = owner self._balance = balance
@property def owner(self): return self._owner
@property def balance(self): return self._balance
def deposit(self, amount): if amount > 0: self._balance += amount
def withdraw(self, amount): if amount > 0 and amount <= self._balance: self._balance -= amount return True return False
account = Account("Maria", 100)account.withdraw(30)account.deposit(50)print(f"{account.owner}: {account.balance} €")Maria: 120 €balance ist eine Property. @property über der Methode bedeutet, dass du sie von außen wie ein Feld liest: account.balance, ohne Klammern. Innen gibt sie den Wert von _balance zurück. Das ist der Getter in Python. Der Kontostand ändert sich nur über deposit und withdraw, und die prüfen.
In eine Property ohne Setter kann man nicht schreiben. Die Zeile account.balance = -500 bricht ab mit:
AttributeError: property 'balance' of 'Account' object has no setterwithdraw gibt jetzt True oder False zurück: ob das Abheben geklappt hat. So weiß jeder, der abhebt, ob es funktioniert hat.
account = Account("Maria", 100)account.withdraw(500)account.deposit(50)print(account.balance)Schritt für Schritt, in Worten
- _balance ist intern: Nach der Abmachung fasst Code von außen das Feld nicht an. Er geht über die Methoden.
- Das Abheben geht durch die Prüfung in der Methode: 500 ist mehr als 100. Abgelehnt, und der Kontostand bleibt, wie er ist.
- Auch die Einzahlung wird geprüft: 50 ist ein positiver Betrag. Die Methode addiert ihn.
- Die Property balance gibt eine Kopie des Kontostands heraus. Das Feld _balance selbst bleibt drinnen.
Probier es aus
Abgelehnt
Was gibt das Programm aus?
account = Account("Maria", 100)print(account.withdraw(150))account.deposit(-20)print(account.balance)150 € sind mehr als der Kontostand, also gibt withdraw False zurück und lässt den Kontostand in Ruhe. Die Einzahlung von -20 € wird ebenfalls abgelehnt, weil der Betrag positiv sein muss. Der Kontostand bleibt 100.
Die Idee
Setter: eine Änderung mit Prüfung
Manchmal muss Code von außen ein Feld ändern, aber nicht auf jeden beliebigen Wert. Das Alter eines Tieres kann sich ändern, aber es kann nicht negativ sein:
class Animal: def __init__(self): self._age = 0
@property def age(self): return self._age
@age.setter def age(self, value): if value >= 0: self._age = value
cat = Animal()cat.age = 4cat.age = -3print(cat.age)4@age.setter macht einen Setter für die Property age: eine Methode mit demselben Namen, die bei cat.age = -3 aufgerufen wird. value ist der Wert rechts von =, hier -3. Die Prüfung schlägt fehl, und das Alter bleibt 4. Von außen sieht das Schreiben aus wie das Schreiben in ein einfaches Feld, aber es geht durch die Prüfung.
Die Idee
Welches Feld bekommt einen Setter?
Nicht jedes Feld braucht einen Setter, und manche brauchen nicht einmal einen Getter. Überleg, was Code von außen mit dem Produkt tun können soll:
- Name und Preis werden beim Erzeugen des Produkts gesetzt und danach nur gelesen: nur ein Getter;
- die Stückzahl im Warenkorb ändert sich, ist aber mindestens 1: ein Getter und ein Setter mit Prüfung.
class Product: def __init__(self, name, price): self._name = name self._price = price self._quantity = 1
@property def name(self): return self._name
@property def price(self): return self._price
@property def quantity(self): return self._quantity
@quantity.setter def quantity(self, value): if value >= 1: self._quantity = value
coffee = Product("Kaffee", 7.50)coffee.quantity = 3coffee.quantity = 0print(f"{coffee.name} x{coffee.quantity}")Kaffee x3Die Regel lautet: Fang mit _ für alles an und öffne nur, was du brauchst. Ein Setter, der ohne Prüfung alles speichert, bringt das Problem aus der ersten Karte zurück.
Probier es aus
Was fehlt?
class Account:
def __init__(self, balance):
self. = balance_balance: Der Unterstrich sagt „intern für die Klasse, von außen nicht anfassen“.Probier es aus
Finde den Fehler
Das Feld ist _balance, und trotzdem wird der Kontostand -500:
account = Account()account.balance = 100account.withdraw(30)account.balance = -500print(account.balance)-500Welche Zeile in der Klasse bricht den Schutz?
Wähle die Zeile mit dem Fehler.
Der Setter unter @balance.setter speichert jeden Wert ohne Prüfung. Das Feld ist intern, aber der Setter öffnet es wieder. Entferne ihn und setz den Startbetrag in __init__. Danach ändert er sich nur über deposit und withdraw.
Übung
Versteckte Daten
Löse die Aufgaben in deinem Editor. Alle Felder beginnen mit einem Unterstrich.
1. Person. Person mit Name und Alter, ein Konstruktor, die schreibgeschützten Properties name und age und introduce().
Maria ist 34 Jahre alt.Hallo, ich bin Maria und bin 34 Jahre alt.2. Ein Konto ohne Minus. Account mit Inhaber und Kontostand. Eine Einzahlung nimmt nur einen positiven Betrag an. Eine Abhebung gibt zurück, ob sie geklappt hat, und lässt den Kontostand nie unter 0 fallen. Code von außen kann den Kontostand lesen, aber nicht schreiben.
account = Account("Peter", 50)account.deposit(25)account.deposit(-10)print(account.withdraw(100))print(account.withdraw(40))print(f"Kontostand: {account.balance} €")FalseTrueKontostand: 35 €3. Tier. Animal mit Art und Alter. Die Art wird im Konstruktor gesetzt und danach nur gelesen. Das Alter hat einen Setter (@age.setter), der eine negative Zahl ablehnt und eine Meldung ausgibt. Auch der Konstruktor geht durch dieselbe Prüfung: Schreib in __init__ self.age = age, ohne Unterstrich.
cat = Animal("Katze", 3)cat.age = 4cat.age = -1print(f"{cat.species}, {cat.age} Jahre")Ungültiges Alter: -1Katze, 4 JahreWeitere Aufgaben
- Note. Eine Klasse
Grademit dem Namen eines Fachs und einer Note von 2 bis 6. Der Setter lehnt alles außerhalb dieser Grenzen ab. Probier es mit 5, 7 und 1. - Produkt mit Rabatt. Gib dem
Productaus der Lektion eine Methode, die einen Rabatt in Prozent anwendet und nur 1 bis 90% annimmt. Der Preis hat keinen Setter.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Parameter und return · Klassen und Objekte · Konstruktoren und self · Instanzmethoden
- Geht weiter in: Was alle Objekte teilen: Klassenattribute · Vererbung · Polymorphie · Abstrakte Klassen
- Aufgaben zum Thema: Klassen und Objekte