1

Estaba haciendo unas pruebas con python tratando de "forzar", por así decirlo el encapsulamiento en python, ya que en python no existe como tal los atributos privados:

import re

class Mixin(object):
    __a = 1

    def __getattribute__(self, name):
        if re.match(f'_{ self.__class__.__name__ }__', name):
            raise AttributeError(f"'{ self.__class__.__name__ }' object has no attribute '{ name }'")

        return super().__getattribute__(name)

Mixin()._Mixin__a

Se supone que de esta manera no se pude acceder al atributo __a así: _Mixin__a.

Pero me da el siguiente error:

Traceback (most recent call last):
  File "/home/lcteen/Documentos/Programming/Python/Practices/sss.py", line 24, in <module>
    Mixin()._Mixin__a
  File "/home/lcteen/Documentos/Programming/Python/Practices/sss.py", line 19, in __getattribute__
    if re.match(f'_{ self.__class__.__name__ }__', name):
  File "/home/lcteen/Documentos/Programming/Python/Practices/sss.py", line 19, in __getattribute__
    if re.match(f'_{ self.__class__.__name__ }__', name):
  File "/home/lcteen/Documentos/Programming/Python/Practices/sss.py", line 19, in __getattribute__
    if re.match(f'_{ self.__class__.__name__ }__', name):
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded

No entiendo que estoy haciendo mal, ni el porque sucede el error.

Julio Cesar
  • 3,150
  • 11
  • 17
  • 39

1 Answers1

2

Es quizás el error más común cuando se reimplementa __getattribute__. El codigo mínimo para reproducir el problema es muy básico:

class Mixin:
    def __getattribute__(self, name):
        self.__class__

Mixin().test

Hay que tener en cuenta que cuando se implementa __getattribute__, dicho método es siempre el punto de entrada para cualquier acceso a atributos de la instancia.

Analicemos el ejemplo anterior:

  1. Mixin().test crea una instancia de la clase e intenta acceder al atributo test.

  2. Lo anterior hace que se llame al método __getattribute__ de la siguiente forma:

    self.__getattribute__("test")
    
  3. Se ejecuta la línea self.__class__, lo que hace que se llame de nuevo a __getattribute__:

    self.__getattribute__("__class__")
    
  4. Se llega de nuevo a la línea self.__class__.... Y así hasta que la pila se llene y el intérprete estalle, bueno no, porque Python limita por defecto a 1000 las llamadas recursivas permitidas y nos muestra el mencionado error.

La solución en estos casos es siempre delegar la resolución del atributo a la clase padre (object en este caso), evitando así la llamada recursiva. Lo normal es usar super para ello:

class Mixin:
    __a = 1

    def __getattribute__(self, name):
        class_ =  super().__getattribute__("__class__")
        if re.match(f"_{ class_.__name__ }__", name):
            raise AttributeError(f"'{ class_.__name__ }' object has no attribute '{ name }'")
        return super().__getattribute__(name)

super () devuelve un objeto proxy que buscará cualquier método que se pueda encontrar a continuación en las clases base siguiendo el MRO. Si no existe tal método, fallará con un AttributeError pero nunca llamará al método original.

Alternativamente, puedes llamar al método __getattribute__ de objet directamente. La implementación en C del método es siempre el punto final en el MRO para el acceso al atributo y tiene acceso directo a __dict__.

class Mixin:
    __a = 1

    def __getattribute__(self, name):
        class_ =  object.__getattribute__(self, "__class__")
        if re.match(f"_{ class_.__name__ }__", name):
            raise AttributeError(f"'{ class_.__name__ }' object has no attribute '{ name }'")
        return super().__getattribute__(name)
FJSevilla
  • 55,603
  • 7
  • 35
  • 58
  • 2
    De todos modos, aún así el atributo sigue expuesto a subclases: `class C(Mixin):pass; C()._Mixin__a` – ChemaCortes May 02 '20 at 11:22
  • 1
    Si tienes razón Chema, me había enfocado en el error y ni siquiera pensé en ello, siguiendo la misma idea original siempre podemos hacer la expresión más genérica, tipo `r"_[^_]+__"` – FJSevilla May 02 '20 at 11:47
  • @FJSevilla se me paso por alto totalmente lo del `self.__class__`, gracias por tu respuesta! – Julio Cesar May 02 '20 at 15:49
  • 1
    @FJSevilla Una alternativa mejor sería controlar el acceso al atributo privado con una _metaclase_, ya que sabes el nombre concreto de la clase. Pero no creo que valga la pena tanto esfuerzo. No creo que se pueda evitar un `object.__getattribute__(Mixin(), "_Mixin__a")`. Peor aún, un `object.__setattr__(m, "_Mixin__a", 0)` – ChemaCortes May 02 '20 at 17:08
  • 1
    @ChemaCortes, no, realmente no vale tanto esfuerzo, desde el principio... Python simplemente no tiene atributos privados como característica de diseño, cualquier desarrollador que conozca como se implementa la POO, si quiere, va a acceder al atributo, como bien comentas por muchas trabas que se pongan los métodos `object.__getattribute__`/`object.__setattr__` siempre van a estar ahí al final de todo y con pleno poder de acceso. Para evitar accesos "por despiste" tenemos la convención `_nombre` o el name-mangling, para los intencionados, no merece la pena siquiera complicarse en mi opinión... – FJSevilla May 19 '20 at 07:05