Wie implementiere ich __getattribute__ ohne einen unendlichen Rekursionsfehler?


100

Ich möchte den Zugriff auf eine Variable in einer Klasse überschreiben, aber alle anderen normal zurückgeben. Wie schaffe ich das mit __getattribute__?

Ich habe Folgendes versucht (was auch veranschaulichen sollte, was ich versuche), aber ich erhalte einen Rekursionsfehler:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return self.__dict__[name]

>>> print D().test
0.0
>>> print D().test2
...
RuntimeError: maximum recursion depth exceeded in cmp

Antworten:


126

Sie erhalten einen Rekursionsfehler, weil Ihr Versuch, auf das darin enthaltene self.__dict__Attribut zuzugreifen, __getattribute__Ihre __getattribute__erneut aufruft . Wenn Sie stattdessen object's __getattribute__verwenden, funktioniert es:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return object.__getattribute__(self, name)

Dies funktioniert, weil object(in diesem Beispiel) die Basisklasse ist. Wenn Sie die Basisversion von __getattribute__Ihnen aufrufen , vermeiden Sie die rekursive Hölle, in der Sie sich zuvor befanden.

Ipython-Ausgabe mit Code in foo.py:

In [1]: from foo import *

In [2]: d = D()

In [3]: d.test
Out[3]: 0.0

In [4]: d.test2
Out[4]: 21

Aktualisieren:

In der aktuellen Dokumentation finden Sie im Abschnitt Mehr Attributzugriff für Klassen neuen Stils etwas, in dem genau dies empfohlen wird, um die unendliche Rekursion zu vermeiden.


1
Interessant. Also, was machst du da? Warum sollte Objekt meine Variablen haben?
Greg

1
..und möchte ich immer Objekt verwenden? Was ist, wenn ich von anderen Klassen erbe?
Greg

1
Ja, jedes Mal, wenn Sie eine Klasse erstellen und keine eigene schreiben, verwenden Sie das vom Objekt bereitgestellte getattribute .
Egil

19
Ist es nicht besser, super () zu verwenden und damit die erste getattribute- Methode zu verwenden, die Python in Ihren Basisklassen findet? -super(D, self).__getattribute__(name)
Gepatino

10
Oder Sie können einfach super().__getattribute__(name)in Python 3 verwenden.
Jeromej

25

Eigentlich glaube ich, dass Sie __getattr__stattdessen die spezielle Methode verwenden möchten .

Zitat aus den Python-Dokumenten:

__getattr__( self, name)

Wird aufgerufen, wenn eine Attributsuche das Attribut nicht an den üblichen Stellen gefunden hat (dh es ist weder ein Instanzattribut noch wird es im Klassenbaum für self gefunden). Name ist der Attributname. Diese Methode sollte den (berechneten) Attributwert zurückgeben oder eine AttributeError-Ausnahme auslösen.
Beachten Sie, dass das Attribut __getattr__()nicht aufgerufen wird , wenn es über den normalen Mechanismus gefunden wird. (Dies ist eine absichtliche Asymmetrie zwischen __getattr__()und __setattr__().) Dies geschieht sowohl aus Effizienzgründen als auch weil sonst __setattr__()keine Möglichkeit besteht, auf andere Attribute der Instanz zuzugreifen. Beachten Sie, dass Sie zumindest für Instanzvariablen die vollständige Kontrolle vortäuschen können, indem Sie keine Werte in das Instanzattributwörterbuch einfügen (sondern sie in ein anderes Objekt einfügen). Siehe die__getattribute__() Methode unten, um eine vollständige Kontrolle über Klassen neuen Stils zu erhalten.

Hinweis: Damit dies funktioniert, sollte die Instanz nicht ein haben testAttribut, so dass die Zeile self.test=20sollte entfernt werden.


2
Entsprechend der Art des OP-Codes wäre das Überschreiben __getattr__für testnutzlos, da es immer "an den üblichen Stellen" gefunden würde.
Casey Kuball

1
Aktueller Link zu relevanten Python-Dokumenten (scheint sich von dem in der Antwort angegebenen zu unterscheiden): docs.python.org/3/reference/datamodel.html#object.__getattr__
CrepeGoat

17

Python-Sprachreferenz:

Um eine unendliche Rekursion in dieser Methode zu vermeiden, sollte ihre Implementierung immer die Basisklassenmethode mit demselben Namen aufrufen, um beispielsweise auf alle benötigten Attribute zuzugreifen object.__getattribute__(self, name).

Bedeutung:

def __getattribute__(self,name):
    ...
        return self.__dict__[name]

Sie fordern ein Attribut namens __dict__. Weil es ein Attribut ist, __getattribute__wird es auf der Suche nach __dict__den Anrufen __getattribute__aufgerufen, die anrufen ... yada yada yada

return  object.__getattribute__(self, name)

Die Verwendung der Basisklassen __getattribute__hilft beim Auffinden des realen Attributs.


13

Sind Sie sicher, dass Sie verwenden möchten __getattribute__? Was versuchst du eigentlich zu erreichen?

Der einfachste Weg, das zu tun, was Sie verlangen, ist:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    test = 0

oder:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    @property
    def test(self):
        return 0

Bearbeiten: Beachten Sie, dass eine Instanz von Djeweils unterschiedliche Werte von haben würde test. Im ersten Fall d.testwäre es 20, im zweiten wäre es 0. Ich überlasse es Ihnen, herauszufinden, warum.

Edit2: Greg wies darauf hin, dass Beispiel 2 fehlschlagen wird, da die Eigenschaft schreibgeschützt ist und die __init__Methode versucht hat, sie auf 20 zu setzen. Ein vollständigeres Beispiel dafür wäre:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    _test = 0

    def get_test(self):
        return self._test

    def set_test(self, value):
        self._test = value

    test = property(get_test, set_test)

Natürlich ist dies als Klasse fast völlig nutzlos, aber es gibt Ihnen eine Idee, von der Sie weitermachen können.


Oh, das funktioniert nicht leise, wenn du die Klasse leitest, nein? Datei "Script1.py", Zeile 5, in init self.test = 20 AttributeError: Attribut kann nicht festgelegt werden
Greg

Wahr. Ich werde das als drittes Beispiel beheben. Gut erkannt.
Singletoned

5

Hier ist eine zuverlässigere Version:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21
    def __getattribute__(self, name):
        if name == 'test':
            return 0.
        else:
            return super(D, self).__getattribute__(name)

Es ruft die Methode __ getattribute __ von der übergeordneten Klasse auf und greift schließlich auf das Objekt zurück. __ getattribute __ Methode, wenn andere Vorfahren sie nicht überschreiben.


5

Wie wird die __getattribute__Methode angewendet?

Es wird vor der normalen gepunkteten Suche aufgerufen. Wenn es erhöht AttributeError, rufen wir an __getattr__.

Die Anwendung dieser Methode ist eher selten. In der Standardbibliothek gibt es nur zwei Definitionen:

$ grep -Erl  "def __getattribute__\(self" cpython/Lib | grep -v "/test/"
cpython/Lib/_threading_local.py
cpython/Lib/importlib/util.py

Beste Übung

Der richtige Weg, um den Zugriff auf ein einzelnes Attribut programmgesteuert zu steuern, ist mit property. Die Klasse Dsollte wie folgt geschrieben werden (mit dem Setter und Deleter optional, um das offensichtliche beabsichtigte Verhalten zu replizieren):

class D(object):
    def __init__(self):
        self.test2=21

    @property
    def test(self):
        return 0.

    @test.setter
    def test(self, value):
        '''dummy function to avoid AttributeError on setting property'''

    @test.deleter
    def test(self):
        '''dummy function to avoid AttributeError on deleting property'''

Und Verwendung:

>>> o = D()
>>> o.test
0.0
>>> o.test = 'foo'
>>> o.test
0.0
>>> del o.test
>>> o.test
0.0

Eine Eigenschaft ist ein Datendeskriptor, daher ist sie das erste, wonach im normalen Algorithmus für die gepunktete Suche gesucht wird.

Optionen für __getattribute__

Sie haben mehrere Möglichkeiten, wenn Sie unbedingt die Suche für jedes Attribut über implementieren müssen __getattribute__.

  • erhöhen AttributeError, wodurch __getattr__aufgerufen wird (falls implementiert)
  • gib etwas davon zurück von
    • Verwenden Sie super, um die übergeordnete objectImplementierung (wahrscheinlich ) aufzurufen
    • Berufung __getattr__
    • Implementieren Sie irgendwie Ihren eigenen gepunkteten Suchalgorithmus

Beispielsweise:

class NoisyAttributes(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self, name):
        print('getting: ' + name)
        try:
            return super(NoisyAttributes, self).__getattribute__(name)
        except AttributeError:
            print('oh no, AttributeError caught and reraising')
            raise
    def __getattr__(self, name):
        """Called if __getattribute__ raises AttributeError"""
        return 'close but no ' + name    


>>> n = NoisyAttributes()
>>> nfoo = n.foo
getting: foo
oh no, AttributeError caught and reraising
>>> nfoo
'close but no foo'
>>> n.test
getting: test
20

Was du ursprünglich wolltest.

Und dieses Beispiel zeigt, wie Sie das tun können, was Sie ursprünglich wollten:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return super(D, self).__getattribute__(name)

Und wird sich so verhalten:

>>> o = D()
>>> o.test = 'foo'
>>> o.test
0.0
>>> del o.test
>>> o.test
0.0
>>> del o.test

Traceback (most recent call last):
  File "<pyshell#216>", line 1, in <module>
    del o.test
AttributeError: test

Code-Review

Ihr Code mit Kommentaren. Sie haben eine gepunktete Suche nach sich selbst in __getattribute__. Aus diesem Grund wird ein Rekursionsfehler angezeigt. Sie können überprüfen, ob der Name lautet, "__dict__"und ihn superzur Problemumgehung verwenden, dies gilt jedoch nicht __slots__. Das überlasse ich dem Leser als Übung.

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:      #   v--- Dotted lookup on self in __getattribute__
            return self.__dict__[name]

>>> print D().test
0.0
>>> print D().test2
...
RuntimeError: maximum recursion depth exceeded in cmp
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.