Leitfaden zu Deskriptoren
*************************


Inhalt
^^^^^^

* Leitfaden zu Deskriptoren

  * Einführung

    * Einfaches Beispiel: Ein Deskriptor, der eine Konstante
      zurückgibt

    * Dynamisches Nachschlagen

    * Verwaltete Attribute

    * Angepasste Namen

    * Abschließende Gedanken

  * Vollständiges praxisnahes Beispiel

    * Die Klasse Validator

    * Eigene Validatoren

    * Praktische Anwendung

  * Technisches Tutorial

    * Zusammenfassung

    * Begriffsbestimmung und Einführung

    * Das Deskriptor-Protokoll

    * Überblick über den Aufruf von Deskriptoren

    * Aufruf aus einer Instanz

    * Aufruf aus einer Klasse

    * Aufruf aus super

    * Zusammenfassung der Aufruflogik

    * Automatische Namensbenachrichtigung

    * ORM-Beispiel

  * Reine Python-Gegenstücke

    * Properties

    * Funktionen und Methoden

    * Arten von Methoden

    * Statische Methoden

    * Klassenmethoden

    * Member-Objekte und __slots__

*Deskriptoren* erlauben es Objekten, das Nachschlagen, Speichern und
Löschen von Attributen anzupassen.

Dieser Leitfaden hat vier große Abschnitte:

1. Die „Einführung“ gibt einen grundlegenden Überblick und geht sachte
   von einfachen Beispielen aus, wobei jeweils eine Eigenschaft
   hinzukommt. Fang hier an, wenn Deskriptoren neu für dich sind.

2. Der zweite Abschnitt zeigt ein vollständiges, praxisnahes
   Deskriptor-Beispiel. Wenn du die Grundlagen schon kennst, fang dort
   an.

3. Der dritte Abschnitt bietet ein technischeres Tutorial, das
   ausführlich auf die Mechanik eingeht, wie Deskriptoren arbeiten.
   Die meisten brauchen diese Detailtiefe nicht.

4. Der letzte Abschnitt enthält reine Python-Gegenstücke zu
   integrierten Deskriptoren, die in C geschrieben sind. Lies ihn,
   wenn du neugierig bist, wie Funktionen zu gebundenen Methoden
   werden, oder wie verbreitete Werkzeuge wie "@classmethod",
   "@staticmethod", "@property" und *__slots__* umgesetzt sind.


Einführung
==========

In dieser Einführung beginnen wir mit dem einfachsten möglichen
Beispiel und ergänzen dann eine neue Fähigkeit nach der anderen.


Einfaches Beispiel: Ein Deskriptor, der eine Konstante zurückgibt
-----------------------------------------------------------------

Die Klasse "Ten" ist ein Deskriptor, dessen Methode "__get__()" immer
die Konstante "10" zurückgibt:

   class Ten:
       def __get__(self, obj, objtype=None):
           return 10

Um den Deskriptor zu verwenden, muss er als Klassenvariable in einer
anderen Klasse abgelegt werden:

   class A:
       x = 5                       # Regular class attribute
       y = Ten()                   # Descriptor instance

Eine interaktive Sitzung zeigt den Unterschied zwischen dem normalen
Nachschlagen eines Attributs und dem Nachschlagen über einen
Deskriptor:

   >>> a = A()                     # Make an instance of class A
   >>> a.x                         # Normal attribute lookup
   5
   >>> a.y                         # Descriptor lookup
   10

Beim Nachschlagen des Attributs "a.x" findet der Punktoperator "'x':
5" im Klassenwörterbuch. Beim Nachschlagen von "a.y" findet der
Punktoperator eine Deskriptor-Instanz, erkennbar an ihrer Methode
"__get__". Der Aufruf dieser Methode gibt "10" zurück.

Beachte, dass der Wert "10" weder im Klassen- noch im
Instanzwörterbuch gespeichert ist. Der Wert "10" wird stattdessen bei
Bedarf berechnet.

Dieses Beispiel zeigt, wie ein einfacher Deskriptor arbeitet, ist aber
nicht besonders nützlich. Um Konstanten abzurufen, wäre das normale
Nachschlagen von Attributen besser.

Im nächsten Abschnitt bauen wir etwas Nützlicheres: ein dynamisches
Nachschlagen.


Dynamisches Nachschlagen
------------------------

Interessante Deskriptoren führen üblicherweise Berechnungen aus,
anstatt Konstanten zurückzugeben:

   import os

   class DirectorySize:

       def __get__(self, obj, objtype=None):
           return len(os.listdir(obj.dirname))

   class Directory:

       size = DirectorySize()              # Descriptor instance

       def __init__(self, dirname):
           self.dirname = dirname          # Regular instance attribute

Eine interaktive Sitzung zeigt, dass das Nachschlagen dynamisch ist –
es berechnet jedes Mal andere, aktuelle Antworten:

   >>> s = Directory('songs')
   >>> g = Directory('games')
   >>> s.size                              # The songs directory has twenty files
   20
   >>> g.size                              # The games directory has three files
   3
   >>> os.remove('games/chess')            # Delete a game
   >>> g.size                              # File count is automatically updated
   2

Dieses Beispiel zeigt nicht nur, wie Deskriptoren Berechnungen
ausführen können, es offenbart auch den Zweck der Parameter von
"__get__()". Der Parameter *self* ist *size*, eine Instanz von
*DirectorySize*. Der Parameter *obj* ist entweder *g* oder *s*, eine
Instanz von *Directory*. Es ist der Parameter *obj*, der die Methode
"__get__()" das Zielverzeichnis erfahren lässt. Der Parameter
*objtype* ist die Klasse *Directory*.


Verwaltete Attribute
--------------------

Ein beliebter Einsatz von Deskriptoren ist, den Zugriff auf
Instanzdaten zu verwalten. Der Deskriptor wird einem öffentlichen
Attribut im Klassenwörterbuch zugewiesen, während die eigentlichen
Daten als privates Attribut im Instanzwörterbuch gespeichert werden.
Die Methoden "__get__()" und "__set__()" des Deskriptors werden
ausgelöst, wenn auf das öffentliche Attribut zugegriffen wird.

Im folgenden Beispiel ist *age* das öffentliche und *_age* das private
Attribut. Wird auf das öffentliche Attribut zugegriffen, protokolliert
der Deskriptor das Nachschlagen oder die Aktualisierung:

   import logging

   logging.basicConfig(level=logging.INFO)

   class LoggedAgeAccess:

       def __get__(self, obj, objtype=None):
           value = obj._age
           logging.info('Accessing %r giving %r', 'age', value)
           return value

       def __set__(self, obj, value):
           logging.info('Updating %r to %r', 'age', value)
           obj._age = value

   class Person:

       age = LoggedAgeAccess()             # Descriptor instance

       def __init__(self, name, age):
           self.name = name                # Regular instance attribute
           self.age = age                  # Calls __set__()

       def birthday(self):
           self.age += 1                   # Calls both __get__() and __set__()

Eine interaktive Sitzung zeigt, dass jeder Zugriff auf das verwaltete
Attribut *age* protokolliert wird, das gewöhnliche Attribut *name*
aber nicht:

   >>> mary = Person('Mary M', 30)         # The initial age update is logged
   INFO:root:Updating 'age' to 30
   >>> dave = Person('David D', 40)
   INFO:root:Updating 'age' to 40

   >>> vars(mary)                          # The actual data is in a private attribute
   {'name': 'Mary M', '_age': 30}
   >>> vars(dave)
   {'name': 'David D', '_age': 40}

   >>> mary.age                            # Access the data and log the lookup
   INFO:root:Accessing 'age' giving 30
   30
   >>> mary.birthday()                     # Updates are logged as well
   INFO:root:Accessing 'age' giving 30
   INFO:root:Updating 'age' to 31

   >>> dave.name                           # Regular attribute lookup isn't logged
   'David D'
   >>> dave.age                            # Only the managed attribute is logged
   INFO:root:Accessing 'age' giving 40
   40

Ein wesentliches Problem an diesem Beispiel ist, dass der private Name
*_age* in der Klasse *LoggedAgeAccess* fest verdrahtet ist. Das heißt,
dass jede Instanz nur ein protokolliertes Attribut haben kann und dass
sein Name unveränderlich ist. Im nächsten Beispiel beheben wir dieses
Problem.


Angepasste Namen
----------------

Verwendet eine Klasse Deskriptoren, kann sie jedem Deskriptor
mitteilen, welcher Variablenname verwendet wurde.

In diesem Beispiel hat die Klasse "Person" zwei Deskriptor-Instanzen,
*name* und *age*. Wird die Klasse "Person" definiert, macht sie einen
Rückruf an "__set_name__()" in *LoggedAccess*, damit die Feldnamen
festgehalten werden können. Jeder Deskriptor bekommt so seinen eigenen
*public_name* und *private_name*:

   import logging

   logging.basicConfig(level=logging.INFO)

   class LoggedAccess:

       def __set_name__(self, owner, name):
           self.public_name = name
           self.private_name = '_' + name

       def __get__(self, obj, objtype=None):
           value = getattr(obj, self.private_name)
           logging.info('Accessing %r giving %r', self.public_name, value)
           return value

       def __set__(self, obj, value):
           logging.info('Updating %r to %r', self.public_name, value)
           setattr(obj, self.private_name, value)

   class Person:

       name = LoggedAccess()                # First descriptor instance
       age = LoggedAccess()                 # Second descriptor instance

       def __init__(self, name, age):
           self.name = name                 # Calls the first descriptor
           self.age = age                   # Calls the second descriptor

       def birthday(self):
           self.age += 1

Eine interaktive Sitzung zeigt, dass die Klasse "Person"
"__set_name__()" aufgerufen hat, damit die Feldnamen festgehalten
werden. Hier rufen wir "vars()" auf, um den Deskriptor nachzuschlagen,
ohne ihn auszulösen:

   >>> vars(vars(Person)['name'])
   {'public_name': 'name', 'private_name': '_name'}
   >>> vars(vars(Person)['age'])
   {'public_name': 'age', 'private_name': '_age'}

Die neue Klasse protokolliert nun den Zugriff auf *name* und auf
*age*:

   >>> pete = Person('Peter P', 10)
   INFO:root:Updating 'name' to 'Peter P'
   INFO:root:Updating 'age' to 10
   >>> kate = Person('Catherine C', 20)
   INFO:root:Updating 'name' to 'Catherine C'
   INFO:root:Updating 'age' to 20

Die beiden *Person*-Instanzen enthalten nur die privaten Namen:

   >>> vars(pete)
   {'_name': 'Peter P', '_age': 10}
   >>> vars(kate)
   {'_name': 'Catherine C', '_age': 20}


Abschließende Gedanken
----------------------

Ein *Deskriptor* ist, wie wir jedes Objekt nennen, das "__get__()",
"__set__()" oder "__delete__()" definiert.

Deskriptoren können wahlweise eine Methode "__set_name__()" haben. Die
wird nur in Fällen verwendet, in denen ein Deskriptor entweder die
Klasse kennen muss, in der er erzeugt wurde, oder den Namen der
Klassenvariablen, der er zugewiesen wurde. (Diese Methode wird, falls
vorhanden, auch dann aufgerufen, wenn die Klasse kein Deskriptor ist.)

Deskriptoren werden beim Nachschlagen von Attributen durch den
Punktoperator aufgerufen. Wird auf einen Deskriptor mittelbar über
"vars(some_class)[descriptor_name]" zugegriffen, wird die Deskriptor-
Instanz zurückgegeben, ohne sie aufzurufen.

Deskriptoren funktionieren nur, wenn sie als Klassenvariablen
verwendet werden. In Instanzen abgelegt haben sie keine Wirkung.

Der wesentliche Grund für Deskriptoren ist, einen Haken zu bieten, mit
dem in Klassenvariablen abgelegte Objekte steuern können, was beim
Nachschlagen von Attributen geschieht.

Herkömmlich steuert die aufrufende Klasse, was beim Nachschlagen
geschieht. Deskriptoren kehren dieses Verhältnis um und erlauben es
den nachgeschlagenen Daten, ein Wort mitzureden.

Deskriptoren werden in der ganzen Sprache verwendet. So werden
Funktionen zu gebundenen Methoden. Verbreitete Werkzeuge wie
"@classmethod", "@staticmethod", "@property" und
"@functools.cached_property" sind alle als Deskriptoren umgesetzt.


Vollständiges praxisnahes Beispiel
==================================

In diesem Beispiel bauen wir ein praxisnahes und mächtiges Werkzeug,
um notorisch schwer zu findende Fehler bei der Datenbeschädigung
aufzuspüren.


Die Klasse Validator
--------------------

Ein Validator ist ein Deskriptor für den Zugriff auf ein verwaltetes
Attribut. Bevor er Daten speichert, prüft er, ob der neue Wert
verschiedene Typ- und Bereichsgrenzen einhält. Sind diese Grenzen
nicht eingehalten, löst er eine Ausnahme aus, um Datenbeschädigung an
ihrer Quelle zu verhindern.

Diese Klasse "Validator" ist sowohl eine *abstrakte Basisklasse* als
auch ein Deskriptor für ein verwaltetes Attribut:

   from abc import ABC, abstractmethod

   class Validator(ABC):

       def __set_name__(self, owner, name):
           self.private_name = '_' + name

       def __get__(self, obj, objtype=None):
           return getattr(obj, self.private_name)

       def __set__(self, obj, value):
           self.validate(value)
           setattr(obj, self.private_name, value)

       @abstractmethod
       def validate(self, value):
           pass

Eigene Validatoren müssen von "Validator" erben und eine Methode
"validate()" mitbringen, um je nach Bedarf verschiedene Grenzen zu
prüfen.


Eigene Validatoren
------------------

Hier drei praxisnahe Hilfsmittel zur Datenprüfung:

1. "OneOf" prüft, ob ein Wert einer aus einer beschränkten Menge von
   Möglichkeiten ist.

2. "Number" prüft, ob ein Wert entweder ein "int" oder ein "float"
   ist. Wahlweise prüft es, ob ein Wert zwischen einem gegebenen
   Minimum und Maximum liegt.

3. "String" prüft, ob ein Wert ein "str" ist. Wahlweise prüft es eine
   gegebene Mindest- oder Höchstlänge. Es kann außerdem ein selbst
   festgelegtes Prädikat prüfen.

   class OneOf(Validator):

       def __init__(self, *options):
           self.options = set(options)

       def validate(self, value):
           if value not in self.options:
               raise ValueError(
                   f'Expected {value!r} to be one of {self.options!r}'
               )

   class Number(Validator):

       def __init__(self, minvalue=None, maxvalue=None):
           self.minvalue = minvalue
           self.maxvalue = maxvalue

       def validate(self, value):
           if not isinstance(value, (int, float)):
               raise TypeError(f'Expected {value!r} to be an int or float')
           if self.minvalue is not None and value < self.minvalue:
               raise ValueError(
                   f'Expected {value!r} to be at least {self.minvalue!r}'
               )
           if self.maxvalue is not None and value > self.maxvalue:
               raise ValueError(
                   f'Expected {value!r} to be no more than {self.maxvalue!r}'
               )

   class String(Validator):

       def __init__(self, minsize=None, maxsize=None, predicate=None):
           self.minsize = minsize
           self.maxsize = maxsize
           self.predicate = predicate

       def validate(self, value):
           if not isinstance(value, str):
               raise TypeError(f'Expected {value!r} to be a str')
           if self.minsize is not None and len(value) < self.minsize:
               raise ValueError(
                   f'Expected {value!r} to be no smaller than {self.minsize!r}'
               )
           if self.maxsize is not None and len(value) > self.maxsize:
               raise ValueError(
                   f'Expected {value!r} to be no bigger than {self.maxsize!r}'
               )
           if self.predicate is not None and not self.predicate(value):
               raise ValueError(
                   f'Expected {self.predicate} to be true for {value!r}'
               )


Praktische Anwendung
--------------------

So lassen sich die Datenprüfer in einer echten Klasse verwenden:

   class Component:

       name = String(minsize=3, maxsize=10, predicate=str.isupper)
       kind = OneOf('wood', 'metal', 'plastic')
       quantity = Number(minvalue=0)

       def __init__(self, name, kind, quantity):
           self.name = name
           self.kind = kind
           self.quantity = quantity

Die Deskriptoren verhindern, dass ungültige Instanzen erzeugt werden:

   >>> Component('Widget', 'metal', 5)      # Blocked: 'Widget' is not all uppercase
   Traceback (most recent call last):
       ...
   ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'

   >>> Component('WIDGET', 'metle', 5)      # Blocked: 'metle' is misspelled
   Traceback (most recent call last):
       ...
   ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}

   >>> Component('WIDGET', 'metal', -5)     # Blocked: -5 is negative
   Traceback (most recent call last):
       ...
   ValueError: Expected -5 to be at least 0

   >>> Component('WIDGET', 'metal', 'V')    # Blocked: 'V' isn't a number
   Traceback (most recent call last):
       ...
   TypeError: Expected 'V' to be an int or float

   >>> c = Component('WIDGET', 'metal', 5)  # Allowed:  The inputs are valid


Technisches Tutorial
====================

Was folgt, ist ein technischeres Tutorial über die Mechanik und die
Einzelheiten, wie Deskriptoren arbeiten.


Zusammenfassung
---------------

Legt Deskriptoren fest, fasst das Protokoll zusammen und zeigt, wie
Deskriptoren aufgerufen werden. Bietet ein Beispiel, das zeigt, wie
objektrelationale Abbildungen arbeiten.

Über Deskriptoren zu lernen gibt nicht nur Zugang zu einem größeren
Werkzeugkasten, es schafft auch ein tieferes Verständnis davon, wie
Python arbeitet.


Begriffsbestimmung und Einführung
---------------------------------

Im Allgemeinen ist ein Deskriptor ein Attributwert, der eine der
Methoden des Deskriptor-Protokolls hat. Diese Methoden sind
"__get__()", "__set__()" und "__delete__()". Ist eine dieser Methoden
für ein Attribut definiert, sagt man, es sei ein *Deskriptor*.

Das Standardverhalten beim Attributzugriff ist, das Attribut aus dem
Wörterbuch eines Objekts zu lesen, darin zu setzen oder daraus zu
löschen. So hat zum Beispiel "a.x" eine Nachschlagekette, die bei
"a.__dict__['x']" beginnt, dann "type(a).__dict__['x']" folgt und sich
durch die Reihenfolge der Methodenauflösung von "type(a)" fortsetzt.
Ist der nachgeschlagene Wert ein Objekt, das eine der Deskriptor-
Methoden definiert, kann Python das Standardverhalten überschreiben
und stattdessen die Deskriptor-Methode aufrufen. Wo das in der
Vorrangkette geschieht, hängt davon ab, welche Deskriptor-Methoden
definiert wurden.

Deskriptoren sind ein mächtiges Protokoll für allgemeine Zwecke. Sie
sind der Mechanismus hinter Properties, Methoden, statischen Methoden,
Klassenmethoden und "super()". Sie werden in Python selbst durchgehend
verwendet. Deskriptoren vereinfachen den darunterliegenden C-Code und
bieten einen flexiblen Satz neuer Werkzeuge für alltägliche Python-
Programme.


Das Deskriptor-Protokoll
------------------------

"descr.__get__(self, obj, type=None)"

"descr.__set__(self, obj, value)"

"descr.__delete__(self, obj)"

Mehr ist es nicht. Definiere eine dieser Methoden, und ein Objekt gilt
als Deskriptor und kann das Standardverhalten überschreiben, wenn es
als Attribut nachgeschlagen wird.

Definiert ein Objekt "__set__()" oder "__delete__()", gilt es als
Datendeskriptor. Deskriptoren, die nur "__get__()" definieren, nennt
man Nicht-Datendeskriptoren (sie werden oft für Methoden verwendet,
andere Verwendungen sind aber möglich).

Daten- und Nicht-Datendeskriptoren unterscheiden sich darin, wie das
Überschreiben gegenüber Einträgen im Wörterbuch einer Instanz
berechnet wird. Hat das Wörterbuch einer Instanz einen Eintrag mit
demselben Namen wie ein Datendeskriptor, hat der Datendeskriptor
Vorrang. Hat das Wörterbuch einer Instanz einen Eintrag mit demselben
Namen wie ein Nicht-Datendeskriptor, hat der Wörterbucheintrag
Vorrang.

Um einen nur lesbaren Datendeskriptor zu bauen, definiere sowohl
"__get__()" als auch "__set__()", wobei "__set__()" beim Aufruf einen
"AttributeError" auslöst. Die Methode "__set__()" mit einem
Platzhalter zu definieren, der eine Ausnahme auslöst, genügt, um
daraus einen Datendeskriptor zu machen.


Überblick über den Aufruf von Deskriptoren
------------------------------------------

Ein Deskriptor lässt sich unmittelbar mit "desc.__get__(obj)" oder
"desc.__get__(None, cls)" aufrufen.

Häufiger ist es aber, dass ein Deskriptor beim Attributzugriff
automatisch aufgerufen wird.

Der Ausdruck "obj.x" schlägt das Attribut "x" in der Kette der
Namensräume für "obj" nach. Findet die Suche einen Deskriptor
außerhalb des "__dict__" der Instanz, wird seine Methode "__get__()"
nach den unten aufgeführten Vorrangregeln aufgerufen.

Die Einzelheiten des Aufrufs hängen davon ab, ob "obj" ein Objekt,
eine Klasse oder eine super-Instanz ist.


Aufruf aus einer Instanz
------------------------

Das Nachschlagen in einer Instanz durchläuft eine Kette von
Namensräumen und gibt Datendeskriptoren den höchsten Vorrang, gefolgt
von Instanzvariablen, dann Nicht-Datendeskriptoren, dann
Klassenvariablen und zuletzt "__getattr__()", sofern vorhanden.

Wird für "a.x" ein Deskriptor gefunden, wird er so aufgerufen:
"desc.__get__(a, type(a))".

Die Logik für ein Nachschlagen mit Punkt steckt in
"object.__getattribute__()". Hier ein reines Python-Gegenstück:

   def find_name_in_mro(cls, name, default):
       "Emulate _PyType_Lookup() in Objects/typeobject.c"
       for base in cls.__mro__:
           if name in vars(base):
               return vars(base)[name]
       return default

   def object_getattribute(obj, name):
       "Emulate PyObject_GenericGetAttr() in Objects/object.c"
       null = sentinel('null')
       objtype = type(obj)
       cls_var = find_name_in_mro(objtype, name, null)
       descr_get = getattr(type(cls_var), '__get__', null)
       if descr_get is not null:
           if (hasattr(type(cls_var), '__set__')
               or hasattr(type(cls_var), '__delete__')):
               return descr_get(cls_var, obj, objtype)     # data descriptor
       if hasattr(obj, '__dict__') and name in vars(obj):
           return vars(obj)[name]                          # instance variable
       if descr_get is not null:
           return descr_get(cls_var, obj, objtype)         # non-data descriptor
       if cls_var is not null:
           return cls_var                                  # class variable
       raise AttributeError(name)

Beachte, dass es im Code von "__getattribute__()" keinen Haken
"__getattr__()" gibt. Deshalb umgeht ein unmittelbarer Aufruf von
"__getattribute__()" oder einer über "super().__getattribute__"
"__getattr__()" vollständig.

Es sind stattdessen der Punktoperator und die Funktion "getattr()",
die dafür zuständig sind, "__getattr__()" aufzurufen, wenn
"__getattribute__()" einen "AttributeError" auslöst. Ihre Logik ist in
einer Hilfsfunktion gekapselt:

   def getattr_hook(obj, name):
       "Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
       try:
           return obj.__getattribute__(name)
       except AttributeError:
           if not hasattr(type(obj), '__getattr__'):
               raise
       return type(obj).__getattr__(obj, name)             # __getattr__


Aufruf aus einer Klasse
-----------------------

Die Logik für ein Nachschlagen mit Punkt wie "A.x" steckt in
"type.__getattribute__()". Die Schritte ähneln denen von
"object.__getattribute__()", aber das Nachschlagen im
Instanzwörterbuch wird durch eine Suche in der *Reihenfolge der
Methodenauflösung* der Klasse ersetzt.

Wird ein Deskriptor gefunden, wird er mit "desc.__get__(None, A)"
aufgerufen.

Die vollständige C-Umsetzung findest du in "type_getattro()" und
"_PyType_Lookup()" in Objects/typeobject.c.


Aufruf aus super
----------------

Die Logik für das Nachschlagen mit Punkt bei super steckt in der
Methode "__getattribute__()" des Objekts, das "super()" zurückgibt.

Ein Nachschlagen mit Punkt wie "super(A, obj).m" durchsucht
"obj.__class__.__mro__" nach der Basisklasse "B", die unmittelbar auf
"A" folgt, und gibt dann "B.__dict__['m'].__get__(obj, A)" zurück. Ist
"m" kein Deskriptor, wird es unverändert zurückgegeben.

Die vollständige C-Umsetzung findest du in "super_getattro()" in
Objects/typeobject.c. Ein reines Python-Gegenstück findest du in
Guidos Tutorial.


Zusammenfassung der Aufruflogik
-------------------------------

Der Mechanismus für Deskriptoren steckt in den Methoden
"__getattribute__()" von "object", "type" und "super()".

Die wichtigen Punkte zum Merken sind:

* Deskriptoren werden von der Methode "__getattribute__()" aufgerufen.

* Klassen erben diese Maschinerie von "object", "type" oder "super()".

* "__getattribute__()" zu überschreiben verhindert automatische
  Deskriptor-Aufrufe, weil die gesamte Deskriptor-Logik in dieser
  Methode steckt.

* "object.__getattribute__()" und "type.__getattribute__()" rufen
  "__get__()" unterschiedlich auf. Das erste übergibt die Instanz und
  möglicherweise die Klasse. Das zweite setzt für die Instanz "None"
  ein und übergibt immer die Klasse.

* Datendeskriptoren überschreiben Instanzwörterbücher immer.

* Nicht-Datendeskriptoren können von Instanzwörterbüchern
  überschrieben werden.


Automatische Namensbenachrichtigung
-----------------------------------

Manchmal ist es wünschenswert, dass ein Deskriptor weiß, welchem
Klassenvariablennamen er zugewiesen wurde. Wird eine neue Klasse
erzeugt, durchsucht die Metaklasse "type" das Wörterbuch der neuen
Klasse. Sind Einträge darin Deskriptoren und definieren sie
"__set_name__()", wird diese Methode mit zwei Argumenten aufgerufen.
*owner* ist die Klasse, in der der Deskriptor verwendet wird, und
*name* ist die Klassenvariable, der der Deskriptor zugewiesen wurde.

Die Einzelheiten der Umsetzung stehen in "type_new()" und
"set_names()" in Objects/typeobject.c.

Da die Aktualisierungslogik in "type.__new__()" steckt, finden
Benachrichtigungen nur zum Zeitpunkt der Klassenerzeugung statt.
Werden Deskriptoren der Klasse danach hinzugefügt, muss
"__set_name__()" von Hand aufgerufen werden.


ORM-Beispiel
------------

Der folgende Code ist ein vereinfachtes Gerüst, das zeigt, wie sich
mit Datendeskriptoren eine objektrelationale Abbildung umsetzen ließe.

Die wesentliche Idee ist, dass die Daten in einer externen Datenbank
gespeichert sind. Die Python-Instanzen halten nur Schlüssel zu den
Tabellen der Datenbank. Die Deskriptoren kümmern sich um das
Nachschlagen und Aktualisieren:

   class Field:

       def __set_name__(self, owner, name):
           self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
           self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'

       def __get__(self, obj, objtype=None):
           return conn.execute(self.fetch, [obj.key]).fetchone()[0]

       def __set__(self, obj, value):
           conn.execute(self.store, [value, obj.key])
           conn.commit()

Mit der Klasse "Field" können wir Modelle definieren, die das Schema
jeder Tabelle in einer Datenbank beschreiben:

   class Movie:
       table = 'Movies'                    # Table name
       key = 'title'                       # Primary key
       director = Field()
       year = Field()

       def __init__(self, key):
           self.key = key

   class Song:
       table = 'Music'
       key = 'title'
       artist = Field()
       year = Field()
       genre = Field()

       def __init__(self, key):
           self.key = key

Um die Modelle zu verwenden, verbinde dich zuerst mit der Datenbank:

   >>> import sqlite3
   >>> conn = sqlite3.connect('entertainment.db')

Eine interaktive Sitzung zeigt, wie Daten aus der Datenbank abgerufen
und wie sie aktualisiert werden können:

   >>> Movie('Star Wars').director
   'George Lucas'
   >>> jaws = Movie('Jaws')
   >>> f'Released in {jaws.year} by {jaws.director}'
   'Released in 1975 by Steven Spielberg'

   >>> Song('Country Roads').artist
   'John Denver'

   >>> Movie('Star Wars').director = 'J.J. Abrams'
   >>> Movie('Star Wars').director
   'J.J. Abrams'


Reine Python-Gegenstücke
========================

Das Deskriptor-Protokoll ist einfach und bietet spannende
Möglichkeiten. Mehrere Anwendungsfälle sind so verbreitet, dass sie in
integrierten Werkzeugen fertig verpackt sind. Properties, gebundene
Methoden, statische Methoden, Klassenmethoden und __slots__ beruhen
alle auf dem Deskriptor-Protokoll.


Properties
----------

"property()" aufzurufen ist ein knapper Weg, einen Datendeskriptor zu
bauen, der beim Zugriff auf ein Attribut einen Funktionsaufruf
auslöst. Seine Signatur ist:

   property(fget=None, fset=None, fdel=None, doc=None) -> property

Die Dokumentation zeigt eine typische Verwendung, um ein verwaltetes
Attribut "x" zu definieren:

   class C:
       def getx(self): return self.__x
       def setx(self, value): self.__x = value
       def delx(self): del self.__x
       x = property(getx, setx, delx, "I'm the 'x' property.")

Um zu sehen, wie "property()" mit Hilfe des Deskriptor-Protokolls
umgesetzt ist, hier ein reines Python-Gegenstück, das den größten Teil
der Kernfunktionalität umsetzt:

   class Property:
       "Emulate PyProperty_Type() in Objects/descrobject.c"

       def __init__(self, fget=None, fset=None, fdel=None, doc=None):
           self.fget = fget
           self.fset = fset
           self.fdel = fdel
           if doc is None and fget is not None:
               doc = fget.__doc__
           self.__doc__ = doc

       def __set_name__(self, owner, name):
           self.__name__ = name

       def __get__(self, obj, objtype=None):
           if obj is None:
               return self
           if self.fget is None:
               raise AttributeError
           return self.fget(obj)

       def __set__(self, obj, value):
           if self.fset is None:
               raise AttributeError
           self.fset(obj, value)

       def __delete__(self, obj):
           if self.fdel is None:
               raise AttributeError
           self.fdel(obj)

       def getter(self, fget):
           return type(self)(fget, self.fset, self.fdel, self.__doc__)

       def setter(self, fset):
           return type(self)(self.fget, fset, self.fdel, self.__doc__)

       def deleter(self, fdel):
           return type(self)(self.fget, self.fset, fdel, self.__doc__)

Die integrierte Funktion "property()" hilft immer dann, wenn eine
Benutzeroberfläche Attributzugriff gewährt hat und spätere Änderungen
das Eingreifen einer Methode erfordern.

So kann eine Tabellenkalkulationsklasse zum Beispiel über
"Cell('b10').value" Zugriff auf einen Zellenwert gewähren. Spätere
Verbesserungen am Programm erfordern, dass die Zelle bei jedem Zugriff
neu berechnet wird. Der Programmierer will bestehenden Clientcode, der
unmittelbar auf das Attribut zugreift, aber nicht beeinträchtigen. Die
Lösung ist, den Zugriff auf das Attribut value in einen property-
Datendeskriptor zu hüllen:

   class Cell:
       ...

       @property
       def value(self):
           "Recalculate the cell before returning value"
           self.recalc()
           return self._value

In diesem Beispiel würden sowohl die integrierte Funktion "property()"
als auch unser Gegenstück "Property()" funktionieren.


Funktionen und Methoden
-----------------------

Die objektorientierten Eigenschaften von Python ruhen auf einer
funktionsbasierten Umgebung. Über Nicht-Datendeskriptoren werden die
beiden bruchlos verbunden.

In Klassenwörterbüchern abgelegte Funktionen werden beim Aufruf zu
Methoden. Methoden unterscheiden sich von gewöhnlichen Funktionen nur
darin, dass die Objektinstanz den anderen Argumenten vorangestellt
wird. Per Konvention heißt die Instanz *self*, könnte aber *this* oder
wie jede andere Variable heißen.

Methoden lassen sich mit "types.MethodType" von Hand erzeugen, was
etwa Folgendem entspricht:

   class MethodType:
       "Emulate PyMethod_Type in Objects/classobject.c"

       def __init__(self, func, obj):
           self.__func__ = func
           self.__self__ = obj

       def __call__(self, *args, **kwargs):
           func = self.__func__
           obj = self.__self__
           return func(obj, *args, **kwargs)

       def __getattribute__(self, name):
           "Emulate method_getset() in Objects/classobject.c"
           if name == '__doc__':
               return self.__func__.__doc__
           return object.__getattribute__(self, name)

       def __getattr__(self, name):
           "Emulate method_getattro() in Objects/classobject.c"
           return getattr(self.__func__, name)

       def __get__(self, obj, objtype=None):
           "Emulate method_descr_get() in Objects/classobject.c"
           return self

Um die automatische Erzeugung von Methoden zu unterstützen, haben
Funktionen die Methode "__get__()", um Methoden beim Attributzugriff
zu binden. Das heißt, dass Funktionen Nicht-Datendeskriptoren sind,
die beim Nachschlagen mit Punkt aus einer Instanz gebundene Methoden
zurückgeben. So funktioniert es:

   class Function:
       ...

       def __get__(self, obj, objtype=None):
           "Simulate func_descr_get() in Objects/funcobject.c"
           if obj is None:
               return self
           return MethodType(self, obj)

Lässt man die folgende Klasse im Interpreter laufen, zeigt sich, wie
der Funktionsdeskriptor in der Praxis arbeitet:

   class D:
       def f(self):
            return self

   class D2:
       pass

Die Funktion hat ein Attribut *qualifizierter Name*, um Introspektion
zu unterstützen:

   >>> D.f.__qualname__
   'D.f'

Auf die Funktion über das Klassenwörterbuch zuzugreifen ruft
"__get__()" nicht auf. Es gibt stattdessen einfach das
darunterliegende Funktionsobjekt zurück:

   >>> D.__dict__['f']
   <function D.f at 0x00C45070>

Der Zugriff mit Punkt aus einer Klasse ruft "__get__()" auf, das die
darunterliegende Funktion einfach unverändert zurückgibt:

   >>> D.f
   <function D.f at 0x00C45070>

Das interessante Verhalten tritt beim Zugriff mit Punkt aus einer
Instanz auf. Das Nachschlagen mit Punkt ruft "__get__()" auf, das ein
Objekt für eine gebundene Methode zurückgibt:

   >>> d = D()
   >>> d.f
   <bound method D.f of <__main__.D object at 0x00B18C90>>

Intern speichert die gebundene Methode die darunterliegende Funktion
und die gebundene Instanz:

   >>> d.f.__func__
   <function D.f at 0x00C45070>

   >>> d.f.__self__
   <__main__.D object at 0x00B18C90>

Wenn du sich je gefragt hast, wo *self* in gewöhnlichen Methoden oder
*cls* in Klassenmethoden herkommt: Das ist es!


Arten von Methoden
------------------

Nicht-Datendeskriptoren bieten einen einfachen Mechanismus für
Abwandlungen der üblichen Muster, Funktionen in Methoden zu binden.

Zur Wiederholung: Funktionen haben eine Methode "__get__()", damit sie
beim Zugriff als Attribut in eine Methode umgewandelt werden können.
Der Nicht-Datendeskriptor verwandelt einen Aufruf "obj.f(*args)" in
"f(obj, *args)". Ein Aufruf von "cls.f(*args)" wird zu "f(*args)".

Diese Tabelle fasst die Bindung und ihre zwei nützlichsten
Abwandlungen zusammen:

   +-------------------+------------------------+--------------------+
   | Umwandlung        | Aufruf aus einem       | Aufruf aus einer   |
   |                   | Objekt                 | Klasse             |
   |===================|========================|====================|
   | function          | f(obj, *args)          | f(*args)           |
   +-------------------+------------------------+--------------------+
   | staticmethod      | f(*args)               | f(*args)           |
   +-------------------+------------------------+--------------------+
   | classmethod       | f(type(obj), *args)    | f(cls, *args)      |
   +-------------------+------------------------+--------------------+


Statische Methoden
------------------

Statische Methoden geben die darunterliegende Funktion unverändert
zurück. Ein Aufruf von "c.f" oder "C.f" entspricht einem unmittelbaren
Nachschlagen in "object.__getattribute__(c, "f")" beziehungsweise
"object.__getattribute__(C, "f")". Die Funktion ist dadurch aus einem
Objekt und aus einer Klasse auf dieselbe Weise zugänglich.

Gute Kandidaten für statische Methoden sind Methoden, die die Variable
"self" nicht verwenden.

So kann ein Statistikpaket zum Beispiel eine Containerklasse für
Versuchsdaten enthalten. Die Klasse bietet gewöhnliche Methoden, um
Durchschnitt, Mittelwert, Median und andere beschreibende Statistiken
zu berechnen, die von den Daten abhängen. Es kann aber nützliche
Funktionen geben, die begrifflich dazugehören, aber nicht von den
Daten abhängen. So ist "erf(x)" zum Beispiel eine praktische
Umwandlungsroutine, die in der statistischen Arbeit vorkommt, aber
nicht unmittelbar von einem bestimmten Datensatz abhängt. Sie lässt
sich entweder aus einem Objekt oder aus der Klasse aufrufen:
"s.erf(1.5) --> 0.9332" oder "Sample.erf(1.5) --> 0.9332".

Da statische Methoden die darunterliegende Funktion unverändert
zurückgeben, sind die Beispielaufrufe unspektakulär:

   class E:
       @staticmethod
       def f(x):
           return x * 10

   >>> E.f(3)
   30
   >>> E().f(3)
   30

Mit dem Protokoll für Nicht-Datendeskriptoren würde eine reine Python-
Fassung von "@staticmethod" so aussehen:

   import functools

   class StaticMethod:
       "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

       def __init__(self, f):
           self.f = f
           functools.update_wrapper(self, f)

       def __get__(self, obj, objtype=None):
           return self.f

       def __call__(self, *args, **kwds):
           return self.f(*args, **kwds)

       @property
       def __annotations__(self):
           return self.f.__annotations__

Der Aufruf von "functools.update_wrapper()" ergänzt ein Attribut
"__wrapped__", das auf die darunterliegende Funktion verweist.
Außerdem überträgt er die Attribute, die nötig sind, damit die Hülle
wie die umhüllte Funktion aussieht, darunter "__name__",
"__qualname__" und "__doc__".


Klassenmethoden
---------------

Anders als statische Methoden stellen Klassenmethoden der
Argumentliste die Klassenreferenz voran, bevor sie die Funktion
aufrufen. Dieses Format ist gleich, ob der Aufrufer ein Objekt oder
eine Klasse ist:

   class F:
       @classmethod
       def f(cls, x):
           return cls.__name__, x

   >>> F.f(3)
   ('F', 3)
   >>> F().f(3)
   ('F', 3)

Dieses Verhalten ist immer dann nützlich, wenn die Methode nur eine
Klassenreferenz braucht und sich nicht auf Daten stützt, die in einer
bestimmten Instanz gespeichert sind. Eine Verwendung von
Klassenmethoden ist, alternative Klassenkonstruktoren zu bauen. So
erzeugt zum Beispiel die Klassenmethode "dict.fromkeys()" ein neues
Wörterbuch aus einer Liste von Schlüsseln. Das reine Python-Gegenstück
ist:

   class Dict(dict):
       @classmethod
       def fromkeys(cls, iterable, value=None):
           "Emulate dict_fromkeys() in Objects/dictobject.c"
           d = cls()
           for key in iterable:
               d[key] = value
           return d

Nun lässt sich ein neues Wörterbuch mit eindeutigen Schlüsseln so
bauen:

   >>> d = Dict.fromkeys('abracadabra')
   >>> type(d) is Dict
   True
   >>> d
   {'a': None, 'b': None, 'r': None, 'c': None, 'd': None}

Mit dem Protokoll für Nicht-Datendeskriptoren würde eine reine Python-
Fassung von "@classmethod" so aussehen:

   import functools

   class ClassMethod:
       "Emulate PyClassMethod_Type() in Objects/funcobject.c"

       def __init__(self, f):
           self.f = f
           functools.update_wrapper(self, f)

       def __get__(self, obj, cls=None):
           if cls is None:
               cls = type(obj)
           return MethodType(self.f, cls)

Der Aufruf von "functools.update_wrapper()" in "ClassMethod" ergänzt
ein Attribut "__wrapped__", das auf die darunterliegende Funktion
verweist. Außerdem überträgt er die Attribute, die nötig sind, damit
die Hülle wie die umhüllte Funktion aussieht: "__name__",
"__qualname__", "__doc__" und "__annotations__".


Member-Objekte und __slots__
----------------------------

Definiert eine Klasse "__slots__", ersetzt sie Instanzwörterbücher
durch ein Feld fester Länge mit Slot-Werten. Aus Sicht des Benutzers
hat das mehrere Wirkungen:

1. Provides immediate detection of bugs due to misspelled attribute
assignments.  Only attribute names specified in "__slots__" are
allowed:

   class Vehicle:
       __slots__ = ('id_number', 'make', 'model')

   >>> auto = Vehicle()
   >>> auto.id_nubmer = 'VYE483814LQEX'
   Traceback (most recent call last):
       ...
   AttributeError: 'Vehicle' object has no attribute 'id_nubmer'

2. Helps create immutable objects where descriptors manage access to
private attributes stored in "__slots__":

   class Immutable:

       __slots__ = ('_dept', '_name')          # Replace the instance dictionary

       def __init__(self, dept, name):
           self._dept = dept                   # Store to private attribute
           self._name = name                   # Store to private attribute

       @property                               # Read-only descriptor
       def dept(self):
           return self._dept

       @property
       def name(self):                         # Read-only descriptor
           return self._name

   >>> mark = Immutable('Botany', 'Mark Watney')
   >>> mark.dept
   'Botany'
   >>> mark.dept = 'Space Pirate'
   Traceback (most recent call last):
       ...
   AttributeError: property 'dept' of 'Immutable' object has no setter
   >>> mark.location = 'Mars'
   Traceback (most recent call last):
       ...
   AttributeError: 'Immutable' object has no attribute 'location'

3. Saves memory.  On a 64-bit Linux build, an instance with two
attributes takes 48 bytes with "__slots__" and 152 bytes without.
This flyweight design pattern likely only matters when a large number
of instances are going to be created.

4. Improves speed.  Reading instance variables is 35% faster with
"__slots__" (as measured with Python 3.10 on an Apple M1 processor).

5. Blocks tools like "@functools.cached_property" which require an
instance dictionary to function correctly:

   from functools import cached_property

   class CP:
       __slots__ = ()                          # Eliminates the instance dict

       @cached_property                        # Requires an instance dict
       def pi(self):
           return 4 * sum((-1.0)**n / (2.0*n + 1.0)
                          for n in reversed(range(100_000)))

   >>> CP().pi
   Traceback (most recent call last):
     ...
   TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.

Es ist nicht möglich, eine genau austauschbare reine Python-Fassung
von "__slots__" zu bauen, weil dafür unmittelbarer Zugriff auf
C-Strukturen und Kontrolle über die Speicheranforderung von Objekten
nötig ist. Wir können aber eine größtenteils treue Nachbildung bauen,
in der die eigentliche C-Struktur für Slots durch eine private Liste
"_slotvalues" nachgebildet wird. Lese- und Schreibzugriffe auf diese
private Struktur werden von Member-Deskriptoren verwaltet:

   null = sentinel('null')

   class Member:

       def __init__(self, name, clsname, offset):
           'Emulate PyMemberDef in Include/descrobject.h'
           # Also see descr_new() in Objects/descrobject.c
           self.name = name
           self.clsname = clsname
           self.offset = offset

       def __get__(self, obj, objtype=None):
           'Emulate member_get() in Objects/descrobject.c'
           # Also see PyMember_GetOne() in Python/structmember.c
           if obj is None:
               return self
           value = obj._slotvalues[self.offset]
           if value is null:
               raise AttributeError(self.name)
           return value

       def __set__(self, obj, value):
           'Emulate member_set() in Objects/descrobject.c'
           obj._slotvalues[self.offset] = value

       def __delete__(self, obj):
           'Emulate member_delete() in Objects/descrobject.c'
           value = obj._slotvalues[self.offset]
           if value is null:
               raise AttributeError(self.name)
           obj._slotvalues[self.offset] = null

       def __repr__(self):
           'Emulate member_repr() in Objects/descrobject.c'
           return f'<Member {self.name!r} of {self.clsname!r}>'

Die Methode "type.__new__()" kümmert sich darum, Member-Objekte zu den
Klassenvariablen hinzuzufügen:

   class Type(type):
       'Simulate how the type metaclass adds member objects for slots'

       def __new__(mcls, clsname, bases, mapping, **kwargs):
           'Emulate type_new() in Objects/typeobject.c'
           # type_new() calls PyTypeReady() which calls add_methods()
           slot_names = mapping.get('slot_names', [])
           for offset, name in enumerate(slot_names):
               mapping[name] = Member(name, clsname, offset)
           return type.__new__(mcls, clsname, bases, mapping, **kwargs)

Die Methode "object.__new__()" kümmert sich darum, Instanzen zu
erzeugen, die Slots anstelle eines Instanzwörterbuchs haben. Hier eine
grobe Nachbildung in reinem Python:

   class Object:
       'Simulate how object.__new__() allocates memory for __slots__'

       def __new__(cls, *args, **kwargs):
           'Emulate object_new() in Objects/typeobject.c'
           inst = super().__new__(cls)
           if hasattr(cls, 'slot_names'):
               empty_slots = [null] * len(cls.slot_names)
               object.__setattr__(inst, '_slotvalues', empty_slots)
           return inst

       def __setattr__(self, name, value):
           'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
           cls = type(self)
           if hasattr(cls, 'slot_names') and name not in cls.slot_names:
               raise AttributeError(
                   f'{cls.__name__!r} object has no attribute {name!r}'
               )
           super().__setattr__(name, value)

       def __delattr__(self, name):
           'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
           cls = type(self)
           if hasattr(cls, 'slot_names') and name not in cls.slot_names:
               raise AttributeError(
                   f'{cls.__name__!r} object has no attribute {name!r}'
               )
           super().__delattr__(name)

Um die Nachbildung in einer echten Klasse zu verwenden, erbe einfach
von "Object" und setze die *Metaklasse* auf "Type":

   class H(Object, metaclass=Type):
       'Instance variables stored in slots'

       slot_names = ['x', 'y']

       def __init__(self, x, y):
           self.x = x
           self.y = y

An diesem Punkt hat die Metaklasse Member-Objekte für *x* und *y*
geladen:

   >>> from pprint import pp
   >>> pp(dict(vars(H)))
   {'__module__': '__main__',
    '__doc__': 'Instance variables stored in slots',
    'slot_names': ['x', 'y'],
    '__init__': <function H.__init__ at 0x7fb5d302f9d0>,
    'x': <Member 'x' of 'H'>,
    'y': <Member 'y' of 'H'>}

Werden Instanzen erzeugt, haben sie eine Liste "slot_values", in der
die Attribute gespeichert sind:

   >>> h = H(10, 20)
   >>> vars(h)
   {'_slotvalues': [10, 20]}
   >>> h.x = 55
   >>> vars(h)
   {'_slotvalues': [55, 20]}

Falsch geschriebene oder nicht zugewiesene Attribute lösen eine
Ausnahme aus:

   >>> h.xz
   Traceback (most recent call last):
       ...
   AttributeError: 'H' object has no attribute 'xz'
