Deskriptor-Leitfaden¶
- Autor:
Raymond Hettinger
- Kontakt:
<python at rcn dot com>
Deskriptoren ermöglichen Objekten, Attribut-Lookup, Speicherung und Löschung anzupassen.
Dieser Leitfaden hat vier Hauptabschnitte.
Die „Einführung“ bietet einen grundlegenden Überblick und bewegt sich sanft von einfachen Beispielen vorwärts, wobei jeweils eine Funktion hinzugefügt wird. Beginnen Sie hier, wenn Sie neu bei Deskriptoren sind.
Der zweite Abschnitt zeigt ein vollständiges, praktisches Deskriptor-Beispiel. Wenn Sie die Grundlagen bereits kennen, beginnen Sie dort.
Der dritte Abschnitt bietet eine technischere Anleitung, die sich mit den detaillierten Mechanismen der Funktionsweise von Deskriptoren befasst. Die meisten Leute benötigen dieses Detailniveau nicht.
Der letzte Abschnitt enthält reine Python-Entsprechungen für integrierte Deskriptoren, die in C geschrieben sind. Lesen Sie diesen, wenn Sie neugierig sind, wie Funktionen zu gebundenen Methoden werden oder wie gängige Werkzeuge wie
classmethod(),staticmethod(),property()und __slots__ implementiert sind.
Einführung¶
In dieser Einführung beginnen wir mit dem denkbar einfachsten Beispiel und fügen dann schrittweise neue Funktionen hinzu.
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 gespeichert werden.
class A:
x = 5 # Regular class attribute
y = Ten() # Descriptor instance
Eine interaktive Sitzung zeigt den Unterschied zwischen normalem Attribut-Lookup und Deskriptor-Lookup.
>>> a = A() # Make an instance of class A
>>> a.x # Normal attribute lookup
5
>>> a.y # Descriptor lookup
10
Beim Attribut-Lookup a.x findet der Punktoperator 'x': 5 im Klassenwörterbuch. Beim Lookup a.y findet der Punktoperator eine Deskriptorinstanz, die durch ihre __get__-Methode erkannt wird. Das Aufrufen dieser Methode gibt 10 zurück.
Beachten Sie, dass der Wert 10 weder im Klassenwörterbuch noch im Instanzwörterbuch gespeichert ist. Stattdessen wird der Wert 10 bei Bedarf berechnet.
Dieses Beispiel zeigt, wie ein einfacher Deskriptor funktioniert, ist aber nicht sehr nützlich. Zum Abrufen von Konstanten wäre ein normaler Attribut-Lookup besser.
Im nächsten Abschnitt erstellen wir etwas Nützlicheres, einen dynamischen Lookup.
Dynamische Lookups¶
Interessante Deskriptoren führen typischerweise Berechnungen durch, 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 der Lookup dynamisch ist – er berechnet jedes Mal unterschiedliche, aktualisierte 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 durchführen können, sondern offenbart auch den Zweck der Parameter für __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 es der Methode __get__() ermöglicht, das Zielverzeichnis zu erfahren. Der Parameter *objtype* ist die Klasse *Directory*.
Verwaltete Attribute¶
Ein beliebter Verwendungszweck für Deskriptoren ist die Verwaltung des Zugriffs auf Instanzdaten. 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 Attribut und *_age* das private Attribut. Wenn auf das öffentliche Attribut zugegriffen wird, protokolliert der Deskriptor den Lookup 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 auf alle Zugriffe auf das verwaltete Attribut *age* zugegriffen wird, aber dass auf das normale Attribut *name* nicht zugegriffen wird.
>>> 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 großes Problem bei diesem Beispiel ist, dass der private Name *_age* fest in der Klasse *LoggedAgeAccess* kodiert ist. Das bedeutet, dass jede Instanz nur ein protokolliertes Attribut haben kann und dessen Name nicht geändert werden kann. Im nächsten Beispiel werden wir dieses Problem beheben.
Benutzerdefinierte Namen¶
Wenn eine Klasse Deskriptoren verwendet, kann sie jeden Deskriptor darüber informieren, welchem Variablennamen er zugewiesen wurde.
In diesem Beispiel hat die Klasse Person zwei Deskriptorinstanzen, *name* und *age*. Wenn die Klasse Person definiert wird, ruft sie __set_name__() in *LoggedAccess* zurück, damit die Feldnamen aufgezeichnet werden können, wodurch jeder Deskriptor seinen eigenen *public_name* und *private_name* erhält.
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 aufgezeichnet 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 *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 die Bezeichnung für jedes Objekt, das __get__(), __set__() oder __delete__() definiert.
Optional können Deskriptoren eine Methode __set_name__() haben. Diese wird nur in Fällen verwendet, in denen ein Deskriptor entweder die Klasse, in der er erstellt wurde, oder den Namen der Klassenvariable, der er zugewiesen wurde, kennen muss. (Diese Methode, falls vorhanden, wird auch dann aufgerufen, wenn die Klasse kein Deskriptor ist.)
Deskriptoren werden durch den Punktoperator während des Attribut-Lookups aufgerufen. Wenn auf einen Deskriptor indirekt mit vars(some_class)[descriptor_name] zugegriffen wird, wird die Deskriptorinstanz zurückgegeben, ohne sie aufzurufen.
Deskriptoren funktionieren nur, wenn sie als Klassenvariablen verwendet werden. Wenn sie in Instanzen platziert werden, haben sie keine Auswirkung.
Die Hauptmotivation für Deskriptoren ist die Bereitstellung eines Hooks, der es Objekten, die in Klassenvariablen gespeichert sind, ermöglicht, zu steuern, was während des Attribut-Lookups geschieht.
Traditionell steuert die aufrufende Klasse, was beim Lookup geschieht. Deskriptoren kehren diese Beziehung um und erlauben es den nachgeschlagenen Daten, ein Mitspracherecht zu haben.
Deskriptoren werden in der gesamten Sprache verwendet. So werden Funktionen zu gebundenen Methoden. Gängige Werkzeuge wie classmethod(), staticmethod(), property() und functools.cached_property() sind alle als Deskriptoren implementiert.
Vollständiges praktisches Beispiel¶
In diesem Beispiel erstellen wir ein praktisches und leistungsfähiges Werkzeug zur Lokalisierung von notorisch schwer zu findenden Datenkorruptionsfehlern.
Validator-Klasse¶
Ein Validator ist ein Deskriptor für den verwalteten Attributzugriff. Bevor Daten gespeichert werden, verifiziert er, dass der neue Wert verschiedene Typ- und Bereichsbeschränkungen erfüllt. Wenn diese Beschränkungen nicht erfüllt sind, löst er eine Ausnahme aus, um Datenkorruption an ihrer Quelle zu verhindern.
Diese Klasse Validator ist sowohl eine abstrakte Basisklasse als auch ein Deskriptor für verwaltete Attribute.
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
Benutzerdefinierte Validatoren müssen von Validator erben und müssen eine Methode validate() bereitstellen, um nach Bedarf verschiedene Beschränkungen zu testen.
Benutzerdefinierte Validatoren¶
Hier sind drei praktische Datenvalidierungsdienstprogramme:
OneOfverifiziert, dass ein Wert eine von einer eingeschränkten Auswahl von Optionen ist.Numberverifiziert, dass ein Wert entweder einintoder einfloatist. Optional verifiziert er, dass ein Wert zwischen einem gegebenen Minimum oder Maximum liegt.Stringverifiziert, dass ein Wert einstrist. Optional validiert er eine gegebene Mindest- oder Maximallänge. Er kann auch ein benutzerdefiniertes Prädikat validieren.
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¶
Hier ist, wie die Datenvalidatoren in einer realen Klasse verwendet werden können.
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 erstellt 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
Technischer Leitfaden¶
Im Folgenden finden Sie eine technischere Anleitung zu den Mechanismen und Details der Funktionsweise von Deskriptoren.
Abstrakt¶
Definiert Deskriptoren, fasst das Protokoll zusammen und zeigt, wie Deskriptoren aufgerufen werden. Bietet ein Beispiel, das zeigt, wie objektrelationale Mappings funktionieren.
Das Erlernen von Deskriptoren bietet nicht nur Zugang zu einem größeren Werkzeugkasten, sondern schafft auch ein tieferes Verständnis dafür, wie Python funktioniert.
Definition und Einführung¶
Im Allgemeinen ist ein Deskriptor ein Attributwert, der eine der Methoden im Deskriptor-Protokoll aufweist. Diese Methoden sind __get__(), __set__() und __delete__(). Wenn eine dieser Methoden für ein Attribut definiert ist, wird es als Deskriptor bezeichnet.
Das Standardverhalten für den Attributzugriff besteht darin, das Attribut aus dem Wörterbuch eines Objekts abzurufen, festzulegen oder zu löschen. Zum Beispiel hat a.x eine Lookup-Kette, die mit a.__dict__['x'] beginnt, dann type(a).__dict__['x'] und weiter durch die Methodeauflösungshierarchie von type(a). Wenn der nachgeschlagene Wert ein Objekt ist, das eine der Deskriptormethoden definiert, kann Python das Standardverhalten überschreiben und stattdessen die Deskriptormethode aufrufen. Wo dies in der Prioritätskette geschieht, hängt davon ab, welche Deskriptormethoden definiert wurden.
Deskriptoren sind ein mächtiges, universelles Protokoll. Sie sind der Mechanismus hinter Eigenschaften, Methoden, statischen Methoden, Klassenmethoden und super(). Sie werden in ganz Python selbst verwendet. Deskriptoren vereinfachen den zugrunde liegenden C-Code und bieten eine flexible Auswahl neuer Werkzeuge für alltägliche Python-Programme.
Deskriptor-Protokoll¶
descr.__get__(self, obj, type=None)
descr.__set__(self, obj, value)
descr.__delete__(self, obj)
Das ist alles. Wenn eine dieser Methoden definiert ist, gilt ein Objekt als Deskriptor und kann das Standardverhalten beim Nachschlagen als Attribut überschreiben.
Wenn ein Objekt __set__() oder __delete__() definiert, gilt es als Daten-Deskriptor. Deskriptoren, die nur __get__() definieren, werden als Nicht-Daten-Deskriptoren bezeichnet (sie werden oft für Methoden verwendet, aber auch andere Verwendungen sind möglich).
Daten- und Nicht-Daten-Deskriptoren unterscheiden sich darin, wie Überschreibungen in Bezug auf Einträge im Wörterbuch einer Instanz berechnet werden. Wenn das Wörterbuch einer Instanz einen Eintrag mit demselben Namen wie ein Daten-Deskriptor hat, hat der Daten-Deskriptor Vorrang. Wenn das Wörterbuch einer Instanz einen Eintrag mit demselben Namen wie ein Nicht-Daten-Deskriptor hat, hat der Wörterbucheintrag Vorrang.
Um einen schreibgeschützten Daten-Deskriptor zu erstellen, definieren Sie sowohl __get__() als auch __set__(), wobei __set__() beim Aufruf eine AttributeError auslöst. Das Definieren der Methode __set__() mit einem Platzhalter, der eine Ausnahme auslöst, reicht aus, um sie zu einem Daten-Deskriptor zu machen.
Übersicht über die Deskriptor-Aufrufe¶
Ein Deskriptor kann direkt mit desc.__get__(obj) oder desc.__get__(None, cls) aufgerufen werden.
Es ist jedoch üblicher, dass ein Deskriptor automatisch über den Attributzugriff aufgerufen wird.
Der Ausdruck obj.x sucht das Attribut x in der Namensraumkette für obj. Wenn die Suche einen Deskriptor außerhalb des Instanz- __dict__ findet, wird seine Methode __get__() gemäß den unten aufgeführten Prioritätsregeln aufgerufen.
Die Details des Aufrufs hängen davon ab, ob obj ein Objekt, eine Klasse oder eine Instanz von super ist.
Aufruf von einer Instanz aus¶
Der Instanz-Lookup durchsucht eine Kette von Namensräumen und gibt Daten-Deskriptoren die höchste Priorität, gefolgt von Instanzvariablen, dann Nicht-Daten-Deskriptoren, dann Klassenvariablen und zuletzt __getattr__(), falls vorhanden.
Wenn für a.x ein Deskriptor gefunden wird, wird er aufgerufen mit: desc.__get__(a, type(a)).
Die Logik für einen gepunkteten Lookup befindet sich in object.__getattribute__(). Hier ist eine reine Python-Entsprechung:
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 = object()
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)
Beachten Sie, dass es keinen __getattr__() Hook im Code von __getattribute__() gibt. Deshalb umgeht das direkte Aufrufen von __getattribute__() oder mit super().__getattribute__ __getattr__() vollständig.
Stattdessen sind der Punktoperator und die Funktion getattr() dafür verantwortlich, __getattr__() aufzurufen, wann immer __getattribute__() eine 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 von einer Klasse aus¶
Die Logik für einen gepunkteten Lookup wie A.x befindet sich in type.__getattribute__(). Die Schritte sind denen für object.__getattribute__() ähnlich, aber der Lookup im Instanzwörterbuch wird durch eine Suche durch die Methode Resolution Order der Klasse ersetzt.
Wenn ein Deskriptor gefunden wird, wird er aufgerufen mit desc.__get__(None, A).
Die vollständige C-Implementierung finden Sie unter type_getattro() und _PyType_Lookup() in Objects/typeobject.c.
Aufruf von super¶
Die Logik für den gepunkteten Lookup von super befindet sich in der Methode __getattribute__() für das von super() zurückgegebene Objekt.
Ein gepunkteter Lookup wie super(A, obj).m sucht in obj.__class__.__mro__ nach der Basisklasse B, die unmittelbar auf A folgt, und gibt dann B.__dict__['m'].__get__(obj, A) zurück. Wenn kein Deskriptor vorhanden ist, wird m unverändert zurückgegeben.
Die vollständige C-Implementierung finden Sie unter super_getattro() in Objects/typeobject.c. Eine reine Python-Entsprechung finden Sie in Guidos Tutorial.
Zusammenfassung der Aufruflogik¶
Der Mechanismus für Deskriptoren ist in den Methoden __getattribute__() für object, type und super() eingebettet.
Die wichtigen Punkte, die Sie beachten sollten, sind:
Deskriptoren werden von der Methode
__getattribute__()aufgerufen.Klassen erben diese Maschinerie von
object,typeodersuper().Das Überschreiben von
__getattribute__()verhindert automatische Deskriptoraufrufe, da die gesamte Deskriptorlogik in dieser Methode liegt.object.__getattribute__()undtype.__getattribute__()rufen__get__()unterschiedlich auf. Das erste schließt die Instanz ein und kann die Klasse einschließen. Das zweite setztNonefür die Instanz und schließt immer die Klasse ein.Daten-Deskriptoren überschreiben immer Instanzwörterbücher.
Nicht-Daten-Deskriptoren können von Instanzwörterbüchern überschrieben werden.
Automatische Namensbenachrichtigung¶
Manchmal ist es wünschenswert, dass ein Deskriptor weiß, welchem Klassennamen er zugewiesen wurde. Wenn eine neue Klasse erstellt wird, durchsucht die Metaklasse type das Wörterbuch der neuen Klasse. Wenn eine der Einträge Deskriptoren sind und sie __set_name__() definieren, 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 Implementierungsdetails finden Sie unter type_new() und set_names() in Objects/typeobject.c.
Da die Aktualisierungslogik in type.__new__() liegt, erfolgen Benachrichtigungen nur zum Zeitpunkt der Klassenerstellung. Wenn Deskriptoren nachträglich zur Klasse hinzugefügt werden, muss __set_name__() manuell aufgerufen werden.
ORM-Beispiel¶
Der folgende Code ist ein vereinfachtes Gerüst, das zeigt, wie Daten-Deskriptoren zur Implementierung eines Objektrelationen-Mappings (ORM) verwendet werden könnten.
Die wesentliche Idee ist, dass die Daten in einer externen Datenbank gespeichert werden. Die Python-Instanzen enthalten nur Schlüssel zu den Tabellen der Datenbank. Deskriptoren kümmern sich um Lookups oder Updates.
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()
Wir können die Klasse Field verwenden, um Modelle zu definieren, die das Schema für jede 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, verbinden Sie sich 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-Entsprechungen¶
Das Deskriptor-Protokoll ist einfach und bietet spannende Möglichkeiten. Mehrere Anwendungsfälle sind so üblich, dass sie in integrierte Werkzeuge vorverpackt wurden. Properties, gebundene Methoden, statische Methoden, Klassenmethoden und \_\_slots\_\_ basieren alle auf dem Deskriptor-Protokoll.
Properties¶
Das Aufrufen von property() ist eine knappe Möglichkeit, einen Daten-Deskriptor zu erstellen, der beim Zugriff auf ein Attribut eine Funktionsaufruf auslöst. Seine Signatur ist:
property(fget=None, fset=None, fdel=None, doc=None) -> property
Die Dokumentation zeigt eine typische Verwendung zur Definition eines verwalteten Attributs x.
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() im Hinblick auf das Deskriptor-Protokoll implementiert ist, hier ist eine reine Python-Entsprechung, die die meisten Kernfunktionen implementiert:
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 eingebaute Funktion property() hilft immer dann, wenn eine Benutzeroberfläche den Attributzugriff gewährt hat und nachfolgende Änderungen die Intervention einer Methode erfordern.
Zum Beispiel kann eine Tabellenkalkulationsklasse den Zugriff auf einen Zellenwert über Cell('b10').value gewähren. Nachfolgende Verbesserungen am Programm erfordern, dass die Zelle bei jedem Zugriff neu berechnet wird; Der Programmierer möchte jedoch keine bestehenden Client-Codes beeinträchtigen, die direkt auf das Attribut zugreifen. Die Lösung besteht darin, den Zugriff auf das Wertattribut in einem Property-Daten-Deskriptor zu verpacken.
class Cell:
...
@property
def value(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
Entweder die eingebaute property() oder unser Property()-Äquivalent würde in diesem Beispiel funktionieren.
Funktionen und Methoden¶
Pythons objektorientierte Merkmale basieren auf einer funktionsbasierten Umgebung. Durch die Verwendung von Nicht-Daten-Deskriptoren werden die beiden nahtlos zusammengeführt.
In Klassenwörterbüchern gespeicherte Funktionen werden bei der Ausführung in Methoden umgewandelt. Methoden unterscheiden sich von regulären Funktionen nur dadurch, dass die Objektinstanz den anderen Argumenten vorangestellt wird. Konventionsgemäß wird die Instanz self genannt, könnte aber auch this oder ein beliebiger anderer Variablenname sein.
Methoden können manuell mit types.MethodType erstellt werden, was in etwa dem Äquivalent 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 Erstellung von Methoden zu unterstützen, enthalten Funktionen die Methode __get__() zur Bindung von Methoden während des Attributzugriffs. Das bedeutet, dass Funktionen Nicht-Daten-Deskriptoren sind, die bei einer Punkt-Notation von 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)
Das Ausführen der folgenden Klasse im Interpreter zeigt, wie der Funktionsdeskriptor in der Praxis funktioniert:
class D:
def f(self):
return self
class D2:
pass
Die Funktion verfügt über ein Attribut qualifizierter Name, um die Introspektion zu unterstützen.
>>> D.f.__qualname__
'D.f'
Der Zugriff auf die Funktion über das Klassenwörterbuch ruft nicht __get__() auf. Stattdessen wird einfach das zugrunde liegende Funktionsobjekt zurückgegeben.
>>> D.__dict__['f']
<function D.f at 0x00C45070>
Der Punktzugriff von einer Klasse ruft __get__() auf, der einfach die zugrunde liegende Funktion unverändert zurückgibt.
>>> D.f
<function D.f at 0x00C45070>
Das interessante Verhalten tritt beim Punktzugriff von einer Instanz auf. Die Punkt-Notation ruft __get__() auf, das ein gebundenes Methodenobjekt zurückgibt.
>>> d = D()
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>
Intern speichert die gebundene Methode die zugrunde liegende Funktion und die gebundene Instanz.
>>> d.f.__func__
<function D.f at 0x00C45070>
>>> d.f.__self__
<__main__.D object at 0x00B18C90>
Wenn Sie sich jemals gefragt haben, woher self in regulären Methoden oder cls in Klassenmethoden kommt, dann ist es hier!
Arten von Methoden¶
Nicht-Daten-Deskriptoren bieten einen einfachen Mechanismus für Variationen der üblichen Muster der Bindung von Funktionen in Methoden.
Zusammenfassend lässt sich sagen, dass Funktionen eine Methode __get__() haben, damit sie bei Zugriff als Attribute in Methoden umgewandelt werden können. Der Nicht-Daten-Deskriptor wandelt einen Aufruf obj.f(*args) in f(obj, *args) um. Der Aufruf cls.f(*args) wird zu f(*args).
Diese Tabelle fasst die Bindung und ihre zwei nützlichsten Varianten zusammen:
Transformation
Aufgerufen von einem Objekt
Aufgerufen von einer Klasse
Funktion
f(obj, *args)
f(*args)
staticmethod
f(*args)
f(*args)
classmethod
f(type(obj), *args)
f(cls, *args)
Statische Methoden¶
Statische Methoden geben die zugrunde liegende Funktion unverändert zurück. Das Aufrufen von c.f oder C.f ist äquivalent zu einem direkten Zugriff auf object.__getattribute__(c, "f") oder object.__getattribute__(C, "f"). Dadurch ist die Funktion sowohl von einem Objekt als auch von einer Klasse identisch zugänglich.
Gute Kandidaten für statische Methoden sind Methoden, die nicht auf die Variable self verweisen.
Zum Beispiel kann ein Statistikpaket eine Container-Klasse für experimentelle Daten enthalten. Die Klasse bietet normale Methoden zur Berechnung des Durchschnitts, Mittelwerts, Medians und anderer beschreibender Statistiken, die von den Daten abhängen. Es kann jedoch nützliche Funktionen geben, die konzeptionell verwandt sind, aber nicht von einem bestimmten Datensatz abhängen. Zum Beispiel ist erf(x) eine nützliche Konvertierungsroutine, die in der statistischen Arbeit vorkommt, aber nicht direkt von einem bestimmten Datensatz abhängt. Sie kann entweder von einem Objekt oder der Klasse aufgerufen werden: s.erf(1.5) --> 0.9332 oder Sample.erf(1.5) --> 0.9332.
Da statische Methoden die zugrunde liegende Funktion ohne Änderungen zurückgeben, sind die Beispielaufrufe nicht aufregend:
class E:
@staticmethod
def f(x):
return x * 10
>>> E.f(3)
30
>>> E().f(3)
30
Unter Verwendung des Nicht-Daten-Deskriptor-Protokolls würde eine reine Python-Version von staticmethod() wie folgt 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() fügt ein Attribut __wrapped__ hinzu, das auf die zugrunde liegende Funktion verweist. Außerdem werden die notwendigen Attribute weitergegeben, damit der Wrapper wie die umwickelte Funktion aussieht, einschließlich __name__, __qualname__ und __doc__.
Klassenmethoden¶
Im Gegensatz zu statischen Methoden stellen Klassenmethoden die Klassenreferenz der Argumentliste voran, bevor die Funktion aufgerufen wird. Dieses Format ist dasselbe, unabhängig davon, 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 nützlich, wenn die Methode nur eine Klassenreferenz benötigt und nicht auf in einer bestimmten Instanz gespeicherte Daten angewiesen ist. Eine Verwendung für Klassenmethoden ist die Erstellung alternativer Klassenkonstruktoren. Zum Beispiel erstellt die Klassenmethode dict.fromkeys() ein neues Wörterbuch aus einer Liste von Schlüsseln. Das reine Python-Äquivalent 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 kann ein neues Wörterbuch mit eindeutigen Schlüsseln wie folgt erstellt werden:
>>> d = Dict.fromkeys('abracadabra')
>>> type(d) is Dict
True
>>> d
{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}
Unter Verwendung des Nicht-Daten-Deskriptor-Protokolls würde eine reine Python-Version von classmethod() wie folgt 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 fügt ein Attribut __wrapped__ hinzu, das auf die zugrunde liegende Funktion verweist. Außerdem werden die notwendigen Attribute weitergegeben, damit der Wrapper wie die umwickelte Funktion aussieht: __name__, __qualname__, __doc__ und __annotations__.
Mitgliedsobjekte und __slots__¶
Wenn eine Klasse __slots__ definiert, ersetzt sie Instanzwörterbücher durch ein Feld fester Länge von Slot-Werten. Aus Benutzersicht hat das mehrere Auswirkungen:
1. Ermöglicht die sofortige Erkennung von Fehlern aufgrund von falsch geschriebenen Attributzuweisungen. Nur Attribute, die in __slots__ angegeben sind, sind zulässig.
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. Hilft bei der Erstellung unveränderlicher Objekte, bei denen Deskriptoren den Zugriff auf private Attribute verwalten, die in __slots__ gespeichert sind.
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. Spart Speicher. Auf einem 64-Bit-Linux-Build benötigt eine Instanz mit zwei Attributen mit __slots__ 48 Bytes und ohne 152 Bytes. Dieses Flyweight-Entwurfsmuster ist wahrscheinlich nur dann wichtig, wenn eine große Anzahl von Instanzen erstellt werden soll.
4. Verbessert die Geschwindigkeit. Das Lesen von Instanzvariablen ist mit __slots__ 35 % schneller (gemessen mit Python 3.10 auf einem Apple M1-Prozessor).
5. Blockiert Werkzeuge wie functools.cached_property(), die ein Instanzwörterbuch benötigen, um korrekt zu funktionieren.
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 exakte Drop-in-Reine-Python-Version von __slots__ zu erstellen, da sie direkten Zugriff auf C-Strukturen und Kontrolle über die Speicherzuweisung von Objekten erfordert. Wir können jedoch eine weitgehend getreue Simulation aufbauen, bei der die tatsächliche C-Struktur für Slots durch eine private Liste _slotvalues emuliert wird. Lese- und Schreibzugriffe auf diese private Struktur werden von Mitgliedsdeskriptoren verwaltet.
null = object()
class Member:
def __init__(self, name, clsname, offset):
'Emulate PyMemberDef in Include/structmember.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 um das Hinzufügen von Mitgliedsobjekten zu Klassenvariablen.
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 um die Erstellung von Instanzen, die Slots anstelle eines Instanzwörterbuchs haben. Hier ist eine grobe Simulation 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 Simulation in einer echten Klasse zu verwenden, erben Sie einfach von Object und setzen Sie 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
Zu diesem Zeitpunkt hat die Metaklasse Mitgliedsobjekte 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'>}
Wenn Instanzen erstellt werden, verfügen sie über eine Liste slot_values, in der die Attribute gespeichert werden.
>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}
Falsch geschriebene oder nicht zugewiesene Attribute führen zu einer Ausnahme.
>>> h.xz
Traceback (most recent call last):
...
AttributeError: 'H' object has no attribute 'xz'