Weiter zum Inhalt

Python-Descriptors: Tutorial

Erfahre, was Python-Descriptors sind, wann du sie einsetzen solltest und warum sie so nützlich sind.
Aktualisiert 18. Sept. 2026  · 13 Min. lesen

Mit KI erkunden

ChatGPTClaudePerplexity

Python-Descriptors, oder allgemeiner Descriptors, sind eine mächtige Technik, um wiederverwendbaren Code zu schreiben, der zwischen Klassen geteilt werden kann. Auf den ersten Blick erinnern sie an Vererbung, technisch sind sie das aber nicht. Vielmehr bieten sie einen allgemeinen Mechanismus, um den Zugriff auf Attribute abzufangen. Descriptors stecken hinter Properties, statischen Methoden, Klassenmethoden, super-Methoden und mehr.

Descriptors wurden in Python 2.2 eingeführt und galten seither als „magisch“, weil sie traditionellen Klassen zu einem neuen Stil verholfen haben. Es sind Klassen, mit denen du in einer anderen Klasse verwaltete Eigenschaften umsetzt. Konkret implementieren sie die Schnittstelle für die Methoden __get__(), __set__() und __delete__() – und genau das macht sie aus vielen Gründen interessant. Ein Beispiel dafür sind Klassen- und Property-Decorator, die du in Python sicher schon gesehen hast.

Einfach gesagt: Eine Klasse, die für ein Objekt __get__(), __set()__ oder __delete()__ implementiert, nennt man „Descriptor“. Laut offizieller Python-Dokumentation ist ein Descriptor ein Objektattribut mit Bindungsverhalten, dessen Attributzugriff durch Methoden des Descriptor-Protokolls überschrieben wurde. Diese Methoden sind __get__(), __set__() und __delete__() (Quelle).

Bindungsverhalten bedeutet bei Descriptors, dass geregelt wird, wie ein Wert für eine Variable, ein Objekt oder einen Datensatz gesetzt, ausgelesen (get) oder gelöscht werden kann. Diese gesamte Interaktion ist an genau dieses Datenelement gebunden, sie gilt also nur für die Daten, auf die du den Descriptor angewendet hast.

Descriptors lassen sich in Data- und Non-Data-Descriptors einteilen. Hat dein Descriptor nur eine __get__()-Methode, ist er ein Non-Data-Descriptor; implementiert er zusätzlich __set__() und __delete__(), spricht man von einem Data-Descriptor. Non-Data-Descriptors sind nur lesbar, Data-Descriptors sind les- und schreibbar.

Wichtig: Descriptors werden an einer Klasse definiert, nicht an deren Instanz. Änderst du die Klasse, überschreibst oder löschst du den Descriptor selbst – sein Code wird dabei nicht ausgelöst (IBM Developer).

Die Descriptor-Klasse ist übrigens nicht auf diese drei Methoden beschränkt. Sie kann auch beliebige weitere Attribute enthalten – neben get, set und delete.

Schauen wir uns die Methoden get, set und delete genauer an – inspiriert von dieser Seite bei IBM Developer:

  • self ist die Instanz des von dir erstellten Descriptors (Real Python).
  • object ist die Instanz des Objekts, an das dein Descriptor angehängt ist (Real Python).
  • type ist der Typ der Klasse, an die der Descriptor gebunden ist (Real Python).
  • value ist der Wert, der dem Attribut des Descriptors zugewiesen wird. get(self, object, type) set(self, object, value) delete(self, object)
  • __get__() greift auf das Attribut zu bzw. wird verwendet, wenn du Informationen auslesen möchtest. Es gibt den Attributwert zurück oder wirft eine AttributeError-Exception, wenn das angefragte Attribut nicht existiert.

  • __set__() wird bei einer Attributzuweisung aufgerufen und setzt den Wert eines Attributs. Es gibt nichts zurück, kann aber eine AttributeError-Exception auslösen.

  • __delete__() steuert eine Löschoperation, also wenn du das Attribut von einem Objekt entfernen möchtest. Es gibt nichts zurück.

Jetzt schauen wir uns den Zweck von Descriptors anhand einiger Beispiele an.

Zweck von Descriptors

Definieren wir eine Klasse car mit drei Attributen: make, model und fuel_cap. Mit der Methode __init__() initialisieren wir die Attribute. Außerdem nutzen wir die Magic-Methode __str__(), die einfach die drei Attribute ausgibt, die wir beim Erstellen des Objekts an die Klasse übergeben.

Beachte: Die Methode __str__() liefert die String-Repräsentation des Objekts. Sie wird aufgerufen, wenn du print() oder str() auf eine Instanz anwendest.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.

Sieht gut aus, oder?

Jetzt ändern wir die Tankkapazität auf minus 40.

car2 = Car("BMW","X7",-40)
print(car2)
BMW model X7 with a fuel capacity of -40 ltr.

Moment – da stimmt doch was nicht. Die Tankkapazität kann nie negativ sein. Python akzeptiert den Wert aber fehlerfrei. Der Grund: Python ist eine dynamische Sprache und führt keine explizite Typprüfung durch.

Um das zu vermeiden, fügen wir im __init__() eine if-Bedingung hinzu, die prüft, ob die angegebene Tankkapazität gültig ist. Ist sie ungültig, werfen wir eine ValueError-Exception.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car("BMW","X7",-40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-22-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-20-1e154783588d> in __init__(self, make, model, fuel_cap)
      5         self.fuel_cap = fuel_cap
      6         if self.fuel_cap < 0:
----> 7             raise ValueError("Fuel Capacity can never be less than zero")
      8
      9     def __str__(self):


ValueError: Fuel Capacity can never be less than zero

Wie du siehst, funktioniert das – das Programm wirft eine ValueError, wenn die Tankkapazität unter null liegt.

Es gibt aber noch ein Problem: Was, wenn die fuel capacity eine Gleitkommazahl oder ein String ist? Oder wenn auch make und model Ganzzahlen sind? In all diesen Fällen würde unser Programm aktuell keinen Fehler melden.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car(-40,"X7",40)
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

Um auch diesen Fall abzufangen, könnten wir eine weitere if-Bedingung ergänzen oder die Typprüfung mit isinstance lösen.

Diesmal verwenden wir die eingebaute Funktion isinstance.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car(-40,"X7",40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-34-75b08cba454f> in <module>
----> 1 car2 = Car(-40,"X7",40)
      2 print(car2)


<ipython-input-31-175690bf3b98> in __init__(self, make, model, fuel_cap)
      7             print(self.make)
      8         else:
----> 9             raise ValueError("Make of the car can never be an integer")
     10
     11         if self.fuel_cap < 0:


ValueError: Make of the car can never be an integer

Super – auch diesen Fehler hast du abgefangen.

Doch was passiert, wenn du die Tankkapazität später bewusst auf minus 40 setzt? Das greift nicht, denn die Typprüfung findet nur einmal im __init__() statt. Wie du weißt, ist __init__() der Konstruktor und wird nur beim Erzeugen des Objekts aufgerufen. Danach greift deine benutzerdefinierte Prüfung nicht mehr.

Hier ein Beispiel:

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2.make = -40
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

Und schon ist die Typprüfung ausgehebelt.

Stell dir vor, du hast noch weitere Attribute wie Verbrauch, Preis, Ausstattung usw., die ebenfalls eine Typprüfung brauchen – und einige davon sollen nur lesbar sein. Ziemlich mühsam, das alles manuell zu verwalten.

Genau hier kommen Python-Descriptors ins Spiel!

Wie oben gelernt: Jede Klasse, die die Magic-Methoden __get__(), __set()__ oder __delete()__ für das Descriptor-Protokoll implementiert, ist ein Descriptor. Damit steuerst du zusätzlich, ob ein Attribut z. B. nur gelesen oder auch geschrieben werden darf.

Erweitern wir nun das obige Beispiel und fügen die Descriptor-Methoden hinzu.

class Descriptor:
    def __init__(self):
        self.__fuel_cap = 0
    def __get__(self, instance, owner):    
        return self.__fuel_cap
    def __set__(self, instance, value):
        if isinstance(value, int):
            print(value)
        else:
            raise TypeError("Fuel Capacity can only be an integer")

        if value < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

        self.__fuel_cap = value

    def __delete__(self, instance):
        del self.__fuel_cap
class Car:
    fuel_cap = Descriptor()
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
40
BMW model X7 with a fuel capacity of 40 ltr.

Wenn dir die Klasse Descriptor noch kryptisch vorkommt, zerlegen wir sie in kleine Teile:

  • Die __init__()-Methode der Descriptor-Klasse setzt eine lokale Variable __fuel_cap auf Null. Das doppelte Unterstrich-Präfix („Dunder“) markiert die Variable als privat. Es dient hier auch dazu, die Tankkapazität des Descriptors von der gleichnamigen Eigenschaft der Car-Klasse zu unterscheiden.
  • Wie du jetzt weißt, liest __get__() Attribute aus und gibt die Tankkapazität zurück. Sie nimmt drei Argumente entgegen: das Descriptor-Objekt, die Instanz der Klasse, die den Descriptor enthält (hier car2), und den Owner, also die Klasse, zu der die Instanz gehört (die Klasse Car). In dieser Methode gibst du einfach den Wert des Attributs fuel_cap zurück, der zuvor in __set__() gesetzt wurde.
  • __set__() wird aufgerufen, wenn dem Attribut ein Wert zugewiesen wird, und gibt – anders als __get__() – nichts zurück. Neben dem Descriptor-Objekt selbst erhält es die Instanz (wie bei __get__()) und den zuzuweisenden Wert. Hier prüfst du, ob der Wert für fuel_cap eine Ganzzahl ist. Wenn nicht, wird eine TypeError-Exception geworfen. Anschließend prüfst du, ob der Wert kleiner als null ist; falls ja, folgt eine ValueError. Nach den Prüfungen setzt du fuel_cap auf den Wert.
  • Die Methode __delete__() wird aufgerufen, wenn das Attribut von einem Objekt gelöscht wird. Wie __set__() gibt sie nichts zurück.

Die Klasse Car bleibt im Wesentlichen unverändert. Neu ist nur, dass du die Instanz fuel_cap der Klasse Descriptor() als Klassenattribut hinzufügst. Wichtig: Eine Descriptor-Instanz muss als Klassenattribut, nicht als Instanzattribut, definiert werden.

Sobald du im __init__() den lokalen Parameter fuel_cap der Instanz zuweist, wird die Methode __set__() des Descriptors aufgerufen.

Ändern wir nun die Tankkapazität auf einen negativen Wert und prüfen, ob eine ValueError-Exception ausgelöst wird.

car2 = Car("BMW","X7",-40)
print(car2)
-40



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-115-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-107-3e1f3e97d3c7> in __init__(self, make, model, fuel_cap)
      4         self.make = make
      5         self.model = model
----> 6         self.fuel_cap = fuel_cap
      7
      8     def __str__(self):


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero

Erinnerst du dich hier daran, dass die Typprüfung scheiterte, wenn du den Wert später ins Negative geändert hast, weil sie nur im __init__() ausgeführt wurde? Testen wir jetzt eine String-Zuweisung.

car2.fuel_cap = -1
-1



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-120-dea9dbe96ebe> in <module>
----> 1 car2.fuel_cap = -1


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero
car2.fuel_cap = "BMW"
----------------------------------------

TypeErrorTraceback (most recent call last)

<ipython-input-121-0b316a9872c6> in <module>
----> 1 car2.fuel_cap = "BMW"


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
      8             print(value)
      9         else:
---> 10             raise TypeError("Fuel Capacity can only be an integer")
     11
     12         if value < 0:


TypeError: Fuel Capacity can only be an integer

Perfekt! Wie du siehst, greifen die Prüfungen auch, wenn du den Attributwert später änderst.

Ein kleiner Haken an Descriptors: Erstellst du eine neue zweite Instanz der Klasse, kann der Wert der vorherigen Instanz überschrieben werden. Der Grund: Descriptors sind an die Klasse gebunden, nicht an die Instanz.

Schauen wir uns das an:

car3 = Car("BMW","X7",48) #created a new instance 'car3' with different values
48

Wenn du anschließend car2 ausgibst, siehst du, dass die Werte von car3 übernommen wurden.

print(car2)
BMW model X7 with a fuel capacity of 48 ltr.

Fazit

Glückwunsch – du hast das Tutorial abgeschlossen.

Dieses Tutorial richtet sich an alle, die mit Python vertraut sind und das nächste Level erreichen wollen. Eine gute Übung: Überlege dir, wie du das Problem des Überschreibens zwischen Instanzen lösen könntest, das wir heute gesehen haben.

Stell deine Fragen zu diesem Tutorial gern unten in den Kommentaren.

Wenn du mehr über Python lernen möchtest, starte den vollständigen Python Programming-Lernpfad von DataCamp.

Sieh dir auch unser Python Functions Tutorial an.

Themen
Python

Python-Kurse

Kurs

Einführung in Python

4 Std.
7M
Lerne in nur vier Stunden die Grundlagen der Datenanalyse mit Python und entdecke beliebte Python-Pakete.
Details anzeigenRight Arrow
Kurs Starten
Mehr anzeigenRight Arrow