¿Cómo implemento __getattribute__ sin un error de recursividad infinita?

100

Quiero anular el acceso a una variable en una clase, pero devolver todas las demás normalmente. ¿Cómo logro esto con __getattribute__?

Intenté lo siguiente (que también debería ilustrar lo que estoy tratando de hacer) pero obtengo un error de recursividad:

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
fuente

Respuestas:

126

Obtiene un error de recursividad porque su intento de acceder al self.__dict__atributo interno __getattribute__invoca su __getattribute__nuevo. Si usa object's __getattribute__en su lugar, funciona:

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)

Esto funciona porque object(en este ejemplo) es la clase base. Al llamar a la versión base de __getattribute__usted, evita el infierno recursivo en el que estaba antes.

Salida de Ipython con código en 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

Actualizar:

Hay algo en la sección titulada Más acceso a atributos para clases de estilo nuevo en la documentación actual, donde recomiendan hacer exactamente esto para evitar la recursividad infinita.

Egil
fuente
1
Interesante. ¿Entonces qué estás haciendo allí? ¿Por qué el objeto tendría mis variables?
Greg
1
..y quiero usar siempre object? ¿Qué pasa si heredo de otras clases?
Greg
1
Sí, cada vez que crea una clase y no escribe la suya propia, usa el atributo getattribute proporcionado por el objeto.
Egil
19
¿No es mejor usar super () y así usar el primer método getattribute que Python encuentra en sus clases base? -super(D, self).__getattribute__(name)
gepatino
10
O simplemente puede usar super().__getattribute__(name)en Python 3.
jeromej
25

En realidad, creo que desea utilizar el __getattr__método especial en su lugar.

Cita de los documentos de Python:

__getattr__( self, name)

Se llama cuando una búsqueda de atributos no ha encontrado el atributo en los lugares habituales (es decir, no es un atributo de instancia ni se encuentra en el árbol de clases para sí mismo). nombre es el nombre del atributo. Este método debe devolver el valor del atributo (calculado) o generar una excepción AttributeError.
Tenga en cuenta que si el atributo se encuentra a través del mecanismo normal, __getattr__()no se llama. (Esta es una asimetría intencional entre __getattr__()y el método a continuación para una forma de obtener un control total en las clases de nuevo estilo.__setattr__() .) Esto se hace tanto por razones de eficiencia como por el__setattr__() no tendría forma de acceder a otros atributos de la instancia. Tenga en cuenta que al menos, por ejemplo, las variables, puede falsificar el control total no insertando ningún valor en el diccionario de atributos de instancia (sino insertándolos en otro objeto). Ver el__getattribute__()

Nota: para que esto funcione, la instancia no debe tener un testatributo, por lo que la línea self.test=20debe eliminarse.

tzot
fuente
2
En realidad, de acuerdo con la naturaleza del código del OP, anular __getattr__por testsería inútil, ya que siempre lo encontraría "en los lugares habituales".
Casey Kuball
1
enlace actual a los documentos de Python relevantes (parece ser diferente del mencionado en la respuesta): docs.python.org/3/reference/datamodel.html#object.__getattr__
CrepeGoat
17

Referencia del lenguaje Python:

Para evitar la recursividad infinita en este método, su implementación siempre debe llamar al método de la clase base con el mismo nombre para acceder a cualquier atributo que necesite, por ejemplo object.__getattribute__(self, name),.

Sentido:

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

Estás pidiendo un atributo llamado __dict__. Como es un atributo, __getattribute__se llama para buscar __dict__qué llamadas__getattribute__ qué llamadas ... yada yada yada

return  object.__getattribute__(self, name)

El uso de las clases base __getattribute__ayuda a encontrar el atributo real.

ttepasse
fuente
13

¿Estás seguro de que quieres usar __getattribute__ ? ¿Qué estás tratando de lograr realmente?

La forma más sencilla de hacer lo que pide es:

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

    test = 0

o:

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

    @property
    def test(self):
        return 0

Editar: Dtenga en cuenta que una instancia de tendría diferentes valores de testen cada caso. En el primer casod.test sería 20, en el segundo sería 0. Dejaré que usted averigüe por qué.

Edit2: Greg señaló que el ejemplo 2 fallará porque la propiedad es de solo lectura y el __init__método intentó establecerla en 20. Un ejemplo más completo sería:

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)

Obviamente, como clase, esto es casi completamente inútil, pero te da una idea para seguir adelante.

Singletoned
fuente
Oh, eso no funciona en silencio cuando diriges la clase, ¿no? Archivo "Script1.py", línea 5, en init self.test = 20 AttributeError: no se puede establecer el atributo
Greg
Cierto. Lo arreglaré como un tercer ejemplo. Bien descrito.
Singletoned
5

Aquí hay una versión más confiable:

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)

Llama al método __ getattribute __ desde la clase principal, y eventualmente regresa al objeto. __ getattribute __ método si otros ancestros no lo anulan.

ElmoVanKielmo
fuente
5

¿Cómo se __getattribute__usa el método?

Se llama antes de la búsqueda de puntos normal. Si sube AttributeError, llamamos__getattr__ .

El uso de este método es bastante raro. Solo hay dos definiciones en la biblioteca estándar:

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

Mejores prácticas

La forma correcta de controlar mediante programación el acceso a un solo atributo es con property. La clase Ddebe escribirse de la siguiente manera (con el establecedor y el eliminador opcionalmente para replicar el comportamiento previsto aparente):

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

Y uso:

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

Una propiedad es un descriptor de datos, por lo que es lo primero que se busca en el algoritmo de búsqueda de puntos normal.

Opciones para __getattribute__

Tiene varias opciones si es absolutamente necesario implementar la búsqueda para cada atributo a través de __getattribute__.

  • subir AttributeError, haciendo __getattr__que se llame (si se implementa)
  • devolver algo de él por
    • usando superpara llamar a la objectimplementación de los padres (probablemente )
    • vocación __getattr__
    • implementar su propio algoritmo de búsqueda de puntos de alguna manera

Por ejemplo:

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

Lo que originalmente querías.

Y este ejemplo muestra cómo podría hacer lo que originalmente quería:

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)

Y se comportará así:

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

Revisión de código

Tu código con comentarios. Tiene una búsqueda punteada de sí mismo en __getattribute__. Es por eso que obtiene un error de recursividad. Puede verificar si el nombre es "__dict__"y usar superpara una solución alternativa, pero eso no cubre __slots__. Dejaré eso como un ejercicio para el lector.

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
Aaron Hall
fuente