Python & AI Tutorials Logo
Python Programmierung

31. Fortgeschrittene Klassenfunktionen

In Kapitel 30 haben wir gelernt, wie man grundlegende Klassen mit Instanzattributen und Methoden erstellt. Jetzt werden wir fortgeschrittenere Klassenfunktionen erkunden, die Ihnen eine fein abgestufte Kontrolle darüber geben, wie sich Ihre Objekte verhalten. Diese Funktionen ermöglichen es Ihnen, Klassen zu erstellen, die sich wie eingebaute Python-Typen anfühlen, mit natürlicher Syntax für Operationen wie Addition, Vergleich und Indexierung.

31.1) Klassenvariablen vs. Instanzvariablen

Wenn wir Attribute in einer Klasse erstellen, haben wir zwei grundsätzlich verschiedene Orte, um sie zu speichern: in der Klasse selbst oder in einzelnen Instanzen. Diese Unterscheidung zu verstehen ist entscheidend, um korrekten objektorientierten Code zu schreiben.

31.1.1) Instanzvariablen verstehen

Instanzvariablen (instance variables) sind Attribute, die zu einem bestimmten Objekt gehören. Jede Instanz hat ihre eigene separate Kopie dieser Variablen. Wir haben Instanzvariablen in Kapitel 30 durchgehend verwendet – es sind die Attribute, die wir in __init__ mit self erstellen:

python
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner      # Instanzvariable
        self.balance = balance  # Instanzvariable
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.balance)  # Output: 1000
print(account2.balance)  # Output: 500

Jede BankAccount-Instanz hat ihr eigenes owner und balance. Das Ändern von account1.balance beeinflusst account2.balance nicht – sie sind vollständig unabhängig.

31.1.2) Klassenvariablen verstehen

Klassenvariablen (class variables) sind Attribute, die zur Klasse selbst gehören, nicht zu einer bestimmten Instanz. Alle Instanzen teilen sich dieselbe Klassenvariable. Wir definieren Klassenvariablen direkt im Klassenrumpf, außerhalb jeder Methode:

python
class BankAccount:
    interest_rate = 0.02  # Klassenvariable – von allen Instanzen geteilt
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
    
    def apply_interest(self):
        self.balance += self.balance * BankAccount.interest_rate
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.interest_rate)  # Output: 0.02
print(account2.interest_rate)  # Output: 0.02
print(BankAccount.interest_rate)  # Output: 0.02

Beachten Sie, dass wir interest_rate über Instanzen (account1.interest_rate) oder über die Klasse selbst (BankAccount.interest_rate) zugreifen können. Beide beziehen sich auf dieselbe Variable.

Hier ist, was Klassenvariablen mächtig macht – wenn wir die Klassenvariable ändern, sehen alle Instanzen die Änderung:

python
class BankAccount:
    interest_rate = 0.02
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.interest_rate)  # Output: 0.02
print(account2.interest_rate)  # Output: 0.02
 
# Ändern Sie die Klassenvariable
BankAccount.interest_rate = 0.03
 
print(account1.interest_rate)  # Output: 0.03
print(account2.interest_rate)  # Output: 0.03

Beide Instanzen sehen sofort den neuen Zinssatz, weil sie alle auf dieselbe Klassenvariable schauen.

31.1.3) Die Shadowing-Falle: Wenn Instanzvariablen Klassenvariablen verdecken

Hier ist ein subtiler, aber wichtiger Effekt: Wenn Sie einem Attribut über eine Instanz einen Wert zuweisen, erstellt Python eine Instanzvariable, die die Klassenvariable überschattet (versteckt):

python
class BankAccount:
    interest_rate = 0.02
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
# Erstellen Sie eine Instanzvariable, die die Klassenvariable überschattet
account1.interest_rate = 0.05
 
print(account1.interest_rate)  # Output: 0.05 (instance variable)
print(account2.interest_rate)  # Output: 0.02 (class variable)
print(BankAccount.interest_rate)  # Output: 0.02 (class variable)

Jetzt hat account1 seine eigene interest_rate-Instanzvariable, die die Klassenvariable verdeckt. Die Klassenvariable existiert weiterhin, aber account1.interest_rate bezieht sich stattdessen auf die Instanzvariable. Das ist normalerweise nicht das, was Sie wollen – wenn Sie eine Klassenvariable ändern müssen, ändern Sie sie über den Klassennamen, nicht über eine Instanz.

31.1.4) Praktische Einsatzmöglichkeiten für Klassenvariablen

Klassenvariablen sind nützlich für Daten, die über alle Instanzen hinweg geteilt werden sollen:

python
class Student:
    school_name = "Python High School"  # Für alle Schüler gleich
    total_students = 0  # Verfolgt, wie viele Schüler existieren
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
        Student.total_students += 1  # Beim Erstellen eines Schülers erhöhen
    
    def __str__(self):
        return f"{self.name} (Grade {self.grade}) at {Student.school_name}"
 
student1 = Student("Alice", 10)
student2 = Student("Bob", 11)
student3 = Student("Carol", 10)
 
print(student1)  # Output: Alice (Grade 10) at Python High School
print(f"Total students: {Student.total_students}")  # Output: Total students: 3

Beachten Sie, wie wir Student.total_students (nicht self.total_students) in __init__ verwenden, um klarzumachen, dass wir die Klassenvariable verändern und nicht eine Instanzvariable erstellen.

Klassenvariablen

Im Klassenrumpf definiert

Von allen Instanzen geteilt

Zugriff über ClassName.variable

Instanzvariablen

In init mit self definiert

Einzigartig für jede Instanz

Zugriff über instance.variable

31.2) Attribute mit @property verwalten

Manchmal möchten Sie steuern, was passiert, wenn jemand auf ein Attribut zugreift oder es verändert. Zum Beispiel möchten Sie vielleicht prüfen, dass ein Wert positiv ist, oder einen Wert „on-the-fly“ berechnen, statt ihn zu speichern. Pythons @property-Dekorator ermöglicht es Ihnen, Methoden zu schreiben, die wie einfacher Attributzugriff aussehen.

31.2.1) Das Problem: Direkter Attributzugriff kann nicht validieren

Wenn Attribute direkt angesprochen werden, können Sie die Werte nicht validieren oder transformieren:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
 
temp = Temperature(25)
print(temp.celsius)  # Output: 25
 
# Nichts hindert uns daran, physikalisch unmögliche Temperaturen zu setzen
temp.celsius = -500  # Unterhalb des absoluten Nullpunkts (-273.15°C)!
print(temp.celsius)  # Output: -500
 
# Oder absurd hohe Werte
temp.celsius = 1000000
print(temp.celsius)  # Output: 1000000

Ohne Validierung können wir versehentlich ungültige Daten setzen, was später im Programm zu Bugs führen kann. Wir könnten Methoden wie get_celsius() und set_celsius() verwenden, aber das ist nicht idiomatisches Python. Python-Entwickler erwarten, dass man direkt auf Attribute zugreift, nicht über Getter-/Setter-Methoden wie in Java oder C++.

31.2.2) @property für berechnete Attribute verwenden

Der @property-Dekorator macht aus einer Methode einen „Getter“, auf den wie auf ein Attribut zugegriffen wird:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        """Konvertiere celsius on-the-fly in fahrenheit"""
        return self.celsius * 9/5 + 32
 
temp = Temperature(25)
print(temp.celsius)  # Output: 25
print(temp.fahrenheit)  # Output: 77.0 (computed, not stored)

Beachten Sie, dass wir temp.fahrenheit ohne Klammern aufrufen – es sieht aus wie der Zugriff auf ein Attribut, aber tatsächlich wird die Methode ausgeführt. Der Fahrenheit-Wert wird bei jedem Zugriff berechnet, daher ist er immer mit celsius synchron:

python
temp = Temperature(0)
print(temp.fahrenheit)  # Output: 32.0
 
temp.celsius = 100
print(temp.fahrenheit)  # Output: 212.0 (automatically updated)

31.2.3) Einen Setter mit @property_name.setter hinzufügen

Um das Setzen einer Property zu erlauben, fügen wir eine Setter-Methode mit dem Dekorator @property_name.setter hinzu:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        """Konvertiere beim Setzen fahrenheit in celsius"""
        self.celsius = (value - 32) * 5/9
 
temp = Temperature(0)
print(temp.celsius)  # Output: 0
print(temp.fahrenheit)  # Output: 32.0
 
# Temperatur über fahrenheit setzen
temp.fahrenheit = 212
print(temp.celsius)  # Output: 100.0
print(temp.fahrenheit)  # Output: 212.0

Die Setter-Methode erhält den neuen Wert und kann ihn vor dem Speichern validieren oder transformieren.

31.2.4) Properties zur Validierung verwenden

Properties eignen sich hervorragend, um Einschränkungen durchzusetzen:

python
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self._balance = balance  # Unterstrich deutet „interne Verwendung“ an
    
    @property
    def balance(self):
        """Gibt den aktuellen Kontostand zurück"""
        return self._balance
    
    @balance.setter
    def balance(self, value):
        """Setzt den Kontostand, aber nur wenn er nicht negativ ist"""
        if value < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = value
 
account = BankAccount("Alice", 1000)
print(account.balance)  # Output: 1000
 
account.balance = 1500  # Funktioniert einwandfrei
print(account.balance)  # Output: 1500
 
# Das löst einen Fehler aus
account.balance = -100
# Output: ValueError: Balance cannot be negative

Beachten Sie die Namenskonvention: Wir speichern den tatsächlichen Wert in _balance (mit führendem Unterstrich) und stellen ihn über die balance-Property bereit. Der Unterstrich ist eine Python-Konvention, die „das ist ein internes Implementierungsdetail“ signalisiert, auch wenn das Attribut technisch weiterhin zugreifbar ist. Dieses Muster erlaubt es uns, den Zugriff über die Property zu steuern, während die eigentliche Speicherung getrennt bleibt.

31.2.5) Schreibgeschützte Properties

Wenn Sie eine Property ohne Setter definieren, wird sie schreibgeschützt:

python
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @property
    def area(self):
        """Berechnete schreibgeschützte Property"""
        return self.width * self.height
 
rect = Rectangle(5, 3)
print(rect.area)  # Output: 15
 
rect.width = 10
print(rect.area)  # Output: 30 (automatically updated)
 
# Der Versuch, area zu setzen, löst einen Fehler aus
rect.area = 50
# Output: AttributeError: property 'area' of 'Rectangle' object has no setter

Das ist nützlich für abgeleitete Werte, die berechnet und nicht gespeichert werden sollen.

@property-Dekorator

Wandelt Methode in Getter um

Zugriff wie auf Attribut

Kann Wert on-the-fly berechnen

@property_name.setter

Fügt Setter für Property hinzu

Kann vor dem Speichern validieren

Kann Wert transformieren

31.3) Klassenmethoden mit @classmethod

Manchmal benötigen Sie Methoden, die mit der Klasse selbst arbeiten, statt mit Instanzen. Klassenmethoden (class methods) erhalten die Klasse als erstes Argument (konventionell cls genannt) statt einer Instanz (self).

31.3.1) Klassenmethoden definieren

Wir erstellen Klassenmethoden mit dem Dekorator @classmethod:

python
class Student:
    school_name = "Python High School"
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
    
    @classmethod
    def get_school_name(cls):
        """Klassenmethode – erhält die Klasse, nicht eine Instanz"""
        return cls.school_name
 
# Auf der Klasse selbst aufrufen
print(Student.get_school_name())  # Output: Python High School
 
# Kann auch auf einer Instanz aufgerufen werden (aber cls ist weiterhin die Klasse)
student = Student("Alice", 10)
print(student.get_school_name())  # Output: Python High School

Der Parameter cls erhält automatisch die Klasse, so wie self in normalen Methoden automatisch die Instanz erhält.

31.3.2) Alternative Konstruktoren mit Klassenmethoden

Eine der häufigsten Verwendungen von Klassenmethoden ist das Erstellen alternativer Konstruktoren – verschiedene Wege, Instanzen zu erzeugen:

python
class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    
    @classmethod
    def from_string(cls, date_string):
        """Erstellt ein Date aus einem String wie '2024-12-27'"""
        year, month, day = date_string.split('-')
        return cls(int(year), int(month), int(day))
    
    @classmethod
    def today(cls):
        """Erstellt ein Date für heute (vereinfachtes Beispiel)"""
        # In echtem Code würden Sie das datetime-Modul verwenden
        return cls(2024, 12, 27)
    
    def __str__(self):
        return f"{self.year}-{self.month:02d}-{self.day:02d}"
 
# Normaler Konstruktor
date1 = Date(2024, 12, 27)
print(date1)  # Output: 2024-12-27
 
# Alternativer Konstruktor aus String
date2 = Date.from_string("2024-12-27")
print(date2)  # Output: 2024-12-27
 
# Alternativer Konstruktor für heute
date3 = Date.today()
print(date3)  # Output: 2024-12-27

Beachten Sie, dass from_string und today beide cls(...) zurückgeben – das erstellt eine neue Instanz der Klasse. Die Verwendung von cls statt hart codiertem Date sorgt dafür, dass der Code auch mit Subklassen korrekt funktioniert (über Vererbung lernen wir in Kapitel 32).

31.3.3) Klassenmethoden für Factory-Patterns

Klassenmethoden sind nützlich, um Instanzen mit unterschiedlichen Konfigurationen zu erzeugen:

python
class DatabaseConnection:
    def __init__(self, host, port, database, username):
        self.host = host
        self.port = port
        self.database = database
        self.username = username
    
    @classmethod
    def for_development(cls):
        """Erstellt eine für Entwicklung konfigurierte Verbindung"""
        return cls("localhost", 5432, "dev_db", "dev_user")
    
    @classmethod
    def for_production(cls):
        """Erstellt eine für Produktion konfigurierte Verbindung"""
        return cls("prod.example.com", 5432, "prod_db", "prod_user")
    
    def __str__(self):
        return f"Connection to {self.database} at {self.host}:{self.port}"
 
# Einfach, vorkonfigurierte Verbindungen zu erstellen
dev_conn = DatabaseConnection.for_development()
prod_conn = DatabaseConnection.for_production()
 
print(dev_conn)  # Output: Connection to dev_db at localhost:5432
print(prod_conn)  # Output: Connection to prod_db at prod.example.com:5432

31.3.4) Klassenmethoden zum Zählen von Instanzen

Klassenmethoden können mit Klassenvariablen zusammenarbeiten, um Informationen über alle Instanzen nachzuverfolgen:

python
class Product:
    total_products = 0
    
    def __init__(self, name, price):
        self.name = name
        self.price = price
        Product.total_products += 1
    
    @classmethod
    def get_total_products(cls):
        """Gibt die Gesamtzahl der erstellten Produkte zurück"""
        return cls.total_products
    
    @classmethod
    def reset_count(cls):
        """Setzt den Produktzähler zurück"""
        cls.total_products = 0
 
product1 = Product("Laptop", 999)
product2 = Product("Mouse", 25)
product3 = Product("Keyboard", 75)
 
print(Product.get_total_products())  # Output: 3
 
Product.reset_count()
print(Product.get_total_products())  # Output: 0

31.4) Statische Methoden mit @staticmethod

Statische Methoden (static methods) sind Methoden, die weder die Instanz (self) noch die Klasse (cls) als erstes Argument erhalten. Sie sind einfach normale Funktionen, die zufällig innerhalb einer Klasse definiert sind, weil sie logisch zu dieser Klasse gehören.

31.4.1) Statische Methoden definieren

Wir erstellen statische Methoden mit dem Dekorator @staticmethod:

python
class MathUtils:
    @staticmethod
    def is_even(number):
        """Prüft, ob eine Zahl gerade ist"""
        return number % 2 == 0
    
    @staticmethod
    def is_prime(number):
        """Prüft, ob eine Zahl eine Primzahl ist (vereinfacht)"""
        if number < 2:
            return False
        for i in range(2, int(number ** 0.5) + 1):
            if number % i == 0:
                return False
        return True
 
# Statische Methoden über die Klasse aufrufen
print(MathUtils.is_even(4))  # Output: True
print(MathUtils.is_even(7))  # Output: False
print(MathUtils.is_prime(17))  # Output: True
print(MathUtils.is_prime(18))  # Output: False
 
# Kann auch über eine Instanz aufgerufen werden (aber es ist dieselbe Funktion)
utils = MathUtils()
print(utils.is_even(10))  # Output: True

Statische Methoden benötigen keinen Zugriff auf Instanz- oder Klassendaten – sie sind eigenständige Hilfsfunktionen.

31.4.2) Wann statische Methoden vs. Klassenmethoden vs. Instanzmethoden verwenden

So wählen Sie aus:

python
class Temperature:
    # Klassenvariable
    absolute_zero_celsius = -273.15
    
    def __init__(self, celsius):
        self.celsius = celsius
    
    # Instanzmethode – benötigt Zugriff auf Instanzdaten (self)
    def to_fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    # Klassenmethode – benötigt Zugriff auf Klassendaten (cls)
    @classmethod
    def get_absolute_zero(cls):
        return cls.absolute_zero_celsius
    
    # Statische Methode – benötigt keine Instanz- oder Klassendaten
    @staticmethod
    def celsius_to_kelvin(celsius):
        return celsius + 273.15
    
    @staticmethod
    def fahrenheit_to_celsius(fahrenheit):
        return (fahrenheit - 32) * 5/9
 
temp = Temperature(25)
 
# Instanzmethode – nutzt Instanzdaten
print(temp.to_fahrenheit())  # Output: 77.0
 
# Klassenmethode – nutzt Klassendaten
print(Temperature.get_absolute_zero())  # Output: -273.15
 
# Statische Methoden – einfach Hilfsfunktionen
print(Temperature.celsius_to_kelvin(25))  # Output: 298.15
print(Temperature.fahrenheit_to_celsius(77))  # Output: 25.0

Richtlinien:

  • Verwenden Sie Instanzmethoden (instance methods), wenn Sie Zugriff auf Instanzattribute (self) benötigen
  • Verwenden Sie Klassenmethoden (class methods), wenn Sie Zugriff auf Klassenattribute brauchen oder alternative Konstruktoren möchten (cls)
  • Verwenden Sie statische Methoden (static methods), wenn Sie keinen Zugriff auf Instanz- oder Klassendaten benötigen, die Funktion aber logisch zur Klasse gehört

Hinweis: Statische Methoden könnten eigenständige Funktionen sein, aber sie in die Klasse zu packen gruppiert zusammengehörige Funktionalität und vermeidet das Überladen des globalen Namensraums.

MethodentypErster ParameterVerwenden, wenn
InstanzmethodeselfZugriff auf Instanzdaten nötig
KlassenmethodeclsZugriff auf Klassendaten oder alternative Konstruktoren nötig
Statische Methode(keiner)Hilfsfunktion, die zur Klasse gehört

31.4.3) Praktisches Beispiel: Validierungs-Utilities

Statische Methoden sind großartig für Validierung und Hilfsfunktionen:

python
class User:
    def __init__(self, username, password):
        if not User.is_valid_username(username):
            raise ValueError("Invalid username")
        if not User.is_valid_password(password):
            raise ValueError("Invalid password")
        
        self.username = username
        self._password = password
    
    @staticmethod
    def is_valid_username(username):
        """Prüft, ob username die Anforderungen erfüllt"""
        return len(username) >= 3 and username.isalnum()
        
    @staticmethod
    def is_valid_password(password):
        """Prüft, ob password die Sicherheitsanforderungen erfüllt"""
        return len(password) >= 8 and any(c.isdigit() for c in password)
 
# Diese Validierungsmethoden können unabhängig verwendet werden
print(User.is_valid_username("alice123"))  # Output: True
print(User.is_valid_username("ab"))  # Output: False
print(User.is_valid_password("pass1234"))  # Output: True
 
# Und sie können in jeder Methode der Klasse verwendet werden
try:
    user = User("ab", "short")
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: Invalid username

31.5) Spezialmethoden verstehen (Magic Methods)

Pythons Spezialmethoden (special methods) (auch Magic Methods (magic methods) oder Dunder-Methoden (dunder methods) genannt, weil sie doppelte Unterstriche haben) ermöglichen es Ihnen, anzupassen, wie sich Ihre Objekte bei Pythons eingebauten Operationen verhalten. Wir haben __init__, __str__ und __repr__ bereits in Kapitel 30 verwendet. Jetzt werden wir viele weitere erkunden.

31.5.1) Was Spezialmethoden tun

Spezialmethoden werden von Python automatisch aufgerufen, wenn Sie bestimmte Syntax oder eingebaute Funktionen verwenden:

python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Point({self.x}, {self.y})"
 
point = Point(3, 4)
 
# Wenn Sie print() aufrufen, ruft Python __str__() auf
print(point)  # Output: Point(3, 4)
# Das ist äquivalent zu: print(point.__str__())

Spezialmethoden ermöglichen es Ihnen, dass sich Ihre Klassen wie eingebaute Typen verhalten. Zum Beispiel können Sie Ihre Objekte:

  • Arithmetische Operationen unterstützen (+, -, *, /)
  • Vergleichbar machen (<, >, ==)
  • Mit len(), in und Indexierung arbeiten lassen
  • Wie Container oder Sequenzen agieren lassen

31.5.2) Häufige Kategorien von Spezialmethoden

Hier sind die wichtigsten Kategorien von Spezialmethoden:

String-Repräsentation (wie Objekte angezeigt werden):

  • __str__() – für print() und str()
  • __repr__() – für die REPL und repr()

Vergleich (Objekte vergleichen):

  • __eq__() – für ==
  • __ne__() – für !=
  • __lt__() – für <
  • __le__() – für <=
  • __gt__() – für >
  • __ge__() – für >=

Arithmetik (mathematische Operationen):

  • __add__() – für +
  • __sub__() – für -
  • __mul__() – für *
  • __truediv__() – für /

Container/Sequenz (Sammlungs-/Sequenzverhalten):

  • __len__() – für len()
  • __contains__() – für in
  • __getitem__() – für Indexierung obj[key]
  • __setitem__() – für Zuweisung obj[key] = value

Wir werden diese in den folgenden Abschnitten im Detail untersuchen.

31.6) Beispiel 1: Collection-Interface (len, contains)

Erstellen wir eine Klasse, die eine Sammlung von Elementen verwaltet, und sorgen wir dafür, dass sie mit Pythons eingebauter len()-Funktion und dem in-Operator funktioniert.

31.6.1) len für len() implementieren

Die Spezialmethode __len__() wird aufgerufen, wenn Sie len() auf Ihr Objekt anwenden:

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def __len__(self):
        """Gibt die Anzahl der Items im Warenkorb zurück"""
        return len(self.items)
 
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
cart.add_item("Orange")
 
# len() ruft __len__() auf
print(len(cart))  # Output: 3

Ohne __len__() würde der Aufruf len(cart) einen TypeError auslösen. Indem wir es implementieren, funktioniert unser ShoppingCart genau wie eingebaute Sammlungen.

31.6.2) contains für den in-Operator implementieren

Die Spezialmethode __contains__() wird aufgerufen, wenn Sie den in-Operator verwenden:

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def __len__(self):
        return len(self.items)
    
    def __contains__(self, item):
        """Prüft, ob sich ein Item im Warenkorb befindet"""
        return item in self.items
 
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
 
# in-Operator ruft __contains__() auf
print("Apple" in cart)  # Output: True
print("Orange" in cart)  # Output: False

Jetzt unterstützt unser Warenkorb die natürliche Python-Syntax für Zugehörigkeitstests.

31.6.3) Eine vollständigere Collection-Klasse bauen

Erstellen wir eine realistischere Collection-Klasse, die Schülernoten nachverfolgt:

python
class GradeBook:
    def __init__(self):
        self.grades = {}  # student_name: Liste von Noten
    
    def add_grade(self, student, grade):
        """Fügt eine Note für einen Schüler hinzu"""
        if student not in self.grades:
            self.grades[student] = []
        self.grades[student].append(grade)
    
    def __len__(self):
        """Gibt die Anzahl der Schüler zurück"""
        return len(self.grades)
    
    def __contains__(self, student):
        """Prüft, ob ein Schüler irgendwelche Noten hat"""
        return student in self.grades
    
    def get_average(self, student):
        """Gibt den Notendurchschnitt eines Schülers zurück"""
        if student not in self:
            return None
        grades = self.grades[student]
        return sum(grades) / len(grades)
    
    def __str__(self):
        return f"GradeBook with {len(self)} students"
 
gradebook = GradeBook()
gradebook.add_grade("Alice", 85)
gradebook.add_grade("Alice", 90)
gradebook.add_grade("Bob", 78)
gradebook.add_grade("Bob", 82)
gradebook.add_grade("Bob", 88)
 
print(gradebook)  # Output: GradeBook with 2 students
print(len(gradebook))  # Output: 2
 
print("Alice" in gradebook)  # Output: True
print("Carol" in gradebook)  # Output: False
 
print(f"Alice's average: {gradebook.get_average('Alice')}")  # Output: Alice's average: 87.5
print(f"Bob's average: {gradebook.get_average('Bob')}")  # Output: Bob's average: 82.66666666666667

Beachten Sie, wie get_average() if student not in self verwendet – das ruft unsere __contains__()-Methode auf, wodurch sich der Code natürlich liest.

31.7) Beispiel 2: Operator Overloading (add, eq, lt)

Operator Overloading (operator overloading) bedeutet, festzulegen, was Operatoren wie +, == und < für Ihre eigenen Klassen tun. Dadurch funktionieren Ihre Objekte natürlich mit Pythons Syntax.

31.7.1) add für Addition implementieren

Die Spezialmethode __add__() wird aufgerufen, wenn Sie den +-Operator verwenden:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """Addiert zwei Vektoren"""
        return Vector(self.x + other.x, self.y + other.y)
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(3, 4)
 
# + Operator ruft __add__() auf
v3 = v1 + v2
print(v3)  # Output: Vector(4, 6)

Wenn Python v1 + v2 sieht, ruft es v1.__add__(v2) auf. Die __add__()-Methode des linken Operanden erhält den rechten Operanden als Argument.

31.7.2) eq für Gleichheit implementieren

Die Spezialmethode __eq__() wird aufgerufen, wenn Sie den ==-Operator verwenden:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __eq__(self, other):
        """Prüft, ob zwei Vektoren gleich sind"""
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(1, 2)
v3 = Vector(3, 4)
 
# == Operator ruft __eq__() auf
print(v1 == v2)  # Output: True
print(v1 == v3)  # Output: False

Ohne __eq__() vergleicht Python die Objektidentität (ob es dasselbe Objekt im Speicher ist), nicht ihre Werte. Mit __eq__() definieren wir, was Gleichheit für unsere Klasse bedeutet.

31.7.3) Vergleichsoperatoren implementieren

Implementieren wir Vergleichsoperatoren für eine Money-Klasse:

python
class Money:
    def __init__(self, amount):
        self.amount = amount
    
    def __eq__(self, other):
        """Prüft, ob die Beträge gleich sind"""
        return self.amount == other.amount
    
    def __lt__(self, other):
        """Prüft, ob dieser Betrag kleiner als other ist"""
        return self.amount < other.amount
    
    def __le__(self, other):
        """Prüft, ob dieser Betrag kleiner oder gleich other ist"""
        return self.amount <= other.amount
    
    def __gt__(self, other):
        """Prüft, ob dieser Betrag größer als other ist"""
        return self.amount > other.amount
    
    def __ge__(self, other):
        """Prüft, ob dieser Betrag größer oder gleich other ist"""
        return self.amount >= other.amount
    
    def __str__(self):
        return f"${self.amount:.2f}"
 
price1 = Money(10.50)
price2 = Money(15.75)
price3 = Money(10.50)
 
print(price1 == price3)  # Output: True
print(price1 < price2)  # Output: True
print(price1 <= price3)  # Output: True
print(price2 > price1)  # Output: True
print(price2 >= price1)  # Output: True

31.7.4) Typinkompatibilitäten in Operatoren behandeln

Beim Implementieren von Operatoren sollten Sie Fälle behandeln, in denen der andere Operand nicht den erwarteten Typ hat:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """Addiert zwei Vektoren oder addiert einen Skalar zu beiden Komponenten"""
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        elif isinstance(other, (int, float)):
            return Vector(self.x + other, self.y + other)
        else:
            return NotImplemented  # Lässt Python other.__radd__(self) versuchen
    
    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(3, 4)
 
print(v1 + v2)  # Output: Vector(4, 6) (vector addition)
print(v1 + 5)  # Output: Vector(6, 7) (scalar addition)
 
print(v1 == v2)  # Output: False
print(v1 == "not a vector")  # Output: False (no error)

Das Zurückgeben von NotImplemented (einer speziellen eingebauten Konstante) sagt Python, dass es die reflektierte Operation auf dem anderen Operanden versuchen soll. Das ist wichtig, damit Operatoren korrekt mit verschiedenen Typen funktionieren.

Operator Overloading

Arithmetische Operatoren

Vergleichsoperatoren

add für +

sub für -

mul für *

truediv für /

eq für ==

lt für <

le für <=

gt für >

ge für >=

31.8) Beispiel 3: Sequenzzugriff (getitem, setitem)

Die Spezialmethoden __getitem__() und __setitem__() ermöglichen es Ihnen, Index-Syntax (obj[key]) mit Ihren eigenen Klassen zu verwenden. Dadurch verhalten sich Ihre Objekte wie Listen, Dictionaries oder andere Sequenzen.

31.8.1) getitem für Indexierung implementieren

Die Methode __getitem__() wird aufgerufen, wenn Sie eckige Klammern verwenden, um auf ein Element zuzugreifen:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        """Gibt einen Song über den Index zurück"""
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
# Indexierung ruft __getitem__() auf
print(playlist[0])  # Output: Song A
print(playlist[1])  # Output: Song B
print(playlist[-1])  # Output: Song C (negative indexing works!)

Da wir an self.songs[index] delegieren, funktionieren alle Listen-Indexierungsfeatures automatisch: positive Indizes, negative Indizes und sogar das Auslösen von IndexError bei ungültigen Indizes.

31.8.2) Slicing mit getitem unterstützen

Dieselbe Methode __getitem__() behandelt auch Slicing:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        """Gibt einen Song über Index oder Slice zurück"""
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
playlist.add_song("Song D")
 
# Slicing ruft ebenfalls __getitem__() auf
print(playlist[1:3])  # Output: ['Song B', 'Song C']
print(playlist[:2])  # Output: ['Song A', 'Song B']
print(playlist[::2])  # Output: ['Song A', 'Song C']

Wenn Sie Slicing verwenden, übergibt Python ein slice-Objekt an __getitem__(). Indem wir an self.songs[index] delegieren, unterstützen wir automatisch die gesamte Slice-Syntax.

31.8.3) setitem für Zuweisung implementieren

Die Methode __setitem__() wird aufgerufen, wenn Sie einem Index etwas zuweisen:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __setitem__(self, index, value):
        """Ersetzt einen Song an einem bestimmten Index"""
        self.songs[index] = value
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
print(playlist[1])  # Output: Song B
 
# Zuweisung ruft __setitem__() auf
playlist[1] = "New Song B"
print(playlist[1])  # Output: New Song B

31.8.4) Objekte mit getitem iterierbar machen

Ein interessanter Nebeneffekt: Wenn Sie __getitem__() mit Integer-Indizes beginnend bei 0 implementieren, wird Ihr Objekt automatisch iterierbar:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
# for-Schleifen funktionieren automatisch!
for song in playlist:
    print(song)
# Output:
# Song A
# Song B
# Song C

Python versucht zu iterieren, indem es __getitem__(0), dann __getitem__(1) usw. aufruft, bis ein IndexError ausgelöst wird. Das ist ein älteres Iterationsprotokoll – über das moderne Iterator-Protokoll lernen wir in Kapitel 35.

31.8.5) Dictionary-ähnlicher Zugriff mit String-Keys

__getitem__() und __setitem__() funktionieren mit jedem Key-Typ, nicht nur mit Integern:

python
class ScoreBoard:
    def __init__(self):
        self.scores = {}
    
    def __getitem__(self, player_name):
        """Gibt die Punktzahl für einen Spieler zurück"""
        return self.scores.get(player_name, 0)
    
    def __setitem__(self, player_name, score):
        """Setzt die Punktzahl für einen Spieler"""
        self.scores[player_name] = score
    
    def __contains__(self, player_name):
        return player_name in self.scores
    
    def __len__(self):
        return len(self.scores)
 
scoreboard = ScoreBoard()
 
# Punktzahlen über String-Keys setzen
scoreboard["Alice"] = 100
scoreboard["Bob"] = 85
 
# Punktzahl aktualisieren
scoreboard["Alice"] = 120
 
# Punktzahlen abrufen
print(scoreboard["Alice"])  # Output: 120
print(scoreboard["Bob"])    # Output: 85
print(scoreboard["Carol"])  # Output: 0
 
print("Alice" in scoreboard)  # Output: True
print(len(scoreboard))  # Output: 2

Sequenzzugriff

getitem

setitem

Aufgerufen für obj[key]

Behandelt Indexierung

Behandelt Slicing

Macht Objekt iterierbar

Aufgerufen für obj[key] = value

Ermöglicht Zuweisung

Kann Werte validieren


Dieses Kapitel hat Ihnen gezeigt, wie Sie anspruchsvolle Klassen erstellen, die sich nahtlos in Pythons Syntax integrieren. Durch das Implementieren von Klassenvariablen, Properties, Klassenmethoden, statischen Methoden und Spezialmethoden können Sie Ihre eigenen Klassen so gestalten, dass sie sich wie eingebaute Typen verhalten. In Kapitel 32 werden wir Vererbung und Polymorphie erkunden, die es Ihnen ermöglichen, Hierarchien verwandter Klassen zu bauen, die Verhalten teilen und erweitern.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai