CSBootcampHSG

Lektion 05

Klassen und Objektorientierung

Klassen schreiben, `__init__`, Methoden, `self`, Vererbung mit `super()`, `__str__` vs `__repr__` und der `@property`-Decorator. Damit fällst du in Quiz 05 nicht über die typischen OOP-Fallen.

Dauer:
60 Min. geschätzt
Voraussetzungen:
Funktionen (Lektion 03) · Listen, Dicts, Sets (Lektion 04)
Architektur-Blueprint einer Klasse 'Student' mit __init__, Attributen und Methoden — daneben vier kleinere Karten als konkrete Instanzen

Theorie & Konzepte

Phase 1 / 4

Bisher hast du Daten in Variablen, Listen und Dicts abgelegt — und Funktionen darauf operieren lassen. Mit Klassen legst du Daten und die Funktionen, die mit ihnen arbeiten, gemeinsam in einer Einheit ab. Das ist Objektorientierung.

Das ist mehr als nur Aufräumen. Es macht zwei Sachen leichter:

  1. Wiederverwendbarkeit. Ein Person-Klasse beschreibt einmal, was eine Person ist — du erzeugst hundert Instanzen davon, ohne den Code zu duplizieren.
  2. Polymorphismus. Eine Superhero-Klasse kann von Person erben und nur die Unterschiede beschreiben. Der Code, der mit Persons arbeitet, funktioniert auch mit Superheros.

Quiz 05 prüft genau die fünf Stolpersteine, die HSG bei dieser Umstellung in den Vordergrund rückt: self, __init__, Vererbung, __str__ vs. __repr__, und der @property-Decorator. Diese Lektion isoliert sie.

Eine Klasse definieren

Eine Klasse ist die Vorlage. Eine Instanz ist eine konkrete Ausprägung dieser Vorlage. Schau dir das Beispiel an:

class Person:
    def __init__(self, name, year_of_birth):
        self.name = name
        self.year_of_birth = year_of_birth

    def compute_age(self):
        return 2026 - self.year_of_birth

# Instanzen erzeugen — die Klasse wie eine Funktion aufrufen:
guido = Person("Guido van Rossum", 1956)
linus = Person("Linus Torvalds", 1969)

guido.name              # 'Guido van Rossum'
guido.compute_age()     # 70
linus.compute_age()     # 57

Drei Vokabeln, die du verinnerlichen solltest:

  • Klasse (Person) — die Definition. Das Rezept.
  • Instanz (guido) — ein konkretes Objekt, das nach dem Rezept gebaut wurde.
  • Methode (compute_age) — eine Funktion, die zur Klasse gehört. Wird auf einer Instanz aufgerufen.

Klasse vs. Instanz — Bauplan und Exemplare

Klasse als Bauplan, mehrere Instanzen mit eigenen Attributwerten aber gemeinsam genutzten Methoden.KLASSEclass Person:def __init__(self,name, year):self.name = nameself.year = yeardef compute_age(self):return …Bauplan: einmalig definiertPerson(...)guido.name, .year = "Guido", 1956linus.name, .year = "Linus", 1969ada.name, .year = "Ada", 1815Drei Instanzen — eigene Attributwerte, dieselben Methoden aus der Klasse.
Die Klasse beschreibt einmal die Form. Jede Instanz hat ihre eigenen Attributwerte, teilt aber die Methoden mit der Klasse.

Cheat-Sheet: Klasse · Instanz · self

Cheat-Sheet: Klasse, Instanz und self — drei verbundene Karten, die zeigen wie aus dem Bauplan eine Instanz wird und wie self in Methoden ankommt.
Die drei OOP-Grundvokabeln, einmal sauber zusammengefasst.

Was ist `self`?

self ist die Instanz, auf der die Methode gerade läuft. Wenn du guido.compute_age() aufrufst, übersetzt Python das intern zu Person.compute_age(guido)guido landet als erstes Argument im Parameter self.

Das erklärt zwei Sachen, die Anfänger verwirren:

  1. Warum steht self als erster Parameter in der Methodendefinition? Damit die Methode weiß, auf welcher Instanz sie arbeitet. Ohne Parameter könnte sie nicht auf self.name zugreifen.
  2. Warum schreibt man self nicht beim Aufruf? guido.compute_age() reicht — Python füllt self automatisch aus. Würdest du Person.compute_age(guido) schreiben (geht!), kommt guido direkt explizit rein.

Funktion vs. Methode

Beide rechnen das Alter aus. Die Methode bündelt Daten + Verhalten in einer Einheit.

Funktion (frei)

def compute_age(year_of_birth):
    return 2026 - year_of_birth

compute_age(1956)   # 70

Neutral — du musst die Daten extra rein-reichen. Ok für einmalige Berechnungen.

Methode (gebunden)

class Person:
    def __init__(self, name, year_of_birth):
        self.year_of_birth = year_of_birth
    def compute_age(self):
        return 2026 - self.year_of_birth

Person("Guido", 1956).compute_age()   # 70

Daten und Verhalten gehören zusammen. Das ganze Objekt ist die Schnittstelle.

Der Konstruktor `__init__`

__init__ ist die spezielle Methode, die Python aufruft, wenn du eine neue Instanz erzeugst. Sie initialisiert die Attribute. Die doppelten Unterstriche ("dunder") signalisieren: das ist Python-interne Magie, du redest hier mit dem Sprachkern.

class Book:
    def __init__(self, title, author, pages=200):
        self.title = title
        self.author = author
        self.pages = pages

# Aufruf:
b = Book("Dune", "Herbert")           # pages=200 per Default
b2 = Book("Solaris", "Lem", pages=350)

Drei Eigenschaften:

  • __init__ gibt kein explizites Wert zurück — return self würde sogar einen Fehler werfen. Die neue Instanz wird implizit zurückgegeben.
  • Defaults und Keyword-Arguments funktionieren genau wie bei normalen Funktionen (siehe Lektion 03).
  • Du kannst auch Berechnungen im __init__ machen — self.created_at = datetime.now() ist häufig.

Vererbung mit `super()`

Eine Subklasse erbt von einer Superklasse. Sie übernimmt deren Methoden und Attribute, kann sie erweitern (mehr hinzufügen) und überschreiben (Verhalten ändern).

class Person:
    def __init__(self, name, year_of_birth):
        self.name = name
        self.year_of_birth = year_of_birth
    def compute_age(self):
        return 2026 - self.year_of_birth
    def __str__(self):
        return f"{self.name} ({self.compute_age()})"

class Superhero(Person):                    # ← erbt von Person
    def __init__(self, name, year_of_birth, alias, powerlevel):
        super().__init__(name, year_of_birth)   # ← Konstruktor der Superklasse aufrufen!
        self.alias = alias
        self.powerlevel = powerlevel

    def __str__(self):                        # ← überschreibt Person.__str__
        return f"{self.alias}: {self.powerlevel}"

hulk = Superhero("Bruce Banner", 1969, "Hulk", 600)
str(hulk)                # 'Hulk: 600'           — eigene __str__-Methode
hulk.compute_age()       # 57                     — Methode von Person geerbt
hulk.name                # 'Bruce Banner'         — Attribut von Person geerbt

super() greift auf die Superklasse zu — meist im __init__, um die geerbten Attribute korrekt zu initialisieren. Vergisst du das, fehlen die geerbten Attribute auf deiner Subklassen-Instanz.

Vererbungskette und `super()`

Vererbungskette: object ← Person ← Superhero. super() ruft die Methode der direkten Superklasse auf.objectWurzel aller Klassenimplizitclass Person:def __init__(name, year)def compute_age()def __str__()erbtclass Superhero(Person):super().__init__(name, year) ← geerbtdef fight(enemy) ← neudef __str__() ← überschriebensuper() greift jeweils auf die direkt darüberliegende Klasse zu.
Subklasse erbt alles von Superklasse. super() ruft die Superklassen-Methode auf — typischerweise im __init__, damit geerbte Attribute initialisiert werden.

Cheat-Sheet: super().__init__ richtig benutzen

Cheat-Sheet zu Vererbung: zwei verschachtelte Klassen Person und Superhero(Person) mit super().__init__(name, year) als markierte erste Zeile.
Erste Zeile in der Subklassen-__init__ — sonst fehlen die geerbten Attribute.

Die Wurzel: `object`

In Python erbt jede Klasse — auch implizit — von der eingebauten Klasse object. Das heißt:

class A:
    pass

class B(A):
    pass

b = B()
isinstance(b, B)        # True
isinstance(b, A)        # True
isinstance(b, object)   # True — auch ohne dass es jemand explizit angibt

Wichtig fürs Quiz: Wenn dich jemand fragt "ist die Instanz einer Subklasse, die von einer Superklasse erbt, immer auch Instanz von object?" — die Antwort ist ja. Über die Vererbungskette landet jede Klasse irgendwann bei object.

Dunder-Methoden — `__str__` und `__repr__`

Python ruft bestimmte Dunder-Methoden automatisch auf, wenn du Operationen wie print(...), str(...) oder den REPL-Echo-Output benutzt:

  • __str__(self) → Was str(instance) und print(instance) zurückgeben.
  • __repr__(self) → Was der REPL und Debug-Tools anzeigen. Sollte eindeutig sein, idealerweise so, dass eval(repr(x)) == x gilt.
class Person:
    def __init__(self, name, year):
        self.name, self.year = name, year
    def __str__(self):
        return f"{self.name} ({2026 - self.year})"
    def __repr__(self):
        return f"Person({self.name!r}, {self.year})"

guido = Person("Guido", 1956)
str(guido)                  # 'Guido (70)'
print(guido)                # Guido (70)
repr(guido)                 # "Person('Guido', 1956)"
guido                       # → REPL zeigt: Person('Guido', 1956)

Fallback-Regeln:

  • Hat eine Klasse nur __repr__, aber kein __str__str(instance) benutzt __repr__.
  • Hat sie weder noch → str(instance) zeigt etwas wie <__main__.Person object at 0x...>.

`__str__` vs `__repr__` — wofür welches?

Beide bauen einen String aus dem Objekt. Der Zweck ist anders.

`__str__` — für Menschen

def __str__(self):
    return f"{self.name} ({self.age})"

Wird von print(...) und str(...) benutzt. Lesbar, freundlich, ohne technisches Drumherum. Beispiel: "Guido (70)".

`__repr__` — für Entwickler

def __repr__(self):
    return f"Person({self.name!r}, {self.year_of_birth})"

Wird vom REPL und vom Debugger benutzt. Eindeutig, möglichst so dass man die Instanz wieder herstellen könnte. Beispiel: "Person('Guido', 1956)".

Wer ruft was? — `print` vs. REPL

Welche Dunder-Methode greift wann: print/str → __str__, REPL/repr → __repr__, mit Fallback.Triggerprint(obj)ruft str(obj)str(obj)explizitREPL: objruft repr(obj)repr(obj)explizit__str__(self)lesbar, für Menschen__repr__(self)eindeutig, für Devswenn __str__ fehltobject.__repr__<obj at 0x…>print/str → __str__ (oder __repr__ als Fallback). REPL → __repr__ (oder object-Default als letzte Stufe).
print(x) → __str__. REPL-Echo, Listen, Debugger → __repr__. Bei fehlendem __str__ fällt str(x) auf __repr__ zurück.

Der `@property`-Decorator

Der @property-Decorator macht eine Methode aussehen wie ein Attribut. Statt obj.compute_age() schreibst du obj.age — aber hinter den Kulissen läuft trotzdem dein Berechnungs-Code:

class Person:
    def __init__(self, name, year_of_birth):
        self.name = name
        self._year = year_of_birth          # Konvention: _ prefix = "intern"

    @property
    def age(self):
        return 2026 - self._year

    @property
    def year_of_birth(self):
        return self._year

p = Person("Guido", 1956)
p.age                    # 70  — wirkt wie Attribut, ist aber Methodenaufruf
p.year_of_birth          # 1956
p.year_of_birth = 1957   # AttributeError: can't set attribute  ← read-only by default!

@property macht die Methode lesbar wie ein Attribut, aber nicht schreibbar. Wer auch Schreiben erlauben will, definiert zusätzlich einen @<name>.setter-Decorator. Im HSG-Stoff brauchst du nur das Lesen.

`@property` — Methode, die wie ein Attribut aussieht

@property: Methodenaufruf der wie ein Attribut aussieht. Lesen via property, Schreiben weiter via _internal.Klassendefinitionclass Person:def __init__(self, name, year):self._year = yearself.name = name@propertydef age(self):return 2026 - self._year◆ self._year intern, schreibbar◆ p.age property, READ-ONLYAufrufortenp.age→ ruft die Methode, gibt 70p.age = 80→ AttributeError (kein Setter)p._year = 1957→ schreibt direkt das interne AttributAm Aufrufort sieht p.age aus wie ein Attribut — ist aber ein Methodenaufruf.
Mit @property liest sich der Methodenaufruf ohne (). Schreiben ist standardmäßig nicht erlaubt — das macht das Attribut read-only.

Cheat-Sheet: `@property` auf einer Seite

Cheat-Sheet: @property. Definition links (mit @property-Decorator über der age-Methode), drei Verwendungsbeispiele rechts (lesen geht, schreiben über public Name geht nicht, schreiben des internen _year geht).
Lesen wirkt wie ein Attribut, Schreiben wirft AttributeError. Das interne `_name` bleibt frei.

Assignment-Walkthrough

Phase 2 / 4

Hier gehen wir typspezifisch durch die 4 Aufgabenformen von Assignment 05: was gefragt ist, wie man es angeht, und wo man sich typischerweise verrennt. Die konkreten Namen und Daten siehst du im HSG-Notebook — die Muster hier passen auf jede Variante.

Assignment-Aufgabe 1

Eine Klasse benutzen — Instanzen und Methoden

Was ist gefragt

Aufgabe 1.1 gibt dir eine fertig definierte Klasse (im HSG-Notebook: Person). Du sollst mehrere Instanzen in einer Liste ablegen — typisch ein Dutzend, jede mit einem eigenen Namen + Geburtsjahr.

Die Klasse hat normalerweise eine eigene __str__-Methode, also kannst du print(p) direkt aufrufen, ohne Attribute manuell zusammenzubauen.

Strategie

  1. Lies die Klassendefinition. Welche Argumente erwartet __init__?
  2. Liste anlegen — eine Variable, die du der Test-Zelle übergibst (Konvention: persons oder ähnlich).
  3. Pro Eintrag ein Person(...) mit den Konstruktor-Argumenten. Ein Eintrag pro Zeile, sauber lesbar.

Lösungsskelett

persons = [
    Person("Bucky Barnes", 1917),
    Person("Steve Rogers", 1918),
    # ... weitere
]

Typische Stolperstellen

  • Falsche Argumentreihenfolge. Sieh in __init__ nach, in welcher Reihenfolge name und year_of_birth stehen. Bei vielen Einträgen ist es leicht, einen zu vertauschen.
  • Person als String statt Instanz. Tests prüfen isinstance(p, Person)"Bucky Barnes" als String fällt durch.
  • Liste vs. Tupel. Die Tests erwarten typischerweise eine list. Das macht keinen Unterschied beim Iterieren, aber ein tuple(...) würde durchfallen.

Assignment-Aufgabe 2

Min/Max einer Liste von Instanzen

Was ist gefragt

Aufgabe 1.2 verlangt aus der Liste die älteste und jüngste Person zu finden — als konkrete Instanz (nicht nur den Namen).

Drei gleichwertige Lösungsformen.

Strategie

A) for-Schleife (am verständlichsten):

oldest, youngest = persons[0], persons[0]
for p in persons:
    if p.compute_age() > oldest.compute_age():
        oldest = p
    if p.compute_age() < youngest.compute_age():
        youngest = p

B) max/min mit key-Argument (kompakt):

oldest = max(persons, key=lambda p: p.compute_age())
youngest = min(persons, key=lambda p: p.compute_age())

C) sorted + Slicing (advanced, ein Liner):

youngest, oldest = sorted(persons, key=lambda p: p.compute_age())[::len(persons)-1]

Lösungsskelett

oldest = max(persons, key=lambda p: p.compute_age())
youngest = min(persons, key=lambda p: p.compute_age())

Typische Stolperstellen

  • Den key= vergessen. max(persons) ohne key ruft __lt__/__gt__ auf, was bei Person nicht definiert ist → TypeError.
  • Den Namen statt der Instanz speichern. oldest = p.name schlägt am assertIsInstance(...)-Test fehl.
  • min/max verwechseln. oldest = max(...), weil ältere Person → höheres Alter.

Assignment-Aufgabe 3

`filter` auf Instanzen — und warum `print(filter(...))` nichts zeigt

Was ist gefragt

Aufgabe 1.4 fragt nach Personen einer bestimmten Generation (z. B. zwischen 1965 und 1979). Du sollst sie mit filter(...) extrahieren und in einer Liste speichern.

Danach (HSG-Klassiker) sollst du print(...) auf das Filter-Objekt aufrufen und überlegen, warum es nicht sinnvoll funktioniert.

Strategie

  1. filter(predicate, iterable) — predicate ist eine Funktion, die True/False liefert; iterable ist die Liste.
  2. Mit lambda inline: lambda p: 1965 <= p.year_of_birth <= 1979 (Python erlaubt verkettete Vergleiche).
  3. list(...) aussen herum, damit du eine konkrete Liste statt eines Iterators hast.

Lösungsskelett

gen_x = list(filter(lambda p: 1965 <= p.year_of_birth <= 1979, persons))

Typische Stolperstellen

  • print(filter(...)) zeigt <filter object at 0x…>. Das ist kein Bug, sondern Pythons lazy iterator. Erst list(...) materialisiert.
  • Iterator nur einmal durchlaufbar. Wenn du gen_x = filter(...) (ohne list) speicherst und zweimal iterierst, ist er beim zweiten Mal leer.
  • Verkettete Vergleiche. 1965 <= p.year_of_birth <= 1979 ist Python-Idiom — entspricht 1965 <= p.year_of_birth and p.year_of_birth <= 1979.

Assignment-Aufgabe 4

Subklasse mit `super().__init__` und überschriebenen Dunder-Methoden

Was ist gefragt

Aufgabe 2 (Superheroes) ist die Vererbungs-Aufgabe. Du definierst eine Superhero-Klasse, die von Person erbt und zusätzliche Attribute (alias, powerlevel) sowie Methoden (fight, receive_damage, ggf. receive_healing) bekommt. Außerdem überschreibst du __str__ und __repr__.

In einer Folge-Aufgabe definierst du HealingSuperhero als Subklasse von Superhero mit zusätzlicher heal(other)-Methode.

Strategie

  1. __init__ mit super() zuerst:
    def __init__(self, name, year_of_birth, alias, powerlevel):
        super().__init__(name, year_of_birth)
        self.alias = alias
        self.powerlevel = powerlevel
    
  2. Methoden hinzufügenfight, receive_damage etc. Die Schadenslogik im HSG-Beispiel ist damage = enemy.powerlevel // 2, dann beide reduzieren.
  3. Dunder-Methoden überschreiben__str__ für print(...), __repr__ für REPL/repr(...). Bedingungen wie 'DEFEATED' bei powerlevel <= 0 werden direkt als f-String mit if/else-Ausdruck formuliert.
  4. Sub-Subklasse HealingSuperhero — auch hier super().__init__(...) zuerst, dann die heal(other)-Methode, die other.receive_healing(self.powerlevel) aufruft.

Lösungsskelett

class Superhero(Person):
    def __init__(self, name, year_of_birth, alias, powerlevel):
        super().__init__(name, year_of_birth)
        self.alias = alias
        self.powerlevel = powerlevel

    def fight(self, enemy):
        incoming = enemy.powerlevel // 2
        outgoing = self.powerlevel // 2
        self.receive_damage(incoming)
        enemy.receive_damage(outgoing)

    def receive_damage(self, damage):
        self.powerlevel = max(self.powerlevel - damage, 0)

    def receive_healing(self, healing):
        self.powerlevel += healing

    def __str__(self):
        state = self.powerlevel if self.powerlevel > 0 else "DEFEATED"
        return f"{self.alias}: {state}"

    def __repr__(self):
        return f"{self} ({super().__str__()})"

Typische Stolperstellen

  • super().__init__ vergessenname und year_of_birth fehlen auf der Subklassen-Instanz.
  • max(...) vergessenpowerlevel kann negativ werden, dann zeigt der 'DEFEATED'-String möglicherweise nicht.
  • __repr__ benutzt super().__str__() — das ruft die __str__-Methode der Superklasse Person auf (also "Bucky Barnes (105)"), nicht die der eigenen Klasse. Das ist im HSG-Beispiel intentional, gibt einen schönen Format wie "Winter Soldier: 50 (Bucky Barnes (105))".

Aufgaben

Phase 3 / 4
Aufgabe

Eine Klasse `Book` definieren

2 Punkte

Definiere eine Klasse Book mit folgenden Anforderungen:

  • Konstruktor __init__(self, title, author, pages) setzt drei Attribute: self.title, self.author, self.pages.
  • Eine Methode is_long(self), die True zurückgibt, wenn das Buch mehr als 300 Seiten hat, sonst False.
  • Eine Methode __str__(self), die einen String der Form "<title> von <author>" zurückgibt — z. B. "Dune von Frank Herbert".

Beispiel:

b = Book("Dune", "Frank Herbert", 412)
b.is_long()   # True
str(b)        # "Dune von Frank Herbert"

Hinweise

    Aufgabe

    Vererbung: `Novel` erbt von `Book`

    3 Punkte

    Aufbauend auf der Book-Klasse aus Aufgabe 1: definiere eine Subklasse Novel mit:

    • Zusätzlichem Attribut genre (z. B. "Sci-Fi", "Fantasy").
    • Konstruktor __init__(self, title, author, pages, genre) — der muss super().__init__(...) aufrufen, um die Attribute der Superklasse zu setzen.
    • __str__(self) überschreibt die Form auf: "<title> von <author> (<genre>)".

    Beispiel:

    n = Novel("Dune", "Frank Herbert", 412, "Sci-Fi")
    n.is_long()   # True — Methode aus Book wird geerbt!
    str(n)        # "Dune von Frank Herbert (Sci-Fi)"
    

    Die Book-Klasse aus Aufgabe 1 ist im selben Notebook bereits definiert; du musst sie nicht neu schreiben.

    Hinweise

      Aufgabe

      Methode, die State mutiert: `Account.transfer`

      3 Punkte

      Definiere eine Klasse Account mit:

      • __init__(self, owner, balance=0)balance ist optional, Default 0.
      • Methode deposit(self, amount) — erhöht self.balance um amount.
      • Methode withdraw(self, amount) — reduziert self.balance um amount. Wenn das Konto nicht genug Geld hat (also amount > self.balance), soll es eine ValueError mit der Nachricht "insufficient funds" werfen — und den Stand nicht ändern.
      • Methode transfer(self, other, amount) — überweist amount von self an other (also: self.withdraw(amount) und dann other.deposit(amount)).

      Beispiel:

      alice = Account("Alice", 100)
      bob   = Account("Bob")     # balance=0 per Default
      alice.transfer(bob, 30)
      # alice.balance == 70, bob.balance == 30
      alice.transfer(bob, 999)   # ValueError("insufficient funds"), beide Stände bleiben!
      

      Hinweise

        Aufgabe

        `@property` + Vererbung: `Vehicle` und `ElectricVehicle`

        2 Punkte

        Definiere zwei Klassen:

        • Vehicle mit Konstruktor __init__(self, brand, year) — speichert intern self._brand und self._year (Underscore-Konvention für "intern"). Stelle die beiden Werte über @property-Methoden brand und year als read-only Eigenschaften zur Verfügung.

        • ElectricVehicle(Vehicle) — erbt von Vehicle, fügt im __init__ den Parameter battery_kwh hinzu (intern self._battery_kwh), exposed über @property battery_kwh. Vergiss super().__init__(...) nicht.

        Beispiel:

        e = ElectricVehicle("Tesla", 2024, 75)
        e.brand           # 'Tesla'
        e.year            # 2024
        e.battery_kwh     # 75
        e.brand = 'X'     # AttributeError — read-only!
        e._brand = 'X'    # geht (interner Name, kein property-Schutz)
        

        Hinweise

          Übungsquiz

          Phase 4 / 4

          Teste dein Verständnis in 15 Minuten mit 5 Frage(n), direkt an den HSG-Quizstolperfallen ausgerichtet.

          Quiz starten