4. Klassen in Python
Klassen in Python
Abschnitt betitelt „Klassen in Python“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.
Klasse und Objekt
Abschnitt betitelt „Klasse und Objekt“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) # 150print(account_bob.balance) # 0Die Bestandteile im Einzelnen:
class Account:eröffnet die Klassendefinition; alles Eingerückte gehört dazu. Klassennamen werden inPascalCasegeschrieben.__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 wienewgibt 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).
self: der Bezug auf das eigene Objekt
Abschnitt betitelt „self: der Bezug auf das eigene Objekt“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:
- Jede Methode beginnt mit
self. Fehlt der Parameter, meldet Python beim Aufrufdeposit() takes 1 positional argument but 2 were given. Diese Meldung bedeutet übersetzt:selfwurde in der Definition vergessen. - Attributzugriff läuft über
self. Innerhalb einer Methode istbalanceeine gewöhnliche lokale Variable; das Attribut des Objekts heißtself.balance. Wer dasself.weglässt, arbeitet unbemerkt mit einer lokalen Variablen, und das Objekt bleibt unverändert.
Kapselung
Abschnitt betitelt „Kapselung“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__balanceum. 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 propertyaccount.balance = 500 # runs through the setter, including validationaccount.balance = -50 # rejected by the setterprint(account.balance) # 500Die 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.
Lesbare Objekte: __str__ und __repr__
Abschnitt betitelt „Lesbare Objekte: __str__ und __repr__“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>>> accountAccount('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.
Klassenattribute und Instanzattribute
Abschnitt betitelt „Klassenattribute und Instanzattribute“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 objectFü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.
Statische Methoden und Klassenmethoden
Abschnitt betitelt „Statische Methoden und Klassenmethoden“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.
@staticmethod: Funktion im Namensraum der Klasse
Abschnitt betitelt „@staticmethod: Funktion im Namensraum der Klasse“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")) # TrueDer 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.
@classmethod: Methode auf der Klasse
Abschnitt betitelt „@classmethod: Methode auf der Klasse“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 Accountprint(account.owner) # Adacls(...) 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.
Was es sonst noch gibt
Abschnitt betitelt „Was es sonst noch gibt“Die drei methodengebundenen Dekoratoren der Standardsprache sind damit vollständig:
@property(oben) für lesenden Zugriff in Attributschreibweise,@staticmethodfür Funktionen ohne Objekt- oder Klassenbezug,@classmethodfü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.
Ein vollständiges Beispiel
Abschnitt betitelt „Ein vollständiges Beispiel“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 constructorprint(outro) # outro (4.0 s)print(Clip.frames_to_seconds(100)) # 4.0, no object neededZu 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.
Zusammenfassung der Syntax
Abschnitt betitelt „Zusammenfassung der Syntax“| Konzept | Python-Schreibweise |
|---|---|
| Klassendefinition | class Account: |
| Konstruktor | def __init__(self, ...): |
| Objekt erzeugen | Account("Ada", 100) |
| Methode | def deposit(self, amount): |
| Objektbezug | self, expliziter erster Parameter |
| Internes Attribut | self._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 Wert | Klassenattribut 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__undselferklä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.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 08 - Konto mit Klasse