web-dev-qa-db-fra.com

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

Je veux 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
83
Greg

Vous obtenez une erreur de récursivité, car votre tentative d'accès à self.__dict__ attribut à l'intérieur __getattribute__ invoque votre __getattribute__ encore. Si vous utilisez object's __getattribute__ à la 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 parce que 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 du 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

Mise à 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écursion infinie.

111
Egil

En fait, je pense que vous souhaitez plutôt utiliser la méthode spéciale __getattr__.

Citation des documents 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 se trouve dans l'arborescence des classes pour lui-même). 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, __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 qu'autrement __setattr__() n'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 plutôt en les insérant dans un autre objet). Voir la méthode __getattribute__() ci-dessous pour un moyen d'obtenir un contrôle total dans les classes de nouveau style.

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

23
tzot

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 à tous les attributs dont elle a besoin, par exemple object.__getattribute__(self, name).

Sens:

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

Vous appelez un attribut appelé __dict__. Parce que c'est un attribut, __getattribute__ est appelé à la recherche de __dict__ qui appelle __getattribute__ qui appelle ... yada yada yada

return  object.__getattribute__(self, name)

Utilisation des classes de base __getattribute__ aide à trouver le véritable attribut.

16
ttepasse

Voulez-vous vraiment 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

Edit: Notez qu'une instance de D aurait des valeurs différentes de test dans chaque cas. Dans le premier cas d.test serait 20, dans le second ce serait 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 le __init__ La méthode a essayé de la mettre à 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)

Évidemment, en tant que classe, cela est presque entièrement inutile, mais cela vous donne une idée pour passer à autre chose.

13
Singletoned

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 parent, pour finalement retomber sur l'objet .__ getattribute __ méthode si d'autres ancêtres ne la remplacent pas.

5
ElmoVanKielmo

Comment est la __getattribute__ méthode utilisée?

Il est appelé avant la recherche normale en pointillés. S'il soulève AttributeError, alors nous appelons __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

Meilleures pratiques

La bonne façon de contrôler par programme l'accès à un seul attribut est avec property . La classe D doit être écrite comme suit (avec le setter et le deleter facultativement 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 de chaque attribut via __getattribute__ .

  • augmenter AttributeError, provoquant __getattr__ à appeler (si implémenté)
  • en retourner quelque chose d'ici le
    • en utilisant super pour appeler l'implémentation parent (probablement object)
    • appelant __getattr__
    • mettre en œuvre votre propre algorithme de recherche en pointillé en quelque sorte

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

Examen du code

Votre code avec commentaires. Vous avez une recherche en pointillé sur vous-même dans __getattribute__. C'est pourquoi vous obtenez une erreur de récursivité. Vous pouvez vérifier si le nom est "__dict__" et utilisez super pour contourner ce problème, mais cela ne couvre pas __slots__. Je laisse 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
3
Aaron Hall