Python: gamberradas con __getattr__ y amigos
Tags: spanish, python, programming, humor
Published: 2024-05-06 11:23:48

---
title: Python: gamberradas con __getattr__ y amigos
published: true
description: ¿Qué se puede hacer con este método especial?
tags: #spanish #python #programming #humor
cover_image: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/2000px-Python-logo-notext.svg.png
# Use a ratio of 100:42 for best results.
# published_at: 2024-05-06 10:20 +0000
---

Supongamos el siguiente (inocente) código:

```python
class Personaje:
    def __init__(self, nombre):
        self.__nombre = nombre
    ...

    @property
    def nombre(self):
        return self.__nombre
    ...
...
```

Tenemos entonces definido un personaje con una propiedad *nombre*. Poca cosa, vamos.

Podríamos crear un método **Personaje**.*di(msg: str)*, que devolviera un mensaje pasado por parámetro formateado adecuadamente. Evidentemente, podríamos crear directamente dicho método, pero eso... es aburrido.

Tenemos la posibilidad de utilizar `__getattr__(item: str)`, que se ejecuta cuando se intenta invocar un miembro cualquiera de la clase (independientemente de que sea un método o atributo), y este miembro no existe. Se podría decir que es un último recurso antes de que el *runtime* del lenguaje lance la excepción **AttributeError** (recordemos que en notación Python, todos los miembros son *atributos*, los métodos solo se distinguen por devolver **True** a la función *callable()*).

La idea es que, cuando se invoque algo como `p1 = Personaje("Homer"); p1.di("¡Bart! ¿Qué has hecho?")`, nuestro método `__getattr__()` entrará en acción y devolverá la respuesta adecuada.

```python
class Personaje:
    def __init__(self, nombre):
        self.__nombre = nombre
    ...

    @property
    def nombre(self):
        return self.__nombre
    ...

    def __getattr__(self, nombre_miembro):
        if nombre_miembro == "di":
            return lambda msg: f"{self.nombre}: {msg}"
        ...

        raise AttributeError(f"{self.__class__.__name__}.{nombre_miembro}??")
    ...
...


if __name__ == "__main__":
    homer = Personaje("Homer Simpson")
    print(homer.di("¡Bart! ¿Qué has hecho?"))
...
```

Hay que tener en cuenta un par de cuestiones. Cuando se ejecuta `__getattr__()`, si no reconocemos el miembro que se intenta invocar, se espera que se invoque **AttributeError** con algo parecido a "el atributo *x* no existe".

La segunda cuestión es: ¿por qué se devuelve una *lambda*? Para responder a esta pregunta tenemos que analizar cómo se realizan las llamadas a los métodos en Python. Así, con la instrucción `homer.di("¡Bart!...")`, en realidad `__getattr__` solo se involucra hasta `homer.di`, es decir, la búsqueda de `di` en el objeto *homer*, y nada más. Cuando se retorna el método, es cuando se le aplica el argumento (el mensaje), es decir, `("¡Bart!...")`. Como ilustración, si Python obtiene una función de `homer.di`, vamos a suponer que se llama `f_di`. Bien, pues el siguiente paso será `f_di("¡Bart!...")`.

Así, tenemos que *simular* este comportamiento, y por eso devolvemos una *lambda* que va a comportarse, al fin y al cabo, como si el método `di()` existiera realmente, y por tanto esta *lambda* tomará el papel de ese `f_di` explicado arriba, teniendo que comportarse de la misma forma.

Si el método especial `__getattr__` se ejecuta cuando un miembro no es encontrado, `__getattribute__` se ejecuta **siempre** que se invoca a un miembro, independientemente de si es encontrado. Si deseamos interferir en la invocación, podemos hacerlo con este método. En otro caso, la "dejamos pasar", invocando a `__getattribute__` en **object**.

Supongamos que queremos crear un personaje como **Maggie Simpson** (sí, el bebé). Realmente, no habla, así que queramos lo que queramos que diga, siempre va a hacer el ruido del chupete: "Chup, chup".

```python
class Personaje:
    def __init__(self, nombre):
        self.__nombre = nombre
    ...

    @property
    def nombre(self):
        return self.__nombre
    ...

    def __getattr__(self, nombre_miembro):
        if nombre_miembro == "di":
            return lambda msg: f"{self.nombre}: {msg}"
        ...

        raise AttributeError(f"{self.__class__.__name__}.{nombre_miembro}??")
    ...

    def __getattribute__(self, nombre_miembro):
        if nombre_miembro == "di":
            nombre = self.nombre
            if nombre.lower().startswith("maggie"):
                return lambda msg: f"{self.nombre}: Chup, chup"
            ...
        ...
        
        return super().__getattribute__(nombre_miembro)
    ...
...

if __name__ == "__main__":
    homer = Personaje("Homer Simpson")
    maggie = Personaje("Maggie Simpson")
    print(homer.di("¡Bart! ¿Qué has hecho?"))
    print(maggie.di("¡Tengo hambre!"))
...
```

La ejecución muestra:

```
Maggie Simpson: Chup, chup
Homer Simpson: ¡Bart! ¿Qué has hecho?
```

Devolvemos una *lambda* en `__getattribute__` que acepta un parámetro como mensaje, pero que realmente ignora, devolviendo la descripción del ruido del chupete.

Por cierto, en ambas funciones hay que tener mucho cuidado al llamar a métodos de la clase, puesto que podemos caer en una llamada recursiva *infinita* (hasta que el número de llamadas recursivas reviente el *stack*).

Como resumen, podemos asumir por tanto que mientras `__getattribute__` permite interferir en el proceso normal de una invocación a un miembro de la clase, `__getattr__` permite añadirle funcionalidad a la clase.

Ahora algo más *serio*. ¿Para qué podemos emplear estos dos métodos especiales? Un ejemplo podría ser el *inyectar* en una clase comprobaciones de precondiciones, postcondiciones e invariantes. En el siguiente ejemplo, emplearemos `__getattribute__` para comprobar una invariante de clase en un método específico.

Supongamos la siguiente clase **Conjunto**.

```python
class Conjunto:
    """Representa una lista de elementos que no se repiten."""
    def __init__(self):
        self.__conj = []
    ...
    
    def __add(self, val):
        self.__conj.append(val)
    ...

    def __str__(self):
        return str.join(", ", [str(x) for x in self.__conj])
    ...

    def __chk_invariante(self):
        for i, x in enumerate(self.__conj):
            cdr = self.__conj[i + 1:]
            if x in cdr:
                raise ValueError(f"{x} repetido en: {cdr.index(x) + i + 1}")
            ...
        ...
    ...
...
```

Podemos crear un método `__getattribute__` que "obligue" a llamar a la invariante de clase *__chk_invariante()*, tras invocar *add(x)* para añadir *x* al conjunto.

```python
class Conjunto:
    # más cosas...

    def __getattr__(self, str_m):
        if str_m == "add":
            return lambda val: (self.__add(val), self.__chk_invariante())[0]
        ...

        raise AttributeError(f"{self.__class__.__name__}.{str_m}??")
    ...
```

Lo cierto es que aquí recurrimos a un truco para poder ejecutar ambas funciones sin tener que modificar sus parámetros y retornos: se crea una tupla con los valores de retorno de ambas funciones (que terminarán siendo `None`, y que en realidad no nos importan en absoluto), para después devolver la primera de ellas, la que realmente añade el elemento. Pero al ser una tupla, también se ejecutará las siguiente a continuación, obteniendo el resultado deseado.

Así, si se ejecuta el siguiente código:

```python
c1 = Conjunto()
c1.add(1)
c1.add(2)
c1.add(3)
print(c1)
```

Se obtiene el resultado esperado de `1, 2, 3`. Pero si en cambio se trata de ejecutar este otro código:

```python
c1 = Conjunto()
c1.add(1)
c1.add(2)
c1.add(2)
print(c1)
```

Lo que obtendremos será:

```
File conjunto.py line 69,
    c1.add(2)
File conjunto.py line 57,
    return lambda val: [self.__add(val), self.__chk_invariante()][0]
ValueError: 2 repetido en: 2
```

¡Python mola! ¿A que sí?