30. Klassen und Objekte einführen
30.1) Die Idee der objektorientierten Programmierung (Eigene Typen erstellen)
In diesem Buch haben Sie durchgehend mit Pythons eingebauten Typen gearbeitet: Ganzzahlen, Strings, Listen, Dictionaries und mehr. Jeder Typ bündelt Daten (wie die Zeichen in einem String) mit Operationen, die Sie auf diesen Daten ausführen können (wie .upper() oder .split()). Diese Kombination aus Daten und Verhalten ist leistungsfähig – sie ermöglicht es Ihnen, Strings als vollständige Einheiten mit eigenen Fähigkeiten zu betrachten, nicht nur als rohe Zeichenfolgen.
Objektorientierte Programmierung (object-oriented programming, OOP) erweitert diese Idee: Sie erlaubt Ihnen, Ihre eigenen benutzerdefinierten Typen zu erstellen, genannt Klassen (classes), die Daten und Verhalten bündeln, die spezifisch für Ihren Problembereich sind. So wie Python einen str-Typ für die Arbeit mit Text und einen list-Typ für die Arbeit mit Sequenzen bereitstellt, können Sie einen BankAccount-Typ zur Verwaltung finanzieller Transaktionen, einen Student-Typ zum Verfolgen akademischer Daten oder einen Product-Typ für ein Inventarsystem erstellen.
Warum eigene Typen erstellen?
Stellen Sie sich vor, Sie verwalten Informationen über Schüler/Studierende in einem Schulsystem. Ohne Klassen könnten Sie separate Variablen oder Dictionaries verwenden:
# Verwendung separater Variablen – wird schnell unübersichtlich
student1_name = "Alice Johnson"
student1_id = "S12345"
student1_gpa = 3.8
student2_name = "Bob Smith"
student2_id = "S12346"
student2_gpa = 3.5
# Oder Verwendung von Dictionaries – besser, aber immer noch eingeschränkt
student1 = {"name": "Alice Johnson", "id": "S12345", "gpa": 3.8}
student2 = {"name": "Bob Smith", "id": "S12346", "gpa": 3.5}Dieser Ansatz funktioniert für einfache Fälle, aber er hat Einschränkungen:
- Keine Validierung: Nichts hindert Sie daran,
gpaauf einen ungültigen Wert wie-5.0oder"excellent"zu setzen - Kein zusammengehöriges Verhalten: Operationen wie das Berechnen eines Ehrenstatus (Honors-Status) oder das Formatieren von Studierendeninformationen sind separate Funktionen, die überall in Ihrem Code verstreut sind
- Keine Typprüfung: Ein Dictionary, das einen Studierenden repräsentiert, sieht identisch aus wie jedes andere Dictionary – Python kann Ihnen nicht helfen, Fehler zu erkennen, bei denen Sie versehentlich ein Produkt-Dictionary verwenden, wo ein Studierenden-Dictionary erwartet wurde
Klassen lösen diese Probleme, indem sie Ihnen erlauben, einen neuen Typ zu definieren, der genau repräsentiert, was ein Student ist und welche Operationen für Studierende sinnvoll sind:
# Darauf arbeiten wir hin – eine Student-Klasse, die Daten und Verhalten bündelt
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def is_honors(self):
return self.gpa >= 3.5
def display_info(self):
status = "Honors" if self.is_honors() else "Regular"
return f"{self.name} ({self.student_id}) - GPA: {self.gpa} [{status}]"
# Jetzt können wir Student-Objekte erstellen
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
print(alice.display_info()) # Output: Alice Johnson (S12345) - GPA: 3.8 [Honors]
print(bob.is_honors()) # Output: TrueDieses Kapitel wird Ihnen beibringen, wie Sie solche Klassen von Grund auf aufbauen. Wir beginnen mit den einfachstmöglichen Klassen und fügen schrittweise Funktionen hinzu, bis Sie reichhaltige, nützliche benutzerdefinierte Typen erstellen können.
Klassen vs. Instanzen: Die Blaupausen-Analogie
Das Verständnis des Unterschieds zwischen einer Klasse (class) und einer Instanz (instance) ist grundlegend für die objektorientierte Programmierung:
-
Eine Klasse (class) ist wie eine Blaupause oder Vorlage. Sie definiert, welche Daten ein Objekttyp enthalten wird und welche Operationen er ausführen kann. Die Klasse selbst ist kein konkreter Studierender – sie ist die Definition dessen, was es bedeutet, ein Studierender zu sein.
-
Eine Instanz (instance) (auch Objekt (object) genannt) ist ein konkretes Beispiel, das aus dieser Blaupause erstellt wurde. Wenn Sie
alice = Student("Alice Johnson", "S12345", 3.8)erstellen, erzeugen Sie eine konkrete Studierendeninstanz mit Alices spezifischen Daten.
Sie können so viele Instanzen erstellen, wie Sie von einer einzigen Klasse benötigen – genauso wie ein Architekt eine Blaupause verwenden kann, um viele Häuser zu bauen. Jede Instanz hat ihre eigenen Daten (Alices GPA ist anders als Bobs), aber sie alle teilen sich dieselbe Struktur und dieselben Fähigkeiten, die durch die Klasse definiert sind.
Was Sie in diesem Kapitel lernen werden
Dieses Kapitel führt die Kernkonzepte der objektorientierten Programmierung in Python ein:
- Klassen definieren mit dem Schlüsselwort
class - Instanzen erstellen und auf ihre Attribute zugreifen
- Methoden (methods) hinzufügen, die auf Instanzdaten arbeiten
selfverstehen und wie Methoden auf Instanzdaten zugreifen- Instanzen initialisieren mit der Methode
__init__ - String-Repräsentationen (string representations) steuern mit
__str__und__repr__ - Mehrere unabhängige Instanzen erstellen aus derselben Klasse
Am Ende dieses Kapitels werden Sie in der Lage sein, Ihre eigenen benutzerdefinierten Typen zu entwerfen und zu implementieren, die Ihre Programme besser organisiert, wartbarer und ausdrucksstärker machen. Wir bauen in Kapitel 31 mit fortgeschritteneren Eigenschaften und Funktionalitäten von Klassen auf diesen Grundlagen auf und in Kapitel 32 mit Vererbung und Polymorphie.
30.2) Einfache Klassen mit class definieren
Beginnen wir damit, die einfachstmögliche Klasse zu erstellen – eine, die nur einen neuen Typ definiert, ohne zunächst Daten oder Verhalten.
Das Schlüsselwort class
Sie definieren eine Klasse mit dem Schlüsselwort class, gefolgt vom Klassennamen und einem Doppelpunkt:
class Student:
pass # Leere Klasse fürs Erste
# Eine Instanz erstellen
alice = Student()
print(alice) # Output: <__main__.Student object at 0x...>
print(type(alice)) # Output: <class '__main__.Student'>Selbst diese minimale Klasse ist nützlich – sie erstellt einen neuen Typ namens Student. Wenn Sie eine Instanz mit alice = Student() erstellen, erzeugt Python ein neues Objekt vom Typ Student. Die Ausgabe zeigt, dass alice tatsächlich ein Student-Objekt ist, auch wenn es noch nichts Interessantes tut.
Namenskonventionen für Klassen
Python-Klassennamen folgen einer spezifischen Konvention namens CapWords oder PascalCase: Jedes Wort beginnt mit einem Großbuchstaben, ohne Unterstriche zwischen den Wörtern:
class BankAccount: # Gut: CapWords
pass
class ProductInventory: # Gut: CapWords
pass
class HTTPRequest: # Gut: Akronyme sind in Großbuchstaben
pass
# Vermeiden Sie diese Stile für Klassen:
# class bank_account: # Wrong: snake_case is for functions/variables
# class bankaccount: # Wrong: hard to read
# class BANKACCOUNT: # Wrong: ALL_CAPS is for constantsDiese Konvention hilft dabei, Klassen von Funktionen und Variablen zu unterscheiden (die snake_case verwenden), wenn man Code liest.
Instanzen erstellen
Eine Instanz aus einer Klasse zu erstellen sieht aus wie ein Funktionsaufruf – Sie verwenden den Klassennamen, gefolgt von Klammern:
class Product:
pass
# Drei verschiedene Produktinstanzen erstellen
item1 = Product()
item2 = Product()
item3 = Product()
# Jede Instanz ist ein separates Objekt
print(item1) # Output: <__main__.Product object at 0x...>
print(item2) # Output: <__main__.Product object at 0x...>
print(item3) # Output: <__main__.Product object at 0x...>
# Es sind unterschiedliche Objekte, obwohl sie denselben Typ haben
print(item1 is item2) # Output: False
print(type(item1) is type(item2)) # Output: TrueJeder Aufruf von Product() erstellt eine neue, unabhängige Instanz. Die Speicheradressen (der 0x...-Teil) sind verschieden und bestätigen, dass es sich um separate Objekte im Speicher handelt.
Warum mit leeren Klassen anfangen?
Sie fragen sich vielleicht, warum wir mit Klassen beginnen, die nichts tun. Dafür gibt es zwei Gründe:
-
Konzeptionelle Klarheit: Zu verstehen, dass eine Klasse einfach nur ein neuer Typ ist – getrennt von ihren Daten und ihrem Verhalten – hilft Ihnen, das grundlegende Konzept zu erfassen, bevor Komplexität hinzukommt.
-
Praktischer Nutzen: Selbst leere Klassen können als Marker oder Platzhalter nützlich sein. Zum Beispiel könnten Sie eigene Exception-Typen definieren:
class InvalidGradeError:
pass
class StudentNotFoundError:
pass
# Diese leeren Klassen dienen als unterschiedliche FehlertypenAllerdings sind leere Klassen in echtem Code selten. Fügen wir einige Daten hinzu, um unsere Klassen nützlich zu machen.
30.3) Instanzen erstellen und auf Attribute zugreifen
Klassen werden nützlich, wenn sie Daten enthalten. In Python können Sie Attribute (attributes) (Daten, die an eine Instanz gebunden sind) jederzeit hinzufügen, indem Sie ihnen einfach Werte zuweisen.
Attribute zu Instanzen hinzufügen
Sie können Attribute zu einer Instanz mit Punktnotation hinzufügen:
class Student:
pass
# Eine Instanz erstellen
alice = Student()
# Attribute hinzufügen
alice.name = "Alice Johnson"
alice.student_id = "S12345"
alice.gpa = 3.8
# Auf Attribute zugreifen
print(alice.name) # Output: Alice Johnson
print(alice.student_id) # Output: S12345
print(alice.gpa) # Output: 3.8Der Punkt-Operator (.) greift auf Attribute zu: alice.name bedeutet „hole das Attribut name des Objekts alice“. Das ist dieselbe Syntax, die Sie bereits bei Strings (wie text.upper()) und Listen (wie numbers.append(5)) verwendet haben – dort greifen Sie auf Methoden und Attribute dieser Objekte zu.
Jede Instanz hat ihre eigenen Attribute
Verschiedene Instanzen derselben Klasse haben unabhängige Attribute:
class Student:
pass
# Zwei Studierende erstellen
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
# Jede Instanz hat ihre eigenen Daten
print(alice.name) # Output: Alice Johnson
print(bob.name) # Output: Bob Smith
# Eine Änderung wirkt sich nicht auf die andere aus
alice.gpa = 3.9
print(alice.gpa) # Output: 3.9
print(bob.gpa) # Output: 3.5 (unchanged)Diese Unabhängigkeit ist entscheidend: alice und bob sind separate Objekte mit separaten Daten. Wenn Sie alice.gpa ändern, beeinflusst das nicht bob.gpa.
Attribute können jeden Typ haben
Attribute sind nicht auf einfache Typen beschränkt – sie können jeden Python-Wert enthalten:
class Student:
pass
student = Student()
student.name = "Carol Davis"
student.grades = [95, 88, 92, 90] # Listenattribut
student.contact = { # Dictionary-Attribut
"email": "carol@example.com",
"phone": "555-0123"
}
student.is_active = True # Boolean-Attribut
# Auf verschachtelte Daten zugreifen
print(student.grades[0]) # Output: 95
print(student.contact["email"]) # Output: carol@example.comDiese Flexibilität ermöglicht es Ihnen, komplexe reale Entitäten mit reichhaltigen Datenstrukturen zu modellieren.
Zugriff auf nicht vorhandene Attribute
Wenn Sie versuchen, auf ein Attribut zuzugreifen, das nicht existiert, wird ein AttributeError ausgelöst:
class Student:
pass
student = Student()
student.name = "David Lee"
print(student.name) # Output: David Lee
# print(student.age) # AttributeError: 'Student' object has no attribute 'age'Dieser Fehler ist hilfreich – er fängt Tippfehler und Logikfehler ab, bei denen Sie erwarten, dass ein Attribut existiert, es aber nicht tut.
Das Problem mit manueller Attributzuweisung
Obwohl Sie Attribute nach dem Erstellen einer Instanz manuell hinzufügen können, hat dieser Ansatz ernste Nachteile:
class Student:
pass
# Man vergisst leicht Attribute oder vertippt sie
alice = Student()
alice.name = "Alice Johnson"
alice.student_id = "S12345"
# Forgot to set gpa!
bob = Student()
bob.name = "Bob Smith"
bob.stuent_id = "S12346" # Typo: stuent instead of student
bob.gpa = 3.5
# Jetzt fehlt alice gpa, und bob hat einen Tippfehler
# print(alice.gpa) # AttributeError
# print(bob.student_id) # AttributeErrorDas ist fehleranfällig und mühsam. Sie brauchen eine Möglichkeit sicherzustellen, dass jede Instanz mit den richtigen Attributen startet. Genau hier kommt die Methode __init__ ins Spiel, die wir in Abschnitt 30.5 behandeln. Aber zuerst lernen wir etwas über Methoden – Funktionen, die zu einer Klasse gehören.
30.4) Instanzmethoden hinzufügen: self verstehen
Methoden sind Funktionen (functions), die innerhalb einer Klasse definiert sind und auf Instanzdaten arbeiten. Sie geben Ihren Klassen Verhalten, nicht nur Daten.
Eine einfache Methode definieren
Fügen wir unserer Student-Klasse eine Methode hinzu:
class Student:
def display_info(self):
print(f"{self.name} - GPA: {self.gpa}")
# Eine Instanz erstellen und Attribute hinzufügen
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
# Die Methode aufrufen
alice.display_info() # Output: Alice Johnson - GPA: 3.8Die Methode display_info wird innerhalb der Klasse mit def definiert, genau wie normale Funktionen. Der entscheidende Unterschied ist der erste Parameter: self.
self verstehen
Der Parameter self ist die Art, wie eine Methode auf die konkrete Instanz zugreift, auf der sie arbeitet. Wenn Sie alice.display_info() aufrufen, übergibt Python automatisch alice als erstes Argument an die Methode. Innerhalb der Methode bezieht sich self auf alice, sodass self.name auf alice.name zugreift und self.gpa auf alice.gpa.
Hier sehen Sie, was im Hintergrund passiert:
class Student:
def display_info(self):
print(f"{self.name} - GPA: {self.gpa}")
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
# Diese beiden Aufrufe sind gleichwertig:
alice.display_info() # Normale Art
Student.display_info(alice) # Was Python tatsächlich macht
# Beide geben aus: Alice Johnson - GPA: 3.8Wenn Sie alice.display_info() schreiben, übersetzt Python das zu Student.display_info(alice). Die Instanz (alice) wird innerhalb der Methode zum Parameter self.
Warum „self“?
Der Name self ist eine Konvention, kein Schlüsselwort. Technisch könnten Sie jeden Namen verwenden:
class Student:
def display_info(this): # Funktioniert, aber machen Sie das nicht
print(f"{this.name} - GPA: {this.gpa}")Verwenden Sie jedoch immer self. Es ist eine universelle Python-Konvention, die Ihren Code für andere Python-Programmierer lesbar macht. Einen anderen Namen zu verwenden, verwirrt Leser und verstößt gegen Community-Standards.
Methoden mit mehreren Instanzen
Die Stärke von self wird deutlich, wenn Sie mehrere Instanzen haben:
class Student:
def display_info(self):
print(f"{self.name} - GPA: {self.gpa}")
# Zwei Studierende erstellen
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
# Dieselbe Methode, unterschiedliche Daten
alice.display_info() # Output: Alice Johnson - GPA: 3.8
bob.display_info() # Output: Bob Smith - GPA: 3.5Wenn Sie alice.display_info() aufrufen, ist self gleich alice. Wenn Sie bob.display_info() aufrufen, ist self gleich bob. Derselbe Methodencode funktioniert für jede Instanz, weil self sich an die Instanz anpasst, die die Methode aufgerufen hat.
Methoden können zusätzliche Parameter annehmen
Methoden können neben self weitere Parameter akzeptieren:
class Student:
def update_gpa(self, new_gpa):
self.gpa = new_gpa
print(f"Updated {self.name}'s GPA to {self.gpa}")
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
alice.update_gpa(3.9) # Output: Updated Alice Johnson's GPA to 3.9
print(alice.gpa) # Output: 3.9Wenn Sie alice.update_gpa(3.9) aufrufen, übergibt Python alice als self und 3.9 als new_gpa. Die Methodensignatur ist def update_gpa(self, new_gpa), aber beim Aufruf übergeben Sie nur ein Argument – Python übernimmt self automatisch.
Methoden können Werte zurückgeben
Methoden können Werte zurückgeben, genau wie normale Funktionen:
class Student:
def is_honors(self):
return self.gpa >= 3.5
def get_status(self):
if self.is_honors():
return "Honors Student"
else:
return "Regular Student"
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.2
print(alice.get_status()) # Output: Honors Student
print(bob.get_status()) # Output: Regular StudentBeachten Sie, wie get_status eine andere Methode (is_honors) mit self.is_honors() aufruft. Methoden können andere Methoden auf derselben Instanz aufrufen.
Methoden vs. Funktionen: Wann man welches verwendet
Sie fragen sich vielleicht, wann Sie eine Methode gegenüber einer eigenständigen Funktion verwenden sollten. Hier ist die Richtlinie:
Verwenden Sie eine Methode, wenn die Operation:
- Zugriff auf Instanzdaten benötigt (
self.name,self.gpausw.) - logisch zum Typ gehört (es ist etwas, das ein Student tut oder ist)
- den Zustand der Instanz verändert
Verwenden Sie eine eigenständige Funktion, wenn die Operation:
- keine Instanzdaten benötigt
- mit mehreren Typen arbeitet
- ein allgemeines Hilfswerkzeug ist
class Student:
# Methode: braucht Instanzdaten
def is_honors(self):
return self.gpa >= 3.5
# Funktion: allgemeines Hilfswerkzeug, funktioniert mit jedem GPA-Wert
def calculate_letter_grade(gpa):
if gpa >= 3.7:
return "A"
elif gpa >= 3.0:
return "B"
elif gpa >= 2.0:
return "C"
else:
return "D"
alice = Student()
alice.gpa = 3.8
# Verwenden Sie die Methode für instanzspezifische Prüfungen
print(alice.is_honors()) # Output: True
# Verwenden Sie die Funktion für allgemeine Berechnungen
print(calculate_letter_grade(alice.gpa)) # Output: A
print(calculate_letter_grade(2.5)) # Output: CHäufige Methodenmuster
Hier sind einige häufige Muster, die Sie oft verwenden werden:
Getter-Methoden (abgeleitete Informationen abrufen):
class Student:
def get_full_info(self):
return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"Setter-Methoden (Attribute mit Validierung ändern):
class Student:
def set_gpa(self, new_gpa):
if 0.0 <= new_gpa <= 4.0:
self.gpa = new_gpa
else:
print("Invalid GPA: must be between 0.0 and 4.0")Query-Methoden (Ja/Nein-Fragen beantworten):
class Student:
def is_honors(self):
return self.gpa >= 3.5
def is_failing(self):
return self.gpa < 2.0Action-Methoden (Operationen ausführen):
class Student:
def add_grade(self, grade):
self.grades.append(grade)
# GPA basierend auf allen grades neu berechnen
self.gpa = sum(self.grades) / len(self.grades)30.5) Instanzen mit __init__ initialisieren
Attribute nach dem Erstellen einer Instanz manuell zu setzen ist mühsam und fehleranfällig. Die Methode __init__ löst dieses Problem, indem sie Ihnen erlaubt, Instanzen beim Erstellen mit Daten zu initialisieren.
Die Methode __init__
Die Methode __init__ (ausgesprochen „dunder init“ oder „init“) ist eine spezielle Methode, die Python automatisch aufruft, wenn Sie eine neue Instanz erstellen:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
# Instanzen mit Anfangsdaten erstellen
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
print(alice.name) # Output: Alice Johnson
print(bob.gpa) # Output: 3.5Wenn Sie Student("Alice Johnson", "S12345", 3.8) schreiben, macht Python:
- Es erstellt eine neue leere
Student-Instanz - Es ruft
__init__mit dieser Instanz alsselfund Ihren Argumenten auf - Es gibt die initialisierte Instanz zurück
Die Methode __init__ gibt keinen Wert explizit zurück – sie verändert die Instanz „in place“, indem sie ihre Attribute setzt. Wenn Sie versuchen, einen Wert aus __init__ zurückzugeben, löst Python einen TypeError aus.
class Student:
def __init__(self, name):
self.name = name
# Geben Sie aus __init__ nichts zurück
# return self # Wrong! TypeError: __init__() should return None, not 'Student'Wie __init__ funktioniert
Zerlegen wir Schritt für Schritt, was passiert:
class Student:
def __init__(self, name, student_id, gpa):
print(f"Initializing student: {name}")
self.name = name
self.student_id = student_id
self.gpa = gpa
print(f"Initialization complete")
alice = Student("Alice Johnson", "S12345", 3.8)
# Output:
# Initializing student: Alice Johnson
# Initialization complete
print(alice.name) # Output: Alice JohnsonDie Parameter nach self (name, student_id, gpa) werden zu erforderlichen Argumenten beim Erstellen einer Instanz. Wenn Sie sie nicht angeben, löst Python einen TypeError aus:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
# student = Student() # TypeError: __init__() missing 3 required positional arguments
# student = Student("Alice") # TypeError: __init__() missing 2 required positional arguments
student = Student("Alice Johnson", "S12345", 3.8) # CorrectDas ist viel besser als manuelle Attributzuweisung – Python erzwingt, dass jede Instanz mit den erforderlichen Daten startet.
Standardparameterwerte in __init__
Sie können Standardparameterwerte in __init__ verwenden, genau wie bei normalen Funktionen:
class Student:
def __init__(self, name, student_id, gpa=0.0):
self.name = name
self.student_id = student_id
self.gpa = gpa
# GPA ist optional und hat den Standardwert 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346") # Verwendet default gpa=0.0
print(alice.gpa) # Output: 3.8
print(bob.gpa) # Output: 0.0Das ist nützlich für Attribute, die sinnvolle Standardwerte haben, aber bei Bedarf angepasst werden können.
Validierung in __init__
Sie können Eingaben in __init__ validieren, um sicherzustellen, dass Instanzen in einem gültigen Zustand starten:
class Student:
def __init__(self, name, student_id, gpa):
if not name:
print("Error: Name cannot be empty")
self.name = "Unknown"
else:
self.name = name
self.student_id = student_id
if 0.0 <= gpa <= 4.0:
self.gpa = gpa
else:
print(f"Warning: Invalid GPA {gpa}, setting to 0.0")
self.gpa = 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice.gpa) # Output: 3.8
bob = Student("", "S12346", 5.0)
# Output:
# Error: Name cannot be empty
# Warning: Invalid GPA 5.0, setting to 0.0
print(bob.name) # Output: Unknown
print(bob.gpa) # Output: 0.0Das stellt sicher, dass selbst dann, wenn jemand ungültige Daten übergibt, die Instanz in einem vernünftigen Zustand landet.
30.6) String-Repräsentationen mit __str__ und __repr__
Wenn Sie eine Instanz mit print() ausgeben oder sie in der interaktiven Shell ansehen, muss Python sie in einen String umwandeln. Standardmäßig erhalten Sie etwas wenig Hilfreiches:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice) # Output: <__main__.Student object at 0x...>Die Standardausgabe zeigt den Klassennamen und die Speicheradresse, aber nichts über Alices tatsächliche Daten. Sie können das mit den speziellen Methoden __str__ und __repr__ anpassen.
Die Methode __str__
Die Methode __str__ definiert, wie Ihre Instanzen von print() und str() in Strings umgewandelt werden:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def __str__(self):
return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice) # Output: Alice Johnson (S12345) - GPA: 3.8
print(str(alice)) # Output: Alice Johnson (S12345) - GPA: 3.8Die Methode __str__ sollte einen String zurückgeben, der für Endnutzer lesbar und informativ ist. Betrachten Sie sie als die „freundliche“ Darstellung.
Die Methode __repr__
Die Methode __repr__ definiert die „offizielle“ String-Darstellung Ihrer Instanzen, die vom REPL und repr() verwendet wird:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def __repr__(self):
return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
alice = Student("Alice Johnson", "S12345", 3.8)
print(repr(alice)) # Output: Student('Alice Johnson', 'S12345', 3.8)Im REPL:
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)Die Methode __repr__ sollte einen String zurückgeben, der wie gültiger Python-Code aussieht, um das Objekt neu zu erstellen. Betrachten Sie sie als die „Entwickler“-Darstellung – sie sollte eindeutig sein und beim Debugging nützlich.
Sowohl __str__ als auch __repr__ verwenden
Sie können beide Methoden für unterschiedliche Zwecke definieren:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def __str__(self):
# Freundliches, gut lesbares Format
return f"{self.name} - GPA: {self.gpa}"
def __repr__(self):
# Eindeutiges, code-ähnliches Format
return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice) # Verwendet __str__
# Output: Alice Johnson - GPA: 3.8
print(repr(alice)) # Verwendet __repr__
# Output: Student('Alice Johnson', 'S12345', 3.8)Im REPL:
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice # Uses __repr__
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice) # Uses __str__
Alice Johnson - GPA: 3.8Wann man welche Methode definieren sollte
Hier ist die Richtlinie:
- Definieren Sie immer
__repr__: Es wird vom REPL und von Debugging-Tools verwendet. Wenn Sie nur eine definieren, dann diese. - Definieren Sie
__str__, wenn Sie ein benutzerfreundliches Format benötigen: Wenn Ihre Klasse für Endnutzer ausgegeben werden soll, stellen Sie ein lesbares__str__bereit. - Wenn Sie nur
__repr__definieren: Python verwendet es fürrepr(), undstr()fällt ebenfalls auf__repr__zurück (also verwendet auchprint()es). - Wenn Sie nur
__str__definieren:print()verwendet__str__, aberrepr()und das REPL verwenden das Standard-__repr__(zeigt die Speicheradresse). Deshalb ist das Definieren von__repr__in der Regel wichtiger.
# Nur __repr__ definiert
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"Product('{self.name}', {self.price})"
item = Product("Laptop", 999.99)
print(item) # Verwendet __repr__ als Fallback
# Output: Product('Laptop', 999.99)
print(repr(item)) # Verwendet __repr__
# Output: Product('Laptop', 999.99)String-Repräsentation in Collections
Wenn Instanzen in Collections (Listen, Dicts usw.) enthalten sind, verwendet Python zur Darstellung __repr__, nicht __str__:
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
def __str__(self):
return f"{self.name}: {self.gpa}"
def __repr__(self):
return f"Student('{self.name}', {self.gpa})"
students = [
Student("Alice", 3.8),
Student("Bob", 3.5),
Student("Carol", 3.9)
]
# Das Ausgeben der Liste verwendet __repr__ für jeden student
print(students)
# Output: [Student('Alice', 3.8), Student('Bob', 3.5), Student('Carol', 3.9)]
# Das Ausgeben einzelner students verwendet __str__
for student in students:
print(student)
# Output:
# Alice: 3.8
# Bob: 3.5
# Carol: 3.9Deshalb sollte __repr__ eindeutig sein – es hilft Ihnen zu verstehen, was in Ihren Datenstrukturen steckt, während Sie debuggen. Wenn Sie eine Liste ausgeben, ruft Python im Wesentlichen repr() für jedes Element auf, um die Struktur klar darzustellen.
30.7) Mehrere unabhängige Instanzen erstellen
Einer der mächtigsten Aspekte von Klassen ist, dass Sie viele unabhängige Instanzen erstellen können, jede mit ihren eigenen Daten. Sehen wir uns das im Detail an.
Jede Instanz hat ihre eigenen Daten
Wenn Sie mehrere Instanzen aus derselben Klasse erstellen, verwaltet jede ihre eigenen separaten Attribute:
class BankAccount:
def __init__(self, account_number, holder_name, balance=0.0):
self.account_number = account_number
self.holder_name = holder_name
self.balance = balance
def deposit(self, amount):
self.balance += amount
print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")
return True
else:
print(f"Insufficient funds. Balance: ${self.balance:.2f}")
return False
def __str__(self):
return f"{self.holder_name}'s account ({self.account_number}): ${self.balance:.2f}"
# Drei unabhängige Konten erstellen
alice_account = BankAccount("ACC-001", "Alice Johnson", 1000.0)
bob_account = BankAccount("ACC-002", "Bob Smith", 500.0)
carol_account = BankAccount("ACC-003", "Carol Davis", 2000.0)
# Operationen an einem Konto beeinflussen andere nicht
alice_account.deposit(500)
# Output: Deposited $500.00. New balance: $1500.00
bob_account.withdraw(200)
# Output: Withdrew $200.00. New balance: $300.00
# Jedes Konto behält seinen eigenen Kontostand
print(alice_account) # Output: Alice Johnson's account (ACC-001): $1500.00
print(bob_account) # Output: Bob Smith's account (ACC-002): $300.00
print(carol_account) # Output: Carol Davis's account (ACC-003): $2000.00Diese Unabhängigkeit ist grundlegend für objektorientierte Programmierung. Jede Instanz ist eine separate Einheit mit ihrem eigenen Zustand.
Instanzen in Collections
Sie können Instanzen in Listen, Dictionaries oder jeder anderen Collection speichern:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def is_honors(self):
return self.gpa >= 3.5
def __repr__(self):
return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
# Eine Liste von Studierenden erstellen
students = [
Student("Alice Johnson", "S12345", 3.8),
Student("Bob Smith", "S12346", 3.2),
Student("Carol Davis", "S12347", 3.9),
Student("David Lee", "S12348", 3.4)
]
# Alle Studierenden mit Honors-Status finden
honors_students = []
for student in students:
if student.is_honors():
honors_students.append(student)
print("Honors students:")
for student in honors_students:
print(f" {student.name}: {student.gpa}")
# Output:
# Honors students:
# Alice Johnson: 3.8
# Carol Davis: 3.9
# Durchschnittlichen GPA berechnen
total_gpa = sum(student.gpa for student in students)
average_gpa = total_gpa / len(students)
print(f"Average GPA: {average_gpa:.2f}") # Output: Average GPA: 3.58Das ist ein häufiges Muster: mehrere Instanzen erstellen, sie in einer Collection speichern und sie dann mit Schleifen und Comprehensions verarbeiten.
Instanzen können auf andere Instanzen verweisen
Instanzen können Attribute haben, die auf andere Instanzen verweisen, wodurch Beziehungen zwischen Objekten entstehen:
class Course:
def __init__(self, course_code, course_name):
self.course_code = course_code
self.course_name = course_name
def __str__(self):
return f"{self.course_code}: {self.course_name}"
class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self.courses = [] # Liste von Course-Instanzen
def enroll(self, course):
self.courses.append(course)
print(f"{self.name} enrolled in {course.course_name}")
def list_courses(self):
print(f"{self.name}'s courses:")
for course in self.courses:
print(f" {course}")
# Kurse erstellen
python_course = Course("CS101", "Introduction to Python")
data_course = Course("CS102", "Data Structures")
web_course = Course("CS103", "Web Development")
# Studierende erstellen und sie in Kurse einschreiben
alice = Student("Alice Johnson", "S12345")
alice.enroll(python_course)
alice.enroll(data_course)
# Output:
# Alice Johnson enrolled in Introduction to Python
# Alice Johnson enrolled in Data Structures
bob = Student("Bob Smith", "S12346")
bob.enroll(python_course)
bob.enroll(web_course)
# Output:
# Bob Smith enrolled in Introduction to Python
# Bob Smith enrolled in Web Development
# Kurse jedes Studierenden auflisten
alice.list_courses()
# Output:
# Alice Johnson's courses:
# CS101: Introduction to Python
# CS102: Data Structures
bob.list_courses()
# Output:
# Bob Smith's courses:
# CS101: Introduction to Python
# CS103: Web DevelopmentBeachten Sie, dass sowohl Alice als auch Bob in python_course eingeschrieben sind – sie verweisen auf dieselbe Course-Instanz. Das modelliert die Beziehung aus der realen Welt, in der mehrere Studierende denselben Kurs belegen können.
Instanzidentität und Gleichheit
Jede Instanz ist ein eindeutiges Objekt, auch wenn sie dieselben Daten wie eine andere Instanz hat:
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
alice1 = Student("Alice", 3.8)
alice2 = Student("Alice", 3.8)
# Unterschiedliche Objekte, selbst mit identischen Daten
print(alice1 is alice2) # Output: False
print(id(alice1) == id(alice2)) # Output: FalseStandardmäßig verhält sich == ebenfalls so, als würde es die Identität prüfen (also ob es dasselbe Objekt ist), nicht ob die Objekte dieselben Daten haben. In Kapitel 31 lernen wir, wie man den Gleichheitsvergleich mit der speziellen Methode __eq__ anpasst.
Dieses Kapitel hat Sie in die Grundlagen der objektorientierten Programmierung in Python eingeführt. Sie haben gelernt, wie man Klassen definiert, Instanzen erstellt, Methoden hinzufügt, Instanzen mit __init__ initialisiert, String-Repräsentationen steuert und mit mehreren unabhängigen Instanzen arbeitet. Diese Konzepte bilden die Grundlage für fortgeschrittenere OOP-Features, die wir in den Kapiteln 31 und 32 erkunden werden.