Zum Inhalt springen

4. Klassen in Python

Zu Zen-Modus wechseln

Mit Funktionen, Listen und Dictionaries lassen sich bereits viele Programme schreiben. Sobald aber zusammengehörige Daten und die dazu passenden Operationen gemeinsam verwaltet werden sollen, stoßen diese Mittel an Grenzen: Ein Bankkonto besteht aus Inhaber und Kontostand, und es gibt klar definierte Operationen darauf (einzahlen, abheben, Stand abfragen), die bestimmte Regeln einhalten müssen. Genau dafür gibt es Klassen, das Kernstück der objektorientierten Programmierung.

Dieses Kapitel führt die Python-Syntax für Klassen ein und behandelt die Konzepte, die alle folgenden Kapitel voraussetzen: Konstruktor, Methoden, Kapselung, Properties und die Unterscheidung von Klassen- und Instanzattributen. Vererbung und Polymorphismus, die eigentlichen Schwerpunkte dieses Jahres, bauen unmittelbar darauf auf.

Eine Klasse ist ein Bauplan: Sie legt fest, welche Daten (Attribute) und welche Operationen (Methoden) eine bestimmte Art von Objekt hat. Ein Objekt (auch Instanz genannt) ist ein konkretes Exemplar nach diesem Bauplan, mit eigenen Werten.

Die Klasse Account beschreibt, dass jedes Konto einen Inhaber und einen Stand hat und dass man einzahlen kann. Das Objekt account_ada ist dann ein konkretes Konto mit dem Inhaber “Ada” und dem Stand 100. Von einer Klasse können beliebig viele Objekte existieren, jedes mit eigenen Attributwerten.

class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount > 0:
self.balance += amount
account_ada = Account("Ada", 100)
account_bob = Account("Bob")
account_ada.deposit(50)
print(account_ada.balance) # 150
print(account_bob.balance) # 0

Die Bestandteile im Einzelnen:

  • class Account: eröffnet die Klassendefinition; alles Eingerückte gehört dazu. Klassennamen werden in PascalCase geschrieben.
  • __init__ ist der Konstruktor (gesprochen “dunder init”, von double underscore). Er wird automatisch ausgeführt, wenn ein Objekt erzeugt wird, und legt dessen Attribute an.
  • Objekte entstehen durch Aufruf der Klasse wie eine Funktion: Account("Ada", 100). Ein eigenes Schlüsselwort wie new gibt es nicht.
  • Attribute werden nicht vorab deklariert; sie entstehen durch die erste Zuweisung in __init__.
  • Methoden werden über die Punktnotation auf einem Objekt aufgerufen: account_ada.deposit(50).

Jede Methode hat als ersten Parameter self: das Objekt, auf dem die Methode aufgerufen wurde. Python übergibt es beim Aufruf automatisch:

account_ada.deposit(50)
# is internally equivalent to:
Account.deposit(account_ada, 50)

Beide Zeilen sind gleichwertig; die zweite zeigt den Mechanismus. Daraus folgen zwei Regeln, deren Verletzung die häufigsten Anfängerfehler verursacht:

  1. Jede Methode beginnt mit self. Fehlt der Parameter, meldet Python beim Aufruf deposit() takes 1 positional argument but 2 were given. Diese Meldung bedeutet übersetzt: self wurde in der Definition vergessen.
  2. Attributzugriff läuft über self. Innerhalb einer Methode ist balance eine gewöhnliche lokale Variable; das Attribut des Objekts heißt self.balance. Wer das self. weglässt, arbeitet unbemerkt mit einer lokalen Variablen, und das Objekt bleibt unverändert.

Im Beispiel oben ist balance von außen frei zugänglich. Nichts hindert einen Aufrufer an der Zeile account_ada.balance = -1000000, und die Prüfung in deposit ist damit wertlos. Kapselung bedeutet, den inneren Zustand eines Objekts vor unkontrolliertem Zugriff zu schützen: Änderungen laufen ausschließlich über Methoden, die die fachlichen Regeln durchsetzen.

Python setzt Kapselung über Namenskonventionen um, nicht über Zugriffsverbote des Compilers:

  • Ein führender Unterstrich (_balance) markiert ein Attribut als intern: Zugriff von außerhalb der Klasse gilt als Verstoß gegen die Schnittstelle. Technisch bleibt er möglich; die Konvention ist aber in der gesamten Python-Welt etabliert und wird von Werkzeugen (Linter, IDE) geprüft.
  • Zwei führende Unterstriche (__balance) aktivieren zusätzlich das Name Mangling: Python benennt das Attribut nach außen in _Account__balance um. Das dient vor allem der Vermeidung von Namenskollisionen in Klassenhierarchien und wird im Alltag selten benötigt. Der einfache Unterstrich ist der Normalfall.

Properties: kontrollierter Zugriff in Attributschreibweise

Abschnitt betitelt „Properties: kontrollierter Zugriff in Attributschreibweise“

Für den lesenden und schreibenden Zugriff auf geschützte Attribute bietet Python Properties: Methoden, die sich nach außen wie Attribute verhalten.

class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, new_balance):
if new_balance < 0:
print("A negative balance is not allowed.")
return
self._balance = new_balance
account = Account("Ada", 100)
print(account.balance) # 100, read through the property
account.balance = 500 # runs through the setter, including validation
account.balance = -50 # rejected by the setter
print(account.balance) # 500

Die Zeile @property über der Methode ist ein Dekorator: eine Markierung, die das Verhalten der folgenden Funktion verändert. Die interne Funktionsweise von Dekoratoren ist hier nicht Thema; das Rezept genügt. Der Gewinn: Der Aufrufer schreibt schlicht account.balance, ohne Methodenklammern, und trotzdem behält die Klasse die Kontrolle über jeden Zugriff. Eine Property ohne Setter macht ein Attribut von außen praktisch schreibgeschützt.

Die Reaktion auf ungültige Werte mit print ist eine Übergangslösung; die saubere Behandlung solcher Fälle mit Exceptions folgt in Kapitel 7.

Der Versuch, ein Objekt direkt auszugeben, liefert standardmäßig wenig Brauchbares:

>>> print(account)
<__main__.Account object at 0x7f3a2c1d5e50>

Die Methode __str__ legt fest, wie sich ein Objekt als Text darstellt; __repr__ liefert eine eindeutige Entwicklerdarstellung, idealerweise als nachvollziehbarer Python-Ausdruck:

class Account:
# ... as above ...
def __str__(self):
return f"account of {self.owner}: {self._balance:.2f} EUR"
def __repr__(self):
return f"Account({self.owner!r}, {self._balance})"
>>> print(account)
account of Ada: 500.00 EUR
>>> account
Account('Ada', 500)

__str__ wird von print und f-Strings verwendet und richtet sich an Benutzer; __repr__ wird von der REPL, von Debuggern und bei der Ausgabe von Listen verwendet und richtet sich an Entwickler. Als Arbeitsregel für dieses Jahr: Jede Klasse bekommt mindestens ein __str__. Der Aufwand liegt bei zwei Zeilen, der Nutzen zeigt sich bei jeder Fehlersuche.

Methoden mit doppelten Unterstrichen auf beiden Seiten heißen Dunder-Methoden. Über sie klinken sich eigene Klassen in die Mechanismen der Sprache ein: __init__ in die Objekterzeugung, __str__ in die Textausgabe, __len__ in die Funktion len(), __eq__ in den Vergleich mit ==. Weitere Dunder-Methoden folgen bei Bedarf in späteren Kapiteln.

Attribute, die in __init__ über self gesetzt werden, gehören dem einzelnen Objekt (Instanzattribute). Variablen, die direkt im Klassenkörper stehen, gehören der Klasse und werden von allen Objekten geteilt (Klassenattribute):

class Account:
interest_rate = 0.02 # class attribute: shared by all accounts
def __init__(self, owner):
self.owner = owner # instance attribute: per object

Für gemeinsame Konstanten wie den Zinssatz ist ein Klassenattribut angemessen. Problematisch wird es bei veränderlichen Werten:

class Playlist:
tracks = [] # WRONG: one list shared by ALL playlists
def add(self, song):
self.tracks.append(song)
rock = Playlist()
jazz = Playlist()
rock.add("Paranoid")
print(jazz.tracks) # ['Paranoid']

Beide Playlists teilen dieselbe Liste, weil sie an der Klasse hängt statt am Objekt. Korrekt wird die Liste im Konstruktor angelegt; dann erhält jedes Objekt seine eigene:

class Playlist:
def __init__(self):
self.tracks = []

Arbeitsregel: Veränderliche Attribute (Listen, Dictionaries) immer in __init__ anlegen. Klassenattribute nur für Werte, die tatsächlich allen Exemplaren gemeinsam sind.

Gewöhnliche Methoden erhalten mit self das Objekt, auf dem sie aufgerufen werden, und arbeiten mit dessen Instanzattributen. Es gibt aber Operationen, die zur Klasse gehören, ohne ein einzelnes Objekt zu betreffen. Für sie bietet Python zwei Dekoratoren.

Eine statische Methode bekommt weder self noch die Klasse übergeben. Sie ist eine gewöhnliche Funktion, die man thematisch in der Klasse ablegt, weil sie dort inhaltlich hingehört. Typisch sind Hilfsberechnungen und Prüfungen, die keinen Objektzustand brauchen:

class Account:
@staticmethod
def is_valid_iban(iban):
cleaned = iban.replace(" ", "")
return len(cleaned) == 20 and cleaned[:2].isalpha()
print(Account.is_valid_iban("AT12 3456 7890 1234 56")) # True

Der Aufruf erfolgt über die Klasse (Account.is_valid_iban(...)), ein Objekt ist nicht nötig. Ohne den Dekorator würde Python das erste Argument als self deuten und beim Aufruf über die Klasse einen Fehler melden.

Eine Klassenmethode bekommt als ersten Parameter nicht das Objekt, sondern die Klasse selbst; sie heißt konventionell cls. Damit kann die Methode auf Klassenattribute zugreifen und, ihr wichtigster Einsatz, alternative Konstruktoren anbieten: Fabrikmethoden, die ein Objekt aus einer anderen Datenquelle erzeugen.

class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
@classmethod
def from_dict(cls, data):
return cls(data["owner"], data.get("balance", 0))
record = {"owner": "Ada", "balance": 100}
account = Account.from_dict(record) # cls(...) creates an Account
print(account.owner) # Ada

cls(...) ruft den Konstruktor der Klasse auf, auf der die Methode aufgerufen wurde. Das ist bewusst nicht als Account(...) fest verdrahtet: In einer Klassenhierarchie erzeugt SavingsAccount.from_dict(...) dadurch automatisch ein SavingsAccount. Vererbung folgt im nächsten Kapitel; die Klassenmethode ist hier der saubere Weg, mehrere Erzeugungswege für dieselbe Klasse anzubieten, ohne den Konstruktor zu überladen.

Die drei methodengebundenen Dekoratoren der Standardsprache sind damit vollständig:

  • @property (oben) für lesenden Zugriff in Attributschreibweise,
  • @staticmethod für Funktionen ohne Objekt- oder Klassenbezug,
  • @classmethod für Methoden, die die Klasse statt des Objekts erhalten.

Als Faustregel: self verwendet, wer Instanzdaten braucht; cls, wer die Klasse braucht (meist für alternative Konstruktoren); keinen der beiden, wenn eine reine Hilfsfunktion sinnvoll in der Klasse aufgehoben ist, dann @staticmethod.

Die folgende Klasse aus dem Videoschnitt-Umfeld kombiniert alle Elemente des Kapitels und dient in den nächsten Kapiteln als Ausgangspunkt:

class Clip:
"""A video clip in an editing project."""
fps = 25 # project standard for all clips
def __init__(self, name, duration_frames):
self.name = name
self._duration_frames = duration_frames
@classmethod
def from_seconds(cls, name, seconds):
"""Create a clip from a duration given in seconds."""
return cls(name, round(seconds * cls.fps))
@staticmethod
def frames_to_seconds(frames):
return frames / Clip.fps
@property
def duration_seconds(self):
return self._duration_frames / Clip.fps
def trim(self, frames):
"""Cut the given number of frames off the end."""
if 0 < frames < self._duration_frames:
self._duration_frames -= frames
def __str__(self):
return f"{self.name} ({self.duration_seconds:.1f} s)"
intro = Clip("intro", 250)
intro.trim(50)
print(intro) # intro (8.0 s)
outro = Clip.from_seconds("outro", 4) # alternative constructor
print(outro) # outro (4.0 s)
print(Clip.frames_to_seconds(100)) # 4.0, no object needed

Zu beachten: ein Klassenattribut für den Projektstandard, ein geschütztes Instanzattribut, eine Property für die abgeleitete Größe, eine Klassenmethode als alternativer Konstruktor, eine statische Hilfsmethode, eine Methode mit Plausibilitätsprüfung, __str__ sowie Docstrings für Klasse und Methoden. Das ist der Standard, den eigene Klassen ab jetzt erfüllen sollen.

KonzeptPython-Schreibweise
Klassendefinitionclass Account:
Konstruktordef __init__(self, ...):
Objekt erzeugenAccount("Ada", 100)
Methodedef deposit(self, amount):
Objektbezugself, expliziter erster Parameter
Internes Attributself._balance (Konvention)
Lesender Zugriff@property
Schreibender Zugriff@balance.setter
Methode ohne Objektbezug@staticmethod
Methode auf der Klasse@classmethod (erster Parameter cls)
Textdarstellung__str__, __repr__
Geteilter WertKlassenattribut im Klassenkörper

Lernergebnisse: Was Sie nach diesem Kapitel können sollten

Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“

Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:

  • Erklären: den Unterschied zwischen Klasse und Objekt sowie die Rolle von __init__ und self erklären.
  • Anwenden: Klassen mit Konstruktor, Methoden und Attributen schreiben und Objekte davon erzeugen und verwenden.
  • Anwenden: Kapselung mit der Unterstrich-Konvention und Properties (@property, Setter mit Validierung) umsetzen.
  • Erklären: den Unterschied zwischen Klassen- und Instanzattributen erklären und die Falle geteilter veränderlicher Klassenattribute vermeiden.
  • Anwenden: statische Methoden (@staticmethod) und Klassenmethoden (@classmethod, alternative Konstruktoren) sachgerecht einsetzen und von Instanzmethoden abgrenzen.
  • Anwenden: Objekte mit __str__ und __repr__ lesbar darstellen.
  • Aufgabe 08 - Konto mit Klasse