Skip to content

Latest commit

 

History

History
844 lines (610 loc) · 33.1 KB

File metadata and controls

844 lines (610 loc) · 33.1 KB

LiaScript

OOP — Konzeptioneller Einstieg (Python)

Parameter Kursinformationen
Veranstaltung: Vorlesung Softwareentwicklung
Teil: 7/27
Semester @config.semester
Hochschule: @config.university
Inhalte: @comment
Link auf den GitHub: https://github.com/TUBAF-IfI-LiaScript/VL_Softwareentwicklung/blob/master/07_OOPGrundlagenPython.md
Autoren @author


Hinweis zum Format: Diese Vorlesung ergänzt die Reihe 07–09 als Einstieg für Teilnehmer ohne C#-Hintergrund. Wir nutzen Python, weil die Syntax die Konzepte weniger verdeckt. In den Folgevorlesungen werden dieselben Ideen in C# vertieft.

Lernziele

Nach der Vorlesung können Sie ...

  • erklären, welches Problem Objektorientierung löst,
  • den Unterschied zwischen Klasse und Objekt in eigenen Worten beschreiben,
  • eine eigene Klasse mit Attributen, Methoden und Konstruktor in Python schreiben,
  • mehrere Objekte zu einer größeren Struktur zusammensetzen (Komposition),
  • mit Vererbung, Überschreiben und super() Code wiederverwenden,
  • den Unterschied zwischen Klassenattributen und Instanzattributen benennen,
  • die ersten Schritte der Kapselung anwenden (_-Konvention).

Warum Objektorientierung?

Ausgangsproblem

Stellen Sie sich vor, Sie verwalten einen kleinen Bauernhof in einem Programm. Drei Tiere sollen gespeichert werden — jeweils mit Name, Geräusch und Alter.

Ein erster, „naiver" Ansatz:

name1 = "Kitty"
sound1 = "Miau"
age1 = 5

name2 = "Wally"
sound2 = "Wuff"
age2 = 3

name3 = "Berta"
sound3 = "Muuh"
age3 = 8

print(f"{name1} ({age1} Jahre) macht {sound1}")
print(f"{name2} ({age2} Jahre) macht {sound2}")
print(f"{name3} ({age3} Jahre) macht {sound3}")

@LIA.eval(["main.py"], none, python3 main.py)

Frage: Was passiert, wenn der Bauernhof auf 50 Tiere wächst? Was, wenn sich „macht" in „sagt" ändert?

{{1-2}}


Probleme dieses Ansatzes:

  1. Daten driften auseinander. name1, sound1, age1 gehören zusammen — der Compiler weiß das aber nicht.
  2. Verhalten ist verstreut. Die print-Zeile wiederholt sich. Eine Änderung muss überall nachgezogen werden.
  3. Es skaliert nicht. 50 Tiere = 150 Variablen + 50 fast identische print-Zeilen.

Idee der Objektorientierung: Wir bündeln Daten (Name, Geräusch, Alter) und Verhalten (Geräusch ausgeben) zu einer Einheit — einem Objekt.


{{2-4}}


Erster Lösungsversuch: Dictionaries und Funktionen

Bevor wir Klassen einführen, schauen wir uns an, wie weit man mit den bisher bekannten Mitteln kommt. Wir bündeln die Daten zu einem Tier in einem Dictionary und schreiben eine Funktion, die darauf arbeitet:

def make_noise(animal):
    print(f"{animal['name']} ({animal['age']} Jahre) macht {animal['sound']}")

kitty = {"name": "Kitty", "sound": "Miau", "age": 5}
wally = {"name": "Wally", "sound": "Wuff", "age": 3}
berta = {"name": "Berta", "sound": "Muuh", "age": 8}

make_noise(kitty)
make_noise(wally)
make_noise(berta)

@LIA.eval(["main.py"], none, python3 main.py)

Frage: Sehen Sie hier schon die Idee von OOP? Was ist besser als vorher, und was könnte noch verbessert werden?


{{3-4}}


Das ist schon deutlich besser. Aber drei Probleme bleiben:

  1. Keine Garantie, dass die Daten zusammenpassen. Niemand hindert uns daran, ein Dictionary ohne sound zu bauen. Der Fehler erscheint erst beim Aufruf.
  2. Daten und Funktionen sind getrennt. Wer das Dictionary benutzt, muss wissen, welche Funktionen dazu gehören. Das steht nirgends im Code.
  3. Keine Bauplan-Beschreibung. Was ein „Tier" ist, ergibt sich nur indirekt aus den Keys, die benutzt werden.

Frage: Wie könnte eine Sprache uns helfen, Daten und passende Funktionen zusammenzuhalten und einen Bauplan zu beschreiben, an den sich alle Tiere halten müssen?


Analogie: Bauplan und Häuser

       Bauplan (Klasse)                          Instanzen (Objekte)
       +------------------+
       |   Haus           |                +--------+ +--------+ +--------+
       |------------------|     -->        | Haus 1 | | Haus 2 | | Haus 3 |
       | - Grundfläche    |                | 120 m² | | 80 m²  | | 200 m² |
       | - Stockwerke     |                | 2 Et.  | | 1 Et.  | | 3 Et.  |
       | - Farbe          |                | rot    | | weiß   | | gelb   |
       +------------------+                +--------+ +--------+ +--------+
                                                                                  .
Begriff Bedeutung
Klasse Bauplan — beschreibt, welche Eigenschaften und welches Verhalten ein Objekt hat
Objekt / Instanz Konkretes Exemplar nach diesem Bauplan — mit eigenen Werten
Attribut / Feld Eine Eigenschaft (z. B. name, farbe)
Methode Ein Verhalten / eine Funktion, die das Objekt ausführen kann

Merke: Eine Klasse beschreibt einen Typ. Ein Objekt ist ein konkreter Vertreter dieses Typs.

Die erste Klasse

Aufbau

In Python schreiben wir den Bauplan mit dem Schlüsselwort class:

class Animal:
    # Attribute werden im Konstruktor definiert
    def __init__(self, name, sound, age):
        self.name = name
        self.sound = sound
        self.age = age

    # Methode
    def make_noise(self):
        print(f"{self.name} ({self.age} Jahre) macht {self.sound}")

Drei Bestandteile sind neu und brauchen eine Erklärung:

  • __init__ — der Konstruktor. Wird einmal beim Erzeugen eines Objekts aufgerufen. Hier werden die Attribute initialisiert.
  • self — der Verweis auf dieses konkrete Objekt. (In C# heißt es this.) Er muss als erster Parameter jeder Methode stehen. Ist aber nur eine Konvention.
  • self.name = name — legt das Attribut name am Objekt ab. Ohne self. wäre name nur eine lokale Variable in der Funktion.

Objekte erzeugen und nutzen

class Animal:
    def __init__(self, name, sound, age):
        self.name = name
        self.sound = sound
        self.age = age

    def make_noise(self):
        print(f"{self.name} ({self.age} Jahre) macht {self.sound}")

# Drei Instanzen aus demselben Bauplan
kitty = Animal("Kitty", "Miau", 5)
wally = Animal("Wally", "Wuff", 3)
berta = Animal("Berta", "Muuh", 8)

kitty.make_noise()
wally.make_noise()
berta.make_noise()

@LIA.eval(["main.py"], none, python3 main.py)

Beobachtung: Aus einem Bauplan entstehen beliebig viele Objekte mit jeweils eigenen Werten. Das Verhalten (make_noise) ist nur einmal beschrieben.

Was bedeutet self?

Jeder Methodenaufruf läuft auf einem konkreten Objekt. Python übergibt dieses Objekt automatisch als ersten Parameter:

kitty.make_noise()
# entspricht intern:
# Animal.make_noise(kitty)

self ist also schlicht „das Objekt, auf dem die Methode gerade arbeitet". Die Methode kann darüber lesen (self.name) und schreiben (self.age = self.age + 1).

class Animal:
    def __init__(self, name, sound, age):
        self.name = name
        self.sound = sound
        self.age = age

    def have_birthday(self):
        self.age = self.age + 1
        print(f"{self.name} ist jetzt {self.age} Jahre alt.")

kitty = Animal("Kitty", "Miau", 5)
kitty.have_birthday()
kitty.have_birthday()

@LIA.eval(["main.py"], none, python3 main.py)

Aufgabe: Erweitern Sie die Klasse um eine Methode rename(self, new_name), die den Namen ändert und das neue Geräusch ausgibt.

Klassenattribute vs. Instanzattribute

Manche Eigenschaften gehören zu jedem einzelnen Objekt (jedes Tier hat einen anderen Namen). Andere Eigenschaften gehören zur ganzen Klasse — sie sind für alle Tiere gleich. Beispiel: Alle Tiere atmen Luft und leben auf demselben Planeten.

class Animal:
    atmen = "Luft"           # Klassenattribut — gehört zur Klasse
    planet = "Erde"          # Klassenattribut — gehört zur Klasse

    def __init__(self, name, sound):
        self.name = name      # Instanzattribut — gehört zum Objekt
        self.sound = sound

    def info(self):
        print(f"{self.name} lebt auf der {Animal.planet} und macht {self.sound}.")


kitty = Animal("Kitty", "Miau")
wally = Animal("Wally", "Wuff")

kitty.info()
wally.info()

# Wenn sich der Planet ändert, ändert er sich für ALLE Instanzen:
Animal.planet = "Mars"
kitty.info()
wally.info()

@LIA.eval(["main.py"], none, python3 main.py)

Art Wo definiert? Wem gehört es?
Klassenattribut direkt unter class Foo: der Klasse — geteilt von allen
Instanzattribut im Konstruktor mit self.x = ... dem konkreten Objekt

Faustregel: Im Zweifel Instanzattribut. Klassenattribute eignen sich für Konstanten oder wirklich gemeinsam genutzte Daten.

Spezielle Methode __str__

Was passiert, wenn wir versuchen, ein Objekt direkt mit print() auszugeben?

class Animal:
    def __init__(self, name, sound):
        self.name = name
        self.sound = sound

kitty = Animal("Kitty", "Miau")
print(kitty)

@LIA.eval(["main.py"], none, python3 main.py)

Wir sehen etwas wie <__main__.Animal object at 0x7f...>. Das ist die Standard-Darstellung — nicht hilfreich. Mit der speziellen Methode __str__ definieren wir selbst, wie unser Objekt als Text aussehen soll:

class Animal:
    def __init__(self, name, sound, age):
        self.name = name
        self.sound = sound
        self.age = age

    def __str__(self):
        return f"Animal({self.name}, {self.age} Jahre, sagt '{self.sound}')"


kitty = Animal("Kitty", "Miau", 5)
print(kitty)
print(f"Mein Tier: {kitty}")

@LIA.eval(["main.py"], none, python3 main.py)

Hintergrund: Methoden, deren Namen in __ eingerahmt sind (man spricht „Dunder-Methoden"), sind Hooks, die Python automatisch aufruft. Wir werden in C# später ähnliche Mechanismen sehen — dort heißen sie ToString(), Equals() etc.

Mehrere Objekte zusammenführen

Wir können Objekte selbst wieder in andere Objekte stecken. Eine Farm enthält eine Liste von Animals:

class Animal:
    def __init__(self, name, sound, age):
        self.name = name
        self.sound = sound
        self.age = age

    def make_noise(self):
        print(f"{self.name} macht {self.sound}")


class Farm:
    def __init__(self, address):
        self.address = address
        self.animals = []          # leere Liste

    def add_animal(self, animal):
        self.animals.append(animal)

    def morning_call(self):
        print(f"Guten Morgen auf {self.address}!")
        for animal in self.animals:
            animal.make_noise()


farm = Farm("Biobauernhof Freiberg")
farm.add_animal(Animal("Kitty", "Miau", 5))
farm.add_animal(Animal("Wally", "Wuff", 3))
farm.add_animal(Animal("Berta", "Muuh", 8))

farm.morning_call()

@LIA.eval(["main.py"], none, python3 main.py)

Beobachtung: Die Farm weiß nichts über die Innereien eines Animal. Sie ruft nur make_noise() auf und vertraut darauf, dass jedes Tier weiß, was zu tun ist. Genau das ist die Stärke von OOP.

Kapselung — Was darf nach außen sichtbar sein?

Das Problem

Bisher konnten wir auf jedes Attribut von außen direkt zugreifen — auch schreibend. Das ist gefährlich:

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age


kitty = Animal("Kitty", 5)
kitty.age = -100             # Unsinn, aber Python lässt es zu
print(f"{kitty.name} ist {kitty.age} Jahre alt.")

@LIA.eval(["main.py"], none, python3 main.py)

Niemand möchte ein Tier mit dem Alter -100 haben. Wir brauchen eine Möglichkeit zu signalisieren: „Bitte greift nicht direkt darauf zu — nutzt diese Methode."

Konvention: Unterstrich als „Bitte nicht anfassen"

Python hat — anders als C# oder Java — keine harten Sichtbarkeitsmodifizierer. Stattdessen gilt eine Konvention: Ein führender Unterstrich _ markiert ein Attribut als intern.

class Animal:
    def __init__(self, name, age):
        self.name = name
        self._age = age            # _age ist "intern"

    def get_age(self):
        return self._age

    def set_age(self, new_age):
        if new_age < 0:
            print(f"Ungültiges Alter: {new_age}")
            return
        self._age = new_age


kitty = Animal("Kitty", 5)
kitty.set_age(6)               # ok
print(f"{kitty.name} ist {kitty.get_age()} Jahre alt.")

kitty.set_age(-100)            # wird abgelehnt
print(f"{kitty.name} ist {kitty.get_age()} Jahre alt.")

@LIA.eval(["main.py"], none, python3 main.py)

Schreibweise Bedeutung
name öffentlich — jeder darf lesen/schreiben (public)
_name „bitte nicht von außen anfassen" — nur für die Klasse selbst (protected)
__name (zwei _) erzwingt Namensumbenennung intern (private)

Merke: Kapselung trennt das Was (öffentliches Verhalten) vom Wie (interne Umsetzung). Wer die Klasse benutzt, soll nur das Was sehen müssen — wir können das Wie ändern, ohne dass Aufrufer kaputt gehen.

In C# werden wir dafür echte Modifizierer (public, private) und sogenannte Properties kennenlernen — Vorlesung 08/09. Das geht in Python auch — siehe nächster Slide.

Python Properties: Elegante Kapselung mit @property

In Python können Properties verwendet werden, um den Zugriff auf Attribute zu kontrollieren, ohne die Syntax für den Zugriff zu ändern. Dies ermöglicht eine saubere Trennung zwischen öffentlicher Schnittstelle und interner Implementierung.

class Animal:
    def __init__(self, name, age):
        self.name = name
        self._age = age  # Internes Attribut

    @property
    def age(self):
        """Getter für das Alter."""
        return self._age

    @age.setter
    def age(self, new_age):
        """Setter für das Alter mit Validierung."""
        if new_age < 0:
            raise ValueError(f"Ungültiges Alter: {new_age}")
        self._age = new_age

    @age.deleter
    def age(self):
        """Löscht das Alter."""
        print(f"Alter von {self.name} wird gelöscht.")
        del self._age

# Verwendung
kitty = Animal("Kitty", 5)
print(f"{kitty.name} ist {kitty.age} Jahre alt.")  # Getter

kitty.age = 6  # Setter
print(f"{kitty.name} ist {kitty.age} Jahre alt.")

try:
    kitty.age = -100  # Wird abgelehnt
except ValueError as e:
    print(e)

del kitty.age  # Deleter

@LIA.eval(["EncapsulationProperties.py"], none, python3 EncapsulationProperties.py)


Dekorator Bedeutung
@property Definiert den Getter für ein Attribut.
@<property>.setter Definiert den Setter für ein Attribut (mit Validierung).
@<property>.deleter Definiert den Deleter für ein Attribut.

Vorteil von Properties:

  • Der Zugriff auf Attribute bleibt intuitiv (obj.age statt obj.get_age()).
  • Die Validierung und Logik bleiben in der Klasse verborgen.
  • Änderungen an der internen Implementierung (z. B. Berechnung von _age) haben keine Auswirkungen auf den Code, der die Klasse nutzt.

Vererbung

Motivation

Auf unserem Bauernhof sollen verschiedene Tierarten leben. Ein Hund kann zusätzlich fetch() (Stöckchen apportieren), eine Kuh liefert milk(). Beide sind aber immer noch Tiere — sie haben Namen, Alter, Geräusch.

Eine naive Lösung wäre, die Animal-Felder in jeder neuen Klasse zu wiederholen. Das ist nicht nur lästig, sondern fehleranfällig.

Idee der Vererbung: Eine neue Klasse („Kind") übernimmt alle Eigenschaften und Methoden einer bestehenden Klasse („Eltern") und ergänzt oder verändert sie nur dort, wo es nötig ist.

Syntax in Python

class Dog(Animal):       # Dog erbt von Animal
    ...

Die Klammer hinter dem Klassennamen nennt die Basisklasse. Alles, was Animal kann, kann Dog automatisch auch.

Beispiel: Hund und Kuh

class Animal:
    def __init__(self, name, sound, age):
        self.name = name
        self.sound = sound
        self.age = age

    def make_noise(self):
        print(f"{self.name} macht {self.sound}")


class Dog(Animal):
    def fetch(self):
        print(f"{self.name} bringt das Stöckchen zurück.")


class Cow(Animal):
    def milk(self):
        print(f"{self.name} gibt heute 12 Liter Milch.")


rex = Dog("Rex", "Wuff", 4)
berta = Cow("Berta", "Muuh", 8)

rex.make_noise()    # geerbt von Animal
rex.fetch()         # eigene Methode

berta.make_noise()  # geerbt von Animal
berta.milk()        # eigene Methode

@LIA.eval(["main.py"], none, python3 main.py)

Dog und Cow mussten weder einen Konstruktor noch make_noise neu schreiben — beides kommt von Animal.

Methoden überschreiben

Manchmal soll das Kind etwas anders machen als das Elternteil. Eine Katze macht nicht einfach Miau — sie schnurrt zusätzlich.

class Animal:
    def __init__(self, name, sound, age):
        self.name = name
        self.sound = sound
        self.age = age

    def make_noise(self):
        print(f"{self.name} macht {self.sound}")


class Cat(Animal):
    def make_noise(self):
        # Wir überschreiben die Methode aus Animal
        print(f"{self.name} macht {self.sound} ... und schnurrt.")

animal = Animal("Berta", "Muuh", 8)
animal.make_noise()  # Berta macht Muuh

kitty = Cat("Kitty", "Miau", 5)
kitty.make_noise()

@LIA.eval(["main.py"], none, python3 main.py)

Merke: Die Methode mit dem gleichen Namen in der Kindklasse „gewinnt". Diesen Mechanismus nennen wir Überschreiben (engl. override).

Auf die Eltern zurückgreifen — super()

Oft soll das Kind das Verhalten der Eltern erweitern, nicht ersetzen. Mit super() rufen wir die Methode der Basisklasse auf:

class Animal:
    def __init__(self, name, sound, age):
        self.name = name
        self.sound = sound
        self.age = age

    def make_noise(self):
        print(f"{self.name} macht {self.sound}")


class Cat(Animal):
    def __init__(self, name, age, fur_color):
        super().__init__(name, "Miau", age)   # Eltern-Konstruktor
        self.fur_color = fur_color            # neues Attribut

    def make_noise(self):
        super().make_noise()                  # Eltern-Verhalten
        print(f"  ({self.fur_color}es Fell, schnurrt zufrieden.)")


kitty = Cat("Kitty", 5, "schwarz")
kitty.make_noise()

@LIA.eval(["main.py"], none, python3 main.py)

super() ist ein Verweis auf die Elternklasse. So nutzen wir bestehendes Verhalten und ergänzen es punktuell — ohne Code zu duplizieren.

Eine gemeinsame Liste verschiedener Tiere

Da Dog und Cat beide Animals sind, haben sie die make_noise Methode und wir können diese aufrufen und erwarten, dass jedes Tier das passende Geräusch macht.

class Animal:
    def __init__(self, name, sound):
        self.name = name
        self.sound = sound

    def make_noise(self):
        print(f"{self.name} macht {self.sound}")


class Dog(Animal):
    def make_noise(self):
        print(f"{self.name} bellt laut: WUFF WUFF!")


class Cat(Animal):
    def make_noise(self):
        print(f"{self.name} schnurrt und macht leise {self.sound}.")


tiere = [
    Dog("Rex", "Wuff"),
    Cat("Kitty", "Miau"),
    Animal("Berta", "Muuh"),
]

for tier in tiere:
    tier.make_noise()

@LIA.eval(["main.py"], none, python3 main.py)

Das ist Polymorphie: Derselbe Aufruf (tier.make_noise()) führt — abhängig vom tatsächlichen Typ des Objekts — zu unterschiedlichem Verhalten. Wir vertiefen das in den folgenden Vorlesungen.

Mehrstufige Hierarchien

Vererbung kann auch über mehrere Stufen gehen. Ein Puppy ist ein Dog, und ein Dog ist ein Animal. Jede Stufe ergänzt etwas Eigenes.

class Animal:
    def __init__(self, name):
        self.name = name

    def describe(self):
        print(f"Ich heiße {self.name} und bin ein Tier.")


class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def describe(self):
        super().describe()
        print(f"Genauer: ein Hund der Rasse {self.breed}.")


class Puppy(Dog):
    def __init__(self, name, breed, weeks):
        super().__init__(name, breed)
        self.weeks = weeks

    def describe(self):
        super().describe()
        print(f"Und zwar erst {self.weeks} Wochen alt!")


bello = Puppy("Bello", "Labrador", 8)
bello.describe()

@LIA.eval(["main.py"], none, python3 main.py)

Beobachtung: Jede Klasse fügt nur das hinzu, was an ihrer Stufe Sinn ergibt. Die Methode describe zeigt durch die super()-Kette das gemeinsame Wissen plus das spezifische Wissen jeder Stufe.

Typprüfung mit isinstance

Manchmal müssen wir wissen, was für ein Objekt wir gerade vor uns haben. isinstance(obj, Klasse) antwortet — und berücksichtigt die Vererbungshierarchie:

class Animal: pass
class Dog(Animal): pass
class Puppy(Dog): pass

bello = Puppy("Bello")  # type: ignore
print(isinstance(bello, Puppy))    # True
print(isinstance(bello, Dog))      # True — ein Puppy ist auch ein Dog
print(isinstance(bello, Animal))   # True — und auch ein Animal

@LIA.eval(["main.py"], none, python3 main.py)

Merke: Vererbung drückt eine „ist-ein"-Beziehung aus. Ein Puppy ist ein Hund. Ein Hund ist ein Tier. Wenn diese Beziehung in der echten Welt nicht stimmt, ist Vererbung das falsche Werkzeug — dann passt eher Komposition (siehe FarmAnimal).

Fallstudie: Ein kleiner Online-Shop

Zum Abschluss verbinden wir alle Konzepte in einem etwas größeren Beispiel. Ein Mini-Shop verwaltet Produkte (mit Preis und Bestand) und digitale Produkte, die zusätzlich eine Downloadgröße haben. Ein Warenkorb sammelt Bestellungen.

class Product:
    def __init__(self, name, price, stock):
        self.name = name
        self._price = price        # Intern, über Property zugreifen
        self._stock = stock        # Intern, über Property zugreifen

    @property
    def price(self):
        """Getter für den Preis."""
        return self._price

    @price.setter
    def price(self, new_price):
        """Setter für den Preis mit Validierung."""
        if new_price < 0:
            raise ValueError(f"Ungültiger Preis: {new_price}")
        self._price = new_price

    @property
    def stock(self):
        """Getter für den Bestand."""
        return self._stock

    @stock.setter
    def stock(self, new_stock):
        """Setter für den Bestand mit Validierung."""
        if new_stock < 0:
            raise ValueError(f"Ungültiger Bestand: {new_stock}")
        self._stock = new_stock

    def is_available(self, amount):
        return self._stock >= amount

    def reserve(self, amount):
        if not self.is_available(amount):
            return False
        self._stock -= amount
        return True

    def __str__(self):
        return f"{self.name} ({self.price:.2f} €, {self.stock} auf Lager)"

class DigitalProduct(Product):
    def __init__(self, name, price, size_mb):
        # Digitale Produkte haben unbegrenzten "Bestand"
        super().__init__(name, price, stock=10**9)
        self.size_mb = size_mb

    def __str__(self):
        return f"{self.name} (digital, {self.price:.2f} €, {self.size_mb} MB)"

class Cart:
    def __init__(self):
        self.items = []   # Liste von (Produkt, Menge)

    def add(self, product, amount=1):
        if product.reserve(amount):
            self.items.append((product, amount))
            print(f"  + {amount} x {product.name} hinzugefügt")
        else:
            print(f"  ! {product.name} nicht ausreichend verfügbar")

    def total(self):
        return sum(p.price * n for p, n in self.items)

    def show(self):
        print("Warenkorb:")
        for product, amount in self.items:
            print(f"  - {amount} x {product}")
        print(f"  = Summe: {self.total():.2f} €")

buch    = Product("OOP-Lehrbuch", 29.90, stock=3)
ebook   = DigitalProduct("OOP-Lehrbuch (PDF)", 14.90, size_mb=12)
stift   = Product("Bleistift", 0.80, stock=50)

cart = Cart()
cart.add(buch, 2)
cart.add(ebook, 1)
cart.add(stift, 5)
cart.add(buch, 5)        # mehr als verfügbar — wird abgelehnt
cart.show()

@LIA.eval(["main.py"], none, python3 main.py)

Welche Konzepte stecken hier drin?

  • Klasse + Konstruktor (Product, __init__)
  • Kapselung (_price, _stock mit Zugriffs-Methoden)
  • Spezielle Methoden (__str__)
  • Vererbung + super() (DigitalProduct(Product))
  • Komposition (Cart enthält Liste von Produkten)
  • Polymorphie (__str__ reagiert je nach Produkttyp anders)

Zusammenfassung

Begriff Python-Syntax Bedeutung
Klasse class Animal: Bauplan
Objekt / Instanz kitty = Animal("Kitty", "Miau", 5) Konkreter Vertreter des Bauplans
Konstruktor def __init__(self, ...): Wird beim Erzeugen einmal aufgerufen
self erster Parameter jeder Methode „Dieses konkrete Objekt"
Attribut self.name = name Eigenschaft am Objekt
Methode def make_noise(self): Verhalten, das das Objekt ausführen kann
Klassenattribut direkt unter class Foo: gehört der Klasse, geteilt von allen Instanzen
Kapselung self._x (Konvention) „bitte nicht von außen anfassen"
__str__ def __str__(self): return ... Wie das Objekt als Text aussieht
Vererbung class Dog(Animal): Dog übernimmt alles von Animal
Überschreiben gleiche Methode in Kindklasse neu definieren Kind reagiert anders als Eltern
super() super().__init__(...) / super().make_noise() Auf Eltern-Verhalten zurückgreifen
isinstance isinstance(obj, Animal) Prüft Typ inkl. Vererbungskette
Polymorphie gleiche Methode, unterschiedliches Verhalten Aufrufer muss konkreten Typ nicht kennen

Brücke zur nächsten Vorlesung (C#)

Die Konzepte sind universell. In C# sehen Sie dieselben Ideen mit anderer Syntax:

Konzept Python C# (Vorschau)
Klasse class Animal: public class Animal { ... }
Konstruktor def __init__(self, name): public Animal(string name) { ... }
self / this self.name this.name
Objekt erzeugen kitty = Animal("Kitty") Animal kitty = new Animal("Kitty");
Vererbung class Dog(Animal): public class Dog : Animal { ... }
Eltern aufrufen super().__init__(...) : base(...) bzw. base.MakeNoise()

In den Vorlesungen 07, 08 und 09 gehen wir tiefer:

  • 07structs und Klassen in C#, Sichtbarkeit (public/private), Wert- vs. Referenztypen
  • 08 — Kapselung, Properties, Operatorenüberladung
  • 09 — Vererbung in C#, abstrakte Methoden, Polymorphie im Detail

Aufgaben

  1. Eigene Klasse modellieren. Wählen Sie ein Objekt aus Ihrem Alltag (Buch, Fahrrad, Café-Bestellung) und schreiben Sie dafür eine Klasse mit mindestens drei Attributen und zwei Methoden. Erzeugen Sie zwei Instanzen.

  2. Vererbung üben. Erweitern Sie das Animal-Beispiel um die Klasse Bird. Vögel haben zusätzlich ein Attribut can_fly (bool) und eine Methode fly(), die abhängig von can_fly „fliegt los" oder „kann nicht fliegen" ausgibt.

  3. Eltern erweitern. Schreiben Sie eine Klasse Puppy(Dog), deren make_noise() zuerst die Methode aus Dog aufruft und danach ... und wedelt mit dem Schwanz ergänzt.

  4. Reflexion. Erklären Sie in eigenen Worten (3–4 Sätze), warum die OOP-Variante des Bauernhof-Beispiels besser skaliert als die Variante mit losen Variablen vom Anfang.

  5. Kapselung anwenden. Erweitern Sie die Klasse Cart aus der Fallstudie um eine Methode remove(self, product), die ein Produkt aus dem Warenkorb entfernt und den reservierten Bestand wieder freigibt. Stellen Sie sicher, dass _stock nicht negativ und nicht über den ursprünglichen Wert hinaus wachsen kann.

  6. Hierarchie modellieren. Modellieren Sie eine Klassenhierarchie für Verkehrsmittel: Vehicle (mit name und Methode move()), darunter Car, Bicycle, Boat. Jede Klasse soll move() passend überschreiben (z. B. „rollt", „tritt", „schwimmt"). Legen Sie eine Liste aus drei verschiedenen Verkehrsmitteln an und rufen Sie move() in einer Schleife auf.

  7. Quizfragen zur Selbstkontrolle.

    • Was ist der Unterschied zwischen einer Klasse und einem Objekt?
    • Warum ist self als erster Parameter jeder Methode notwendig?
    • Wann verwenden Sie Vererbung, wann eher Komposition?
    • Was bewirkt der Aufruf super().__init__(...) im Konstruktor einer Kindklasse?
    • Warum ist die _-Konvention nur eine Bitte, kein technischer Schutz?