Home | Lehre | Videos | Texte | Vorträge | Software | Person | Impressum, Datenschutzerklärung | Blog RSS

Stand: 2025-05-15
weitgehend formuliert von ChatGPT 4, verbessert von ChatGPT 4o, redigiert/korrigiert von Jörn Loviscach

Objektorientierte Programmierung, Teil 2

Vererbung

Vererbung ist ein Grundkonzept der objektorientierten Programmierung. Sie ermöglicht, eine neue Klasse (Unterklasse) zu erstellen, die die Eigenschaften und Methoden einer bestehenden Klasse (Oberklasse) übernimmt. Dies hilft, Code wiederzuverwenden und die Struktur von Programmen klarer und wartbarer zu gestalten.

Angenommen, wir möchten ein Bestellsystem für einen Online-Shop entwickeln. Wir haben eine allgemeine Klasse Produkt, und daraus abgeleitet spezifische Klassen für verschiedene Produkttypen wie Buch und Elektronik.

Schritt 1: Definition der allgemeinen Klasse Produkt

class Produkt:
    def __init__(self, name, preis):
        self.name = name
        self.preis = preis

    def zeige_info(self):
        print(f'Produkt: {self.name}, Preis: {self.preis:.2f} EUR')
(Professionell würde man keine zeige_info-Methode schreiben. Siehe unten zu __str__. Aber eines nach dem anderen!)

Schritt 2: Definition der spezialisierten Klassen

class Buch(Produkt):
    def __init__(self, name, preis, autor):
        super().__init__(name, preis)
        self.autor = autor

class Elektronik(Produkt):
    def __init__(self, name, preis, garantie):
        super().__init__(name, preis)
        self.garantie = garantie

super() wird benutzt, um auf Methoden der Oberklasse = superclass zuzugreifen, die in der Unterklasse überschrieben sind. So sorgt super().__init__(name, preis) in den Unterklassen dafür, dass der Initialisierer der Oberklasse aufgerufen wird und dann die Attribute name und preis anlegt.

Vorsicht: self ist eine Variable (die man zur Verwirrung auch anders nennen dürfte), aber super() ein Funktionsaufruf, also mit Klammern dahinter.

Und nochmal Vorsicht: Das Folgende ist erlaubt, denn super().__init__(...) ist ein ganz normaler Funktionsaufruf. Dass man in der Unterklasse die Variablennamen name und preis wie in der Oberklasse verwendet, dient nur dem Verständnis.

def __init__(mein_Selbst, komischer_name, preis, garantie):
    super().__init__(komischer_name, 2 * preis)
    mein_Selbst.garantie = garantie

Schritt 3: Verwendung der Klassen

# Erstellen von Produktinstanzen
produkt1 = Produkt('Allgemeines Produkt', 19.99)
buch1 = Buch('Der Python-Kurs', 29.99, 'John Doe')
elektronik1 = Elektronik('Smartphone', 399.99, 2)

# Informationen anzeigen
produkt1.zeige_info()
# Ausgabe:
# Produkt: Allgemeines Produkt, Preis: 19.99 EUR

buch1.zeige_info()
# Ausgabe: 
# Produkt: Der Python-Kurs, Preis: 29.99 EUR

elektronik1.zeige_info()
# Ausgabe:
# Produkt: Smartphone, Preis: 399.99 EUR

Klassenhierarchie

Durch die Verwendung von Vererbung können wir die allgemeine Funktionalität in der Produkt-Klasse definieren und dann spezialisierte Klassen wie Buch und Elektronik erstellen, die diese Funktionalität erweitern oder anpassen. Dies führt zu einem klaren und übersichtlichen Code, der leicht zu erweitern und zu warten ist.

Produkt ist die Oberklasse = Basisklasse = Elternklasse der Klassen Buch und Elektronik. Diese letzteren beiden Klassen sind Unterklassen = Ableitungen = Kindklassen von Produkt.

Das Spiel lässt sich weitertreiben, indem etwa eine Klasse Notebook von der Klasse Elektronik erbt. Notebook wäre dann eine Unterklasse einer Unterklasse von Produkt. Nein, man sagt nicht Enkelklasse dazu. ;-)

Überschreiben von Methoden

Das Überschreiben (englisch overriding) von Methoden bedeutet, dass eine Methode in einer abgeleiteten Klasse eine Methode der Basisklasse mit demselben Namen ersetzt. Dadurch kann das Verhalten der Methode in der abgeleiteten Klasse spezifiziert oder angepasst werden.

Das Überschreiben von Methoden ist aus mehreren Gründen nützlich:

  1. Spezialisierung des Verhaltens: Es ermöglicht einer abgeleiteten Klasse, das Verhalten der geerbten Methoden zu spezifizieren und zu ändern.

  2. Polymorphismus: Es unterstützt das Konzept des Polymorphismus, bei dem eine Methode unterschiedlich implementiert wird, basierend darauf, welches Objekt sie aufruft. (Unten mehr dazu.)

  3. Wiederverwendbarkeit und Wartbarkeit: Durch das Überschreiben können wir Code wiederverwenden und dennoch flexible und erweiterbare Entwürfe erstellen, ohne die Basisklasse ändern zu müssen.

Formalitäten

Um keine Verwirrung hervorzurufen, sollte die überschreibende Methode dieselben Typen an Parametern nehmen wie die Oberklasse. (Etwa in C++ und Java ist das eine Pflicht für das Überschreiben.)

Ein beliebter Fehler: Vertippt man sich in der Unterklasse beim Namen der Methode, überschreibt man nicht die Methode der Oberklasse, sondern legt in der Unterklasse eine ganz neue Methode an. Um diesen Fehler automatisch entdecken zu lassen, kann man ab Python 3.12 in die Zeile vor die Definition der überschreibenden Methode in der Unterklasse ein @override schreiben. Dazu ist derzeit noch vorher ein from typing import override nötig.

Beispiel: Online-Shop-Bestellsystem

Das Beispiel von oben lässt sich fortsetzen, indem man die zeige_info-Methode überschreibt Die Produkt-Klasse definiert ja eine allgemeine Methode zeige_info, die grundlegende Informationen über das Produkt anzeigt:

class Produkt:
    def __init__(self, name, preis):
        self.name = name
        self.preis = preis

    def zeige_info(self):
        print(f'Produkt: {self.name}, Preis: {self.preis:.2f} EUR')

Die Buch-Klasse erbt von Produkt und fügt das Attribut autor hinzu. Die Klasse Buch kann nun die Methode zeige_info von Produkt überschreiben, um die spezifischen Informationen des Buches anzuzeigen. super().zeige_info() ruft die zeige_info-Methode der Oberklasse auf, um zunächst die allgemeinen Produktinformationen anzuzeigen:

class Buch(Produkt):
    def __init__(self, name, preis, autor):
        super().__init__(name, preis)
        self.autor = autor

    def zeige_info(self):
        super().zeige_info()
        print(f'Autor: {self.autor}')

Entsprechendes kann in der Methode zeige_info der Elektronik-Klasse passieren:

class Elektronik(Produkt):
    def __init__(self, name, preis, garantie):
        super().__init__(name, preis)
        self.garantie = garantie

    def zeige_info(self):
        super().zeige_info()
        print(f'Garantie: {self.garantie} Jahre')

Die Ausgabe im Beispiel von oben ist damit nun:

buch1.zeige_info()
# Ausgabe: 
# Produkt: Der Python-Kurs, Preis: 29.99 EUR
# Autor: John Doe

elektronik1.zeige_info()
# Ausgabe:
# Produkt: Smartphone, Preis: 399.99 EUR
# Garantie: 2 Jahre

Durch das Überschreiben der zeige_info-Methode in den abgeleiteten Klassen können wir spezifische Informationen für unterschiedliche Produkttypen anzeigen, ohne die allgemeine Logik der Basisklasse Produkt zu verändern. Dies zeigt, wie Vererbung und Überschreiben zusammenarbeiten, um flexiblen und erweiterbaren Code zu erstellen.

Warum würde beim Überschreiben oben self.zeige_info() statt super().zeige_info() Unsinn sein?

Die vordefinierten, zu überschreibenden Methoden __str__ und __repr__

Jede Klasse in Python erbt direkt oder über ihre Oberklasse die Methoden __str__ und __repr__. Diese bestimmen, wie eine Instanz der Klasse als Zeichenkette dargestellt wird.

Wenn man diese Methoden nicht überschreibt, zeigt Python standardmäßig etwa bei print() nur etwas wie <MeineKlasse object at 0x7f...> – das ist meist nicht hilfreich. Deshalb überschreibt man diese beiden Methoden in eigenen Klassen fast immer:

class Auto:
    def __init__(self, marke, baujahr):
        self.marke = marke
        self.baujahr = baujahr

    def __str__(self):
        return f'{self.marke} aus {self.baujahr}'

    def __repr__(self):
        return f'Auto("{self.marke}", {self.baujahr})'
a = Auto('Opel', 2015)

print(a)      # Ausgabe: Opel aus 2015
a             # Ausgabe: Auto("Opel", 2015)

Die schon bekannte Initialisierungsmethode __ini__ ist ebenfalls von dieser Art: Auch sie wird von der Oberklasse geerbt und fast immer überschrieben. (Die Konstruktoren in den Sprachen C++, Java, C# werden nicht vererbt und deshalb auch nicht überschrieben.)

Polymorphie

Polymorphie ist ein fundamentales Konzept in der objektorientierten Programmierung, das es Objekten verschiedener Klassen ermöglicht, auf die gleiche Art angesprochen zu werden. Eine der Hauptideen hinter Polymorphie ist, dass Objekte einer Unterklasse (und Unter-Unterklasse usw.) überall dort verwendet werden können, wo Objekte der Oberklasse erwartet werden. Das heißt, Unterklassen (Kindklassen) können ihre jeweiligen Oberklassen (Elternklassen) vertreten. Die Unterklassen können dabei aber zusätzliche Attribute und Methoden haben und sie können Methoden ihrer jeweiligen Oberklasse überschreiben und damit den Effekt dieser Methoden ändern.

Beispiel: Liste von Produkten

Der Online-Shop aus dem bereits verwendeten Beispiel benötigt einen Datenspeicher für den Gesamtbestand an Produkten. Hierbei verwenden wir eine Liste, die Objekte verschiedener Produktklassen enthält, aber dennoch als Liste von Produkt-Objekten betrachtet wird.

Dank Polymorphie können wir eine Liste von Produkten definieren, die sowohl Produkt, Buch als auch Elektronik-Objekte enthält, denn Buch und Elektronik können ihre Oberklasse Produkt vertreten. Die Methode zeige_info wird korrekt für jeden Produkttyp aufgerufen.

# Liste von verschiedenen Produktobjekten
produkte: list[Produkt] = [
    Produkt('Allgemeines Produkt', 19.99),
    Buch('Der Python-Kurs', 29.99, 'John Doe'),
    Elektronik('Smartphone', 399.99, 2)
]

# Informationen anzeigen für alle Produkte in der Liste
for produkt in produkte:
    produkt.zeige_info()
    print()  # Leere Zeile zur besseren Lesbarkeit

Vorteile der Polymorphie

Durch Polymorphie können wir eine einheitliche Schnittstelle (Sammlung der Methoden) für verschiedene Produkttypen bereitstellen, was zu einem flexiblen, erweiterbaren und gut strukturierten Code führt.

Kapselung und Unterstriche

Sprachen wie C++ und Java sind sehr strikt darin, festzulegen, welche Klassen auf welche Bestandteile welcher anderen Klassen zugreifen können. In Python ist das viel lockerer. Unterstriche vor den Namen von Attributen und Methoden spielen dabei die Hauptrolle.

Einfacher Unterstrich (_)

Ein einzelner Unterstrich am Anfang eines Namens ist ein Hinweis, dass das jeweilige Element als geschützt betrachtet werden soll, sozusagen unter der Motorhaube liegend oder versehen mit der Aufschrift Finger weg!. Es soll nicht von außerhalb der Klasse direkt angesprochen werden. Das ist wohlgemerkt nur ein soll, denn Python verbietet diesen Zugriff nicht.

Der Gedanke hinter diesem (zugegebenermaßen schwachen) Schutz ist, dass man dank ihm unter der Motorhaube der Klasse Änderungen vornehmen kann, ohne auf andere Klassen Rücksicht nehmen zu müssen. Das gelingt um so besser, je mehr Attribute und Methoden man mit dem Unterstrich als geschützt markiert und je mehr sich andere Klassen an diesen Hinweis halten, also diese Attribute und Methoden nicht direkt ansprechen.

Ein Beispiel:

class Beispiel:
    def __init__(self):
        self._geschütztes_attribut = 42

    def _geschützte_methode(self):
        print('Diese Methode ist geschützt.')

obj = Beispiel()
print(obj._geschütztes_attribut)  # 42, aber eigentlich sollte man das nicht machen
obj._geschützte_methode()         # Geht, aber eigentlich sollte man das nicht machen

In diesem Beispiel werden _geschütztes_attribut und _geschützte_methode mit einem einfachen Unterstrich versehen, um anzuzeigen, dass sie nicht außerhalb der Klasse verwendet werden sollten.

Doppelte Unterstriche (__)

Doppelte Unterstriche vor einem Attribut oder einer Methode bieten erstens einen (minimal) stärkeren Schutz und ersparen zweitens Stress beim Ableiten von Klassen. Doppelte Unterstriche führen anders als der einfache Unterstrich zu einer Aktion der Python-Laufzeitumgebung: Intern wird dem jeweiligen Namen der Name der Klasse vorangestellt, mit einem führenden Unterstrich. (Dieser Mechanismus gilt nicht für Namen wie __str__, die obendrein zwei oder mehr Unterstriche am Ende haben.)

Dies ist keine ernstzunehmende Schutzmaßnahme, um das Attribut oder die Methode zu verbergen, aber es erleichtert das Ableiten: Eine Unterklasse kann die gleichen Namen für eigene weitere Attribute und Methoden verwenden wie ihre Oberklasse; sie werden dann zusätzlich angelegt, weil sie ja intern anders heißen. Das vermeidet Konflikte.

Doppelte Unterstriche sind vor allem in tiefen Klassenhierarchien mit Kindeskindeskinderklassen usw. hilfreich, in denen man den Überblick darüber verliert, welche Attribute und Methoden die Oberklassen und Oberoberklassen usw. haben, oder in denen die Gefahr besteht, dass später in einer Oberklasse ein Attribut oder eine Methode ergänzt wird, dessen/deren Namen es schon in einer der Unterklassen gibt.

Ein Beispiel:

class Beispiel:
    def __init__(self):
        self.__privates_attribut = 42

    def __private_methode(self):
        print('Diese Methode ist privat.')

    def zugriff_methode(self):
        self.__private_methode()

obj = Beispiel()
# Direkter Zugriff schlägt fehl:
# print(obj.__privates_attribut)   # Fehler
# obj.__private_methode()          # Fehler

# Der Zugriff ist mit ein wenig mehr Mühe aber möglich:
print(obj._Beispiel__privates_attribut)  # 42
obj._Beispiel__private_methode()         # Diese Methode ist privat.

Klassen-Attribute und -Methoden

Alle bisher erwähnten Attribute und Methoden bezogen sich jeweils auf eine Instanz, also auf ein Objekt der Klasse: Jede Instanz der Klasse besitzt eigene Werte für diese Attribute und hat in den Methoden die Variable self, die auf die aktuelle Instanz verweist. In der Objektorientierung unterscheidet man solche Instanz-Attribute und -Methoden, die spezifisch für eine Instanz (ein Objekt) einer Klasse sind, von Klassen-Attributen und -Methoden, die für die gesamte Klasse und alle ihre Instanzen gelten. Letztere heißen oft auch statisch.

In Python gibt es (anderes als in den meisten anderen Sprachen) eine Unterscheidung zwischen Klassenmethoden und statischen Methoden. Erstere können direkt auf Klassenattribute zugreifen, letztere nicht.

Ein Beispiel

In Python wird ein Klassen-Attribut in der Klassendefinition außerhalb aller Methoden definiert:

class Fahrzeug:
    # Klassen-Attribut
    anzahl_fahrzeuge = 0

    def __init__(self, marke, modell):
        # Instanz-Attribute
        self.marke = marke
        self.modell = modell
        # Erhöhen der Anzahl der Fahrzeuge bei jeder Instanziierung
        Fahrzeug.anzahl_fahrzeuge += 1

# Instanziiere Fahrzeuge
auto1 = Fahrzeug('Toyota', 'Corolla')
auto2 = Fahrzeug('Honda', 'Civic')

print(f'Anzahl der Fahrzeuge: {Fahrzeug.anzahl_fahrzeuge}')  # Ausgabe: 2

Eine Klassen-Methode wird mit dem Decorator @classmethod und einem speziellen ersten Parameter cls (der auf die Klasse selbst verweist) definiert:

class Fahrzeug:
    anzahl_fahrzeuge = 0

    def __init__(self, marke, modell):
        self.marke = marke
        self.modell = modell
        Fahrzeug.anzahl_fahrzeuge += 1

    @classmethod
    def get_anzahl_fahrzeuge(cls):
        return cls.anzahl_fahrzeuge

# Instanziiere Fahrzeuge
auto1 = Fahrzeug('Toyota', 'Corolla')
auto2 = Fahrzeug('Honda', 'Civic')

print(f'Anzahl der Fahrzeuge: {Fahrzeug.get_anzahl_fahrzeuge()}')  # Ausgabe: 2

In einer Klassenmethode hat also cls die Rolle des self in einer Instanzmethode. Eine Instanzmethode wird mittels einer konkreten Instanz aufgerufen wird, nach dem Muster auto1.tu_etwas(). Eine Klassenmethode wird dagegen über die Klasse selbst aufgerufen: Fahrzeug.tu_etwas().

Anders als eine Klassenmethode arbeitet eine statische Methode unabhängig von der Klasse und ihren Instanzen, hat also keinen Zugriff auf cls oder self. Sie wird mit dem Dekorator @staticmethod definiert und ebenfalls über die Klasse selbst aufgerufen, nicht über eine Instanz:

class Fahrzeug:
    anzahl_fahrzeuge = 0

    def __init__(self, marke, modell):
        self.marke = marke
        self.modell = modell
        Fahrzeug.anzahl_fahrzeuge += 1

    @classmethod
    def get_anzahl_fahrzeuge(cls):
        return cls.anzahl_fahrzeuge

    @staticmethod
    def beispiel_static_method():
        print('Dies ist eine statische Methode.')

# Aufruf der statischen Methode
Fahrzeug.beispiel_static_method()  # Ausgabe: Dies ist eine statische Methode.

Werte und Referenzen

In Python enthalten alle Variablen Referenzen und nicht direkt Werte: Jede Variable zeigt auf ein Objekt und enthält nie den Wert selbst. Das ist anders als in C, C++, Java und vielen anderen Sprachen, in denen insbesondere Zahlenvariablen direkt Werte speichern, statt (etwa als Zeiger) auf Werte zu verweisen.

Python arbeitet also so, als ob man in C ausschließlich Zeiger (Pointer) nutzen würde. Das kann für Überraschungen sorgen, weil man dasselbe Objekt über verschiedene Referenzen ansprechen und ändern kann.

Mehrere Referenzen auf ein Objekt

Mehrere Variablen können auf dasselbe Objekte verweisen. Dies bedeutet, dass, wenn man ein Objekt über einer dieser Variable ändert, diese Änderung ebenfalls über die anderen Variablen sichtbar ist:

liste1 = [1, 2, 3]
liste2 = liste1  # Es gibt weiterhin nur eine Liste, aber nun zwei Verweise darauf!

liste2.append(4)

print(liste1)    # Ausgabe: [1, 2, 3, 4]
print(liste2)    # Ausgabe: [1, 2, 3, 4]

In diesem Beispiel verweisen sowohl liste1 als auch liste2 auf dasselbe Listenobjekt. Wenn man liste2 ändert, wird liste1 ebenfalls geändert, da beide Variablen auf dasselbe Objekt verweisen. Korrekterweise dürfte man sowieso nicht sagen ich ändere liste2, sondern man müsste sagen ich ändere das Objekt, auf das liste2 verweist.

Verwirrungsgefahr

Wenn zwei Referenzen auf dieselbe Instanz einer Klasse zeigen, können Änderungen über eine Referenz unerwartete Auswirkungen darauf haben, was man unter der anderen Referenz sieht. Dies kann besonders verwirrend sein, wenn man nicht bewusst ist, dass beide Referenzen tatsächlich auf dasselbe Objekt im Speicher zeigen.

Stellen wir uns ein Szenario vor, bei dem wir eine Klasse Konto haben, die ein Bankkonto darstellt. Wir erstellen ein Kontoobjekt und verwenden zwei verschiedene Referenzen darauf.

class Konto:
    def __init__(self, inhaber: str, kontostand: float):
        self.inhaber = inhaber
        self.kontostand = kontostand

    def einzahlen(self, betrag: float):
        self.kontostand += betrag

    def abheben(self, betrag: float):
        if betrag <= self.kontostand:
            self.kontostand -= betrag
        else:
            print('Nicht genügend Guthaben.')

    def zeige_info(self):
        print(f'Inhaber: {self.inhaber}, Kontostand: {self.kontostand:.2f} EUR')

# Erstellen eines Konto-Objekts
konto1 = Konto('Alice', 1000.00)
# Erstellen einer zweiten Referenz auf dasselbe Konto-Objekt
konto2 = konto1

# Einzahlen über die erste Referenz
konto1.einzahlen(500.00)

# Anzeigen des Kontostands über beide Referenzen
konto1.zeige_info()  # Ausgabe: Inhaber: Alice, Kontostand: 1500.00 EUR
konto2.zeige_info()  # Ausgabe: Inhaber: Alice, Kontostand: 1500.00 EUR

# Abheben über die zweite Referenz
konto2.abheben(200.00)

# Anzeigen des Kontostands über beide Referenzen
konto1.zeige_info()  # Ausgabe: Inhaber: Alice, Kontostand: 1300.00 EUR
konto2.zeige_info()  # Ausgabe: Inhaber: Alice, Kontostand: 1300.00 EUR

Hier sind einige Szenarien, in denen diese Art von Verwirrung auftreten kann:

Unveränderbare Objekte

Unveränderbare (immutable) Python-Objekte wie Zahlen und Strings können nicht direkt geändert werden. Stattdessen wird bei jeder Änderung ein neues Objekt erzeugt. Dadurch merkt man oft nicht, dass Python mit Referenzen arbeitet, da Änderungen scheinbar lokal auf die jeweilige Variable beschränkt bleiben.

zahl1 = 10
zahl2 = zahl1

zahl2 += 5

print(zahl1)  # Ausgabe: 10
print(zahl2)  # Ausgabe: 15

Hier verweisen zahl1 und zahl2 zunächst auf dasselbe Integer-Objekt 10. Wenn zahl2 jedoch geändert wird (zahl2 += 5), nimmt Python für das Ergebnis ein anderes Integer-Objekt 15 und lässt zahl2 nun auf dieses andere Objekt verweisen, während zahl1 weiterhin auf 10 verweist.

Strings sind ebenfalls unveränderbar. Jede Änderung an einem String erzeugt ein neues String-Objekt.

string1 = 'Hallo'
string2 = string1

string2 += ', Welt!'

print(string1)  # Ausgabe: Hallo
print(string2)  # Ausgabe: Hallo, Welt!

Hier verweist string1 auf ein String-Objekt mit dem Inhalt Hallo und string2 auf dasselbe Objekt. Wenn string2 geändert wird, erzeugt Python ein neues String-Objekt Hallo, Welt! und string2 verweist nun auf dieses neue Objekt, während string1 weiterhin auf Hallo verweist.

Unveränderbare Objekte machen einige Arten von Fehlern (siehe oben unter Verwirrungsgefahr) unmöglich. Deshalb schreibt man oft auch eigene Klassen so, dass sie nie Veränderungen an einer einmal gebauten Instanz vornehmen.

Die Nullreferenz None

In Python ist None ein spezieller Wert, der verwendet wird, um das Fehlen eines konkreten Wertes zu kennzeichnen. (Pythons None entspricht in C und C++ der Nullzeiger.) Es ist ein einzelnes Objekt, das oft verwendet wird, um Variablen zu initialisieren oder anzuzeigen, dass eine Angabe fehlt oder dass eine Funktion nichts zurückgibt. Zum Beispiel gibt eine Funktion, die keinen return-Befehl enthält, automatisch None zurück. None ist auch nützlich, wenn Sie eine Variable anlegen wollen, die später einen Wert erhalten soll, aber vorerst leer ist.

Oft missverstanden: Eine Nullreferenz ist nicht etwas wie die Zahl 0 oder die leere Zeichenkette ''. Die Zahl 0 hat den Typ int und die leere Zeichenkette hat den Typ str, also niemals den Wert None. Die Nullreferenz ist vielmehr die Abwesenheit von konkreten Informationen. 0 Euro zu verdienen, heißt zu wissen, was man verdient (nämlich nichts). Ein Passwort, das keine Zeichen enthält, ist ein konkretes (wenn auch schlechtes) Passwort.

Wenn eine Variable wahlweise den Wert None haben kann, können Sie dies mit einem Type Hint der Art float | None darstellen. Wenn eine Funktion nichts zurückgeben soll, kann dies mit -> None angegeben werden. Zum Beispiel:

def beispiel_function(wert: float | None) -> None:
    if wert is None:
        print('Kein Wert angegeben')
    else:
        print(f'Der Wert ist {wert}')

Das logische Gegenteil von wert is None ist wert is not None, was für an C Gewohnte falsch aussieht. Die Schreibweisen wert == None bzw. wert != None werden nicht empfohlen, weil sie in ungewöhnlich programmierten Klassen überraschende Resultate haben können.

Nullreferenzen wie None hat deren Erfinder Tony Hoare später als seinen Milliarden-Dollar-Fehler bezeichnet, da die Einführung von Nullreferenzen in Programmiersprachen zu einer Vielzahl von Programmfehlern geführt habe, die in Summe Milliarden von Dollar gekosten hätten. Nullreferenzen verursachen häufig Programmabstürze und Sicherheitslücken, weil oft vergessen wird, ausreichend zu prüfen, ob eine Variable (je nach Programmiersprache) None, NULL, nullptr, null ist, bevor man darauf zugreift. Inzwischen bieten viele Programmiersprachen besondere Sicherheitsmechanismen für Nullreferenzen oder sogar Alternativen zu Nullreferenzen. In Python sind vor allem die Type Hints zu nennen, die angeben können, dass None nicht als Eingabe oder Ergebnis erlaubt ist. Wenn es laut Programmcode trotzdem auftreten könnte, warnt dann die Typprüfung davor.

Vergleiche auf Identität und Klassenzugehörigkeit

In Python gibt es zwei Möglichkeiten, Objekte zu vergleichen: den Operator == und den Operator is. Obwohl sie ähnlich aussehen, prüfen sie völlig unterschiedliche Dinge.

Obendrein kann man prüfen, ob ein Objekt von einer bestimmten Klasse ist. Dazu dient die Funktion isinstance:

isinstance('Hallo', str)  # True
isinstance(5, int)        # True
isinstance(3.14, int)     # False

Weil jede Unterklasse perfekt ihre Oberklasse vertreten können muss, wird isinstance auch True, wenn man nach einer (Ober-)Oberklasse der Instanz fragt.

Vokabular der Objektorientierung

OOP bringt einige Vokabeln mit sich, die in der Fachsprache der Informatik völlig andere Bedeutungen haben als im Alltag. Im Laufe des Semesters sollten Sie lernen, die folgenden Vokabeln selbst zu verwenden: