Betrachten Sie den folgenden Code:
class Foo: def __init__(self): self.bar = 'hello!' foo = Foo() print(foo.bar)
Heute werden wir die Antwort auf die Frage analysieren: "Was genau passiert, wenn wir foo.bar schreiben?"
Möglicherweise wissen Sie bereits, dass die meisten Objekte über ein internes Wörterbuch __dict__ verfügen, das alle ihre Attribute enthält. Besonders erfreulich ist, wie einfach es ist, so einfache Details in Python zu studieren:
>>> foo = Foo() >>> foo.__dict__ {'bar': 'hello!'}
Beginnen wir mit dem Versuch, diese (unvollständige) Hypothese zu formulieren:
foo.bar ist äquivalent zu foo .__ dict __ ['bar'] .
Während es wie die Wahrheit klingt:
>>> foo = Foo() >>> foo.__dict__['bar'] 'hello!'
Angenommen, Sie wissen bereits, dass dynamische Attribute in Klassen deklariert werden können:
>>> class Foo: ... def __init__(self): ... self.bar = 'hello!' ... ... def __getattr__(self, item): ... return 'goodbye!' ... ... foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.__dict__ {'bar': 'hello!'}
Hmm ... okay. Es ist zu sehen, dass __getattr__ den Zugriff auf "gefälschte" Attribute emulieren kann, jedoch nicht funktioniert, wenn bereits eine deklarierte Variable vorhanden ist (z. B. foo.bar , die "Hallo!" Und nicht "Auf Wiedersehen!" Zurückgibt ). Alles scheint etwas komplizierter zu sein, als es am Anfang schien.
Und tatsächlich: Es gibt eine magische Methode, die aufgerufen wird, wenn wir versuchen, ein Attribut abzurufen , aber wie das obige Beispiel zeigt, ist dies nicht __getattr__ . Die aufgerufene Methode heißt __getattribute__ und wir werden versuchen zu verstehen, wie es genau funktioniert, indem wir verschiedene Situationen beobachten.
Bisher ändern wir unsere Hypothese wie folgt:
foo.bar ist äquivalent zu foo .__ getattribute __ ('bar') , was ungefähr so funktioniert:
def __getattribute__(self, item): if item in self.__dict__: return self.__dict__[item] return self.__getattr__(item)
Wir testen es, indem wir diese Methode (unter einem anderen Namen) implementieren und direkt aufrufen:
>>> class Foo: ... def __init__(self): ... self.bar = 'hello!' ... ... def __getattr__(self, item): ... return 'goodbye!' ... ... def my_getattribute(self, item): ... if item in self.__dict__: ... return self.__dict__[item] ... return self.__getattr__(item) >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.my_getattribute('bar') 'hello!' >>> foo.my_getattribute('baz') 'goodbye!'
Sieht richtig aus, richtig?
Nun, alles, was bleibt, ist zu überprüfen, ob die Zuweisung von Variablen unterstützt wird. Danach können Sie nach Hause gehen ... -
>>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.my_getattribute('baz') = 'h4x0r' SyntaxError: can't assign to function call
Hölle
my_getattribute gibt ein Objekt zurück. Wir können es ändern, wenn es veränderlich ist, aber wir können es nicht mit dem Zuweisungsoperator durch ein anderes ersetzen. Was zu tun Wenn foo.baz dem Aufruf einer Funktion entspricht, wie können wir dann einem Attribut im Prinzip einen neuen Wert zuweisen?
Wenn wir uns einen Ausdruck wie foo.bar = 1 ansehen, steckt mehr dahinter, als nur eine Funktion aufzurufen, um den Wert von foo.bar zu erhalten . Das Zuweisen eines Werts zu einem Attribut scheint sich grundlegend vom Abrufen eines Attributwerts zu unterscheiden . Und die Wahrheit: Wir können __setattr__ implementieren, um Folgendes zu sehen:
>>> class Foo: ... def __init__(self): ... self.__dict__['my_dunder_dict'] = {} ... self.bar = 'hello!' ... ... def __setattr__(self, item, value): ... self.my_dunder_dict[item] = value ... ... def __getattr__(self, item): ... return self.my_dunder_dict[item] >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.bar = 'goodbye!' >>> foo.bar 'goodbye!' >>> foo.baz Traceback (most recent call last): File "<pyshell#75>", line 1, in <module> foo.baz File "<pyshell#70>", line 10, in __getattr__ return self.my_dunder_dict[item] KeyError: 'baz' >>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.__dict__ {'my_dunder_dict': {'bar': 'goodbye!', 'baz': 1337}}
Ein paar Dinge, die in Bezug auf diesen Code beachtet werden müssen:
- __setattr__ hat kein Gegenstück zu __getattribute__ (d. h. die magische Methode __setattribute__ existiert nicht).
- __setattr__ heißt in __init__ , weshalb wir gezwungen sind, uns selbst zu tun .__ dict __ ['my_dunder_dict'] = {} anstelle von self.my_dunder_dict = {} . Andernfalls würden wir auf eine unendliche Rekursion stoßen.
Wir haben aber auch Eigentum (und seine Freunde). Ein Dekorator, mit dem Methoden als Attribute fungieren können.
Versuchen wir zu verstehen, wie das passiert.
>>> class Foo(object): ... def __getattribute__(self, item): ... print('__getattribute__ was called') ... return super().__getattribute__(item) ... ... def __getattr__(self, item): ... print('__getattr__ was called') ... return super().__getattr__(item) ... ... @property ... def bar(self): ... print('bar property was called') ... return 100 >>> f = Foo() >>> f.bar __getattribute__ was called bar property was called
Was haben wir nur zum Spaß in f .__ dict__ ?
>>> f.__dict__ __getattribute__ was called {}
In __dict__ gibt es keinen Strichcode , aber __getattr__ wird aus irgendeinem Grund nicht aufgerufen. Wat?
bar ist eine Methode, die auch self als Parameter verwendet. Nur diese Methode befindet sich in der Klasse und nicht in der Klasseninstanz. Und das ist leicht zu sehen:
>>> Foo.__dict__ mappingproxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, '__doc__': None, '__getattr__': <function Foo.__getattr__ at 0x038308A0>, '__getattribute__': <function Foo.__getattribute__ at 0x038308E8>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Foo' objects>, 'bar': <property object at 0x0381EC30>})
Der Balkenschlüssel befindet sich in der Tat im Klassenattributverzeichnis. Um zu verstehen, wie __getattribute__ funktioniert , müssen wir die Frage beantworten: Wessen __getattribute__ wird zuvor aufgerufen - eine Klasse oder eine Instanz?
>>> f.__dict__['bar'] = 'will we see this printed?' __getattribute__ was called >>> f.bar __getattribute__ was called bar property was called 100
Es ist zu sehen, dass die Überprüfung als erstes in der Klasse __dict__ erfolgt , d.h. es hat Vorrang vor der Instanz.
Warten Sie eine Minute, wann haben wir die Balkenmethode aufgerufen? Ich meine, unser Pseudocode für __getattribute__ ruft niemals ein Objekt auf. Was ist los?
Treffen Sie das Deskriptor-Protokoll :
beschr .__ get __ (self, obj, type = None) -> value
beschr .__ set __ (self, obj, value) -> None
beschr .__ lösche __ (self, obj) -> Keine
Der springende Punkt ist hier. Implementieren Sie eine dieser drei Methoden, damit das Objekt zu einem Deskriptor wird und das Standardverhalten ändern kann, wenn es als Attribut behandelt wird.
Wenn ein Objekt sowohl __get __ () als auch __set __ () deklariert, wird es als Datendeskriptor bezeichnet . Deskriptoren, die nur __get __ () implementieren, werden als Nicht-Daten-Deskriptoren bezeichnet.
Beide Arten von Deskriptoren unterscheiden sich darin, wie die Elemente des Objektattributverzeichnisses überschrieben werden. Wenn das Wörterbuch einen Schlüssel mit demselben Namen wie der Datendeskriptor enthält , hat der Datendeskriptor Vorrang (d. H., __Set __ () wird aufgerufen). Wenn das Wörterbuch einen Schlüssel mit demselben Namen wie der Deskriptor ohne Daten enthält, hat das Wörterbuch Priorität (d. H. Das Wörterbuchelement wird überschrieben).
Um einen Nur-Lese-Deskriptor für Daten zu erstellen, deklarieren Sie sowohl __get __ () als auch __set __ () , wobei __set __ () beim Aufrufen einen AttributeError auslöst . Das Implementieren dieses __set __ () reicht aus, um einen Datendeskriptor zu erstellen.
Kurz gesagt, wenn Sie eine dieser Methoden __get__ , __set__ oder __delete__ deklariert haben , haben Sie die Unterstützung für das Deskriptorprotokoll implementiert. Und genau das macht der Property Decorator: Er deklariert einen schreibgeschützten Deskriptor, der in __getattribute__ aufgerufen wird .
Letzte Änderung in unserer Implementierung:
foo.bar ist äquivalent zu foo .__ getattribute __ ('bar') , was ungefähr so funktioniert:
def __getattribute__(self, item): if item in self.__class__.__dict__: v = self.__class__.__dict__[item] elif item in self.__dict__: v = self.__dict__[item] else: v = self.__getattr__(item) if hasattr(v, '__get__'): v = v.__get__(self, type(self)) return v
Versuchen wir in der Praxis zu demonstrieren:
class Foo: class_attr = "I'm a class attribute!" def __init__(self): self.dict_attr = "I'm in a dict!" @property def property_attr(self): return "I'm a read-only property!" def __getattr__(self, item): return "I'm dynamically returned!" def my_getattribute(self, item): if item in self.__class__.__dict__: print('Retrieving from self.__class__.__dict__') v = self.__class__.__dict__[item] elif item in self.__dict__: print('Retrieving from self.__dict__') v = self.__dict__[item] else: print('Retrieving from self.__getattr__') v = self.__getattr__(item) if hasattr(v, '__get__'): print("Invoking descriptor's __get__") v = v.__get__(self, type(self)) return v
>>> foo = Foo() ... ... print(foo.class_attr) ... print(foo.dict_attr) ... print(foo.property_attr) ... print(foo.dynamic_attr) ... ... print() ... ... print(foo.my_getattribute('class_attr')) ... print(foo.my_getattribute('dict_attr')) ... print(foo.my_getattribute('property_attr')) ... print(foo.my_getattribute('dynamic_attr')) I'm a class attribute! I'm in a dict! I'm a read-only property! I'm dynamically returned! Retrieving from self.__class__.__dict__ I'm a class attribute! Retrieving from self.__dict__ I'm in a dict! Retrieving from self.__class__.__dict__ Invoking descriptor's __get__ I'm a read-only property! Retrieving from self.__getattr__ I'm dynamically returned!
Wir haben gerade die Oberfläche der Attributimplementierung in Python ein wenig gekratzt. Obwohl unser letzter Versuch, foo.bar zu emulieren, im Allgemeinen korrekt ist, sollten Sie bedenken, dass es immer kleine Details geben kann, die unterschiedlich implementiert sind.
Ich hoffe, dass ich nicht nur weiß, wie Attribute funktionieren, sondern auch die Schönheit der Sprache vermittelt habe, die zum Experimentieren anregt. Zahlen Sie heute einen Teil Ihrer Wissensschuld ab .