Comment implémenter __getattribute__ sans erreur de récursivité infinie?

101

Je souhaite remplacer l'accès à une variable dans une classe, mais renvoyer toutes les autres normalement. Comment puis-je accomplir cela avec __getattribute__?

J'ai essayé ce qui suit (qui devrait également illustrer ce que j'essaie de faire) mais j'obtiens une erreur de récursivité:

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
Greg
la source

Réponses:

127

Vous obtenez une erreur de récursivité car votre tentative d'accéder à l' self.__dict__attribut à l'intérieur __getattribute__vous appelle à __getattribute__nouveau. Si vous utilisez à objectla __getattribute__place, cela fonctionne:

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)

Cela fonctionne car object(dans cet exemple) est la classe de base. En appelant la version de base de __getattribute__vous évitez l'enfer récursif dans lequel vous étiez auparavant.

Sortie Ipython avec code dans 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

Mettre à jour:

Il y a quelque chose dans la section intitulée Plus d'accès aux attributs pour les classes de nouveau style dans la documentation actuelle, où ils recommandent de faire exactement cela pour éviter la récursivité infinie.

Egil
la source
1
Intéressant. Alors qu'est-ce que tu fais là? Pourquoi object aurait-il mes variables?
Greg
1
..et est-ce que je veux toujours utiliser un objet? Et si j'hérite d'autres classes?
Greg
1
Oui, chaque fois que vous créez une classe et que vous n'écrivez pas la vôtre, vous utilisez le getattribute fourni par object.
Egil
20
n'est-il pas préférable d'utiliser super () et donc d'utiliser la première méthode getattribute que python trouve dans vos classes de base? -super(D, self).__getattribute__(name)
gepatino
10
Ou vous pouvez simplement utiliser super().__getattribute__(name)en Python 3.
jeromej
25

En fait, je pense que vous souhaitez utiliser la __getattr__méthode spéciale à la place.

Citation de la documentation Python:

__getattr__( self, name)

Appelé lorsqu'une recherche d'attribut n'a pas trouvé l'attribut aux endroits habituels (c'est-à-dire qu'il ne s'agit pas d'un attribut d'instance ni qu'il ne se trouve dans l'arborescence de classes pour soi). nom est le nom de l'attribut. Cette méthode doit renvoyer la valeur d'attribut (calculée) ou déclencher une exception AttributeError.
Notez que si l'attribut est trouvé via le mécanisme normal, il __getattr__()n'est pas appelé. (Il s'agit d'une asymétrie intentionnelle entre __getattr__()et __setattr__().) Ceci est fait à la fois pour des raisons d'efficacité et parce que sinon, il __setattr__()n'y aurait aucun moyen d'accéder à d'autres attributs de l'instance. Notez qu'au moins pour les variables d'instance, vous pouvez simuler un contrôle total en n'insérant aucune valeur dans le dictionnaire d'attributs d'instance (mais en les insérant à la place dans un autre objet). Voir le__getattribute__() ci-dessous pour un moyen d'obtenir un contrôle total dans les classes de style nouveau.

Remarque: pour que cela fonctionne, l'instance ne doit pas avoir d' testattribut, la ligne self.test=20doit donc être supprimée.

tzot
la source
2
En fait, selon la nature du code de l'OP, le dépassement de __getattr__for testserait inutile, car il le trouverait toujours «aux endroits habituels».
Casey Kuball
1
lien actuel vers les documents Python pertinents (semble être différent de celui référencé dans la réponse): docs.python.org/3/reference/datamodel.html#object.__getattr__
CrepeGoat
17

Référence du langage Python:

Afin d'éviter une récursion infinie dans cette méthode, son implémentation doit toujours appeler la méthode de classe de base avec le même nom pour accéder aux attributs dont elle a besoin, par exemple object.__getattribute__(self, name),.

Sens:

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

Vous appelez pour un attribut appelé __dict__. Parce que c'est un attribut, __getattribute__est appelé à la recherche de __dict__quels appels __getattribute__qui appelle ... yada yada yada

return  object.__getattribute__(self, name)

L'utilisation des classes de base __getattribute__aide à trouver l'attribut réel.

ttepasse
la source
13

Êtes-vous sûr de vouloir l'utiliser __getattribute__? Qu'essayez-vous réellement d'accomplir?

La façon la plus simple de faire ce que vous demandez est:

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

    test = 0

ou:

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

    @property
    def test(self):
        return 0

Modifier: Notez qu'une instance de Daurait des valeurs différentes de testdans chaque cas. Dans le premier cas d.test, 20, dans le second, 0. Je vous laisse le soin de comprendre pourquoi.

Edit2: Greg a souligné que l'exemple 2 échouera car la propriété est en lecture seule et la __init__méthode a essayé de la définir sur 20. Un exemple plus complet serait:

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)

De toute évidence, en tant que classe, cela est presque entièrement inutile, mais cela vous donne une idée de départ.

Singleton
la source
Oh, ça ne marche pas quand tu diriges la classe, non? Fichier "Script1.py", ligne 5, dans init self.test = 20 AttributeError: impossible de définir l'attribut
Greg
Vrai. Je vais corriger cela comme un troisième exemple. Bien repéré.
Singletoned
5

Voici une version plus fiable:

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)

Il appelle la méthode __ getattribute __ de la classe parente, pour finalement revenir à l'objet. __ méthode getattribute __ si les autres ancêtres ne la remplacent pas.

ElmoVanKielmo
la source
5

Comment la __getattribute__méthode est-elle utilisée?

Il est appelé avant la recherche pointillée normale. Si cela augmente AttributeError, alors nous suivons __getattr__.

L'utilisation de cette méthode est plutôt rare. Il n'y a que deux définitions dans la bibliothèque standard:

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

Meilleur entrainement

La méthode appropriée pour contrôler par programme l'accès à un seul attribut est d'utiliser property. La classe Ddoit être écrite comme suit (avec le setter et le deleter en option pour reproduire le comportement prévu apparent):

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'''

Et l'utilisation:

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

Une propriété est un descripteur de données, c'est donc la première chose recherchée dans l'algorithme de recherche en pointillé normal.

Options pour __getattribute__

Vous avez plusieurs options si vous devez absolument implémenter la recherche pour chaque attribut via __getattribute__.

  • lever AttributeError, provoquant __getattr__l'appel (si implémenté)
  • en renvoyer quelque chose en
    • en utilisant superpour appeler le parent (probablementobject implémentation )
    • appel __getattr__
    • implémenter votre propre algorithme de recherche en pointillé d'une manière ou d'une autre

Par exemple:

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

Ce que vous vouliez à l'origine.

Et cet exemple montre comment vous pourriez faire ce que vous vouliez à l'origine:

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)

Et se comportera comme ceci:

>>> 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

Revue de code

Votre code avec des commentaires. Vous avez une recherche en pointillé sur vous-même __getattribute__. C'est pourquoi vous obtenez une erreur de récursivité. Vous pouvez vérifier si le nom est "__dict__"et utiliser superpour contourner le problème, mais cela ne couvre pas __slots__. Je vais laisser cela comme un exercice au lecteur.

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
Salle Aaron
la source