Fundamentos de POO para interfaces gráficas en Python

Métodos de clase

Más allá de los atributos, una clase también puede definir métodos. Los métodos son funciones que encontramos en los objetos para interactuar con ellos.

Hay un ejemplo que me encanta para ilustrar esto. Supongamos que nuestra galleta tiene un atributo chocolate que es falso por defecto, indicando que la galleta no tiene chocolate:

objetos.py

class Galleta:
    sabor = "dulce"
    chocolate = False

Ahora si quisiéramos chocolatear nuestras galletas sin modificar directamente el atributo chocolate, lo que podemos hacer es crear un método chocolatear:

class Galleta:
    sabor = "dulce"
    chocolate = False

    def chocolatear():
    	chocolate = True

galleta = Galleta()
galleta.chocolatear()
print(galleta.chocolate)

Esta es la lógica básica, sin embargo tendremos un problema al ejecutarlo, Python nos devolverá un error indicando:

TypeError: chocolatear() takes 0 positional arguments but 1 was given

Se está quejando de que el método chocolatear requiere definir por lo menos un argumento, ¿cuál es la razón? Ahora mismo lo vais a entender.

Haced esto:

class Galleta:
    sabor = "dulce"
    chocolate = False

    def chocolatear(argumento_misterioso):
    	chocolate = True

galleta = Galleta()
galleta.chocolatear()
print(galleta.chocolate)

Al hacer este cambio veremos que el código se ejecuta, sin embargo el atributo chocolate no se ha modificado al llamar al método chocolate.

Esto se debe a que el atributo chocolate que hay definido fuera del método y el que hay dentro no son el mismo, forman parte de ámbitos diferentes. El primero pertenece a la clase y el otro al método, algo que podemos constatar consultando sus posiciones en la memoria:

class Galleta:
    sabor = "dulce"
    chocolate = False

    def chocolatear(argumento_misterioso):
        chocolate = True
        print("Posición en la memoria de 'chocolate' en chocolatear ->",
              hex(id(chocolate)))


galleta = Galleta()
galleta.chocolatear()
# print(galleta.chocolate)
print("Posición en la memoria de 'chocolate' en la galleta ->",
      hex(id(galleta.chocolate)))

Como véis se almacenan en lugares diferentes y eso significa que no son lo mismo.

¿Entonces como vamos a interactuar con los atributos de nuestras instancias? Tranquilos, aquí entra en juego ese argumento misterioso. ¿Qué debe de ser? Vamos a mostrarlo:

class Galleta:
    sabor = "dulce"
    chocolate = False

    def chocolatear(argumento_misterioso):
        chocolate = True
        print("El valor del argumento misterioso es ->", argumento_misterioso)


galleta = Galleta()
galleta.chocolatear()
# print(galleta.chocolate)

Y nos devolverá:

El valor del argumento misterioso es <__main__.Galleta object at 0x01170830>

¡Un objeto galleta! ¿Podría ser la propia instancia del objeto galleta? Vamos a comparar la posición de sus instancias en la memoria:

class Galleta:
    sabor = "dulce"
    chocolate = False

    def chocolatear(argumento_misterioso):
        chocolate = True
        print("Instancia en la memoria del argumento misterioso ->",
              hex(id(argumento_misterioso)))


galleta = Galleta()
galleta.chocolatear()
# print(galleta.chocolate)
print("Instancia en la memoria de la galleta ->", hex(id(galleta)))

Pues efectivamente, este argumento misterioso es en realidad la propia instancia de la galleta, la posición en la memoria donde se ha creado y se guardan sus datos. En otras palabras, si hacemos referencia a él debería ser lo mismo que hacer referencia a la galleta, así que podríamos asignar su atributo chocolate:

class Galleta:
    sabor = "dulce"
    chocolate = False

    def chocolatear(instancia):     # <-- cambiar nombre a instancia
        instancia.chocolate = True  # <-- cambiar nombre a instancia


galleta = Galleta()
galleta.chocolatear()
print(galleta.chocolate)

¡Por fin hemos llegado al kit de la cuestión! Ese argumento que necesitan los métodos es una referencia a la propia instancia para poder acceder a sus atributos y métodos internamente.

¿Y sabéis qué? Por conveniencia a este argumento deberíamos llamarlo self haciendo referencia a "la instancia en sí misma". Esta es la razón por la cuál VSC se lleva quejando un montón de rato, así que vamos a escribirlo bien:

def chocolatear(self):
    self.chocolate = True

En resumen, para manipular los atributos de los objetos desde sus métodos internos es necesario tener la referencia a sus instancias en la memoria y esa es la razón por la que se envía automáticamente como primer argumento.