Mostrando entradas con la etiqueta Atributos. Mostrar todas las entradas
Mostrando entradas con la etiqueta Atributos. Mostrar todas las entradas

jueves, 13 de junio de 2024

POO

Atributos. Modificar valores


Una de las tareas que con más frecuencia debemos realizar al trabajar con objetos es dar valor a los atributos y modificar esos valores. En la entrada anterior en la que comparamos funciones con métodos ya estuvimos trabajando sobre ello, así como en la entrada referida a los atributos. En ésta nos vamos a centrar en esta cuestión específicamente.


Caben tres posibilidades u opciones para modificar el valor de un atributo:
  • Modificarlo directamente
  • Modificarlo mediante un método específico
  • O modificarlo de forma incremental, también mediante un método
Partiremos de una clase simple, en cuyos objetos no se tiene pensado modificar el contenidos de los atributos, para ir mostrando las tres opciones enunciadas antes.

class coche:
    def __init__(self,marca,modelo,anno):
        self.marca=marca
        self.modelo=modelo
        self.anno=anno

    def descripcion(self):
        coche_descripcion= f"Mi coche es un {self.marca} {self.modelo} de {self.anno}"
        return coche_descripcion

mi_coche=coche("Seat","Toledo",1996)
texto=mi_coche.descripcion()
print(texto)

La única novedad que presenta este script es que el método (descripcion(self)) devuelve (return) una cadena f que describe el objeto, por lo que su uso debe asociarse a una variable (texto). Por lo demás, presenta un funcionamiento de sobre conocido.

Para explicar las implicaciones de la modificación de atributo, deberemos incluir en la definición de atributos uno que no figure como parámetro, por ejemplo, el valor del cuentakilómetros, así como un método para mostrar el cuentakilómetros actual del vehículo. Esto modifica la definición de la clase como sigue:

class coche:
    def __init__(self,marca,modelo,anno):
        self.marca=marca
        self.modelo=modelo
        self.anno=anno
        self.cuentakilometros=0 

    def descripcion(self):
        coche_descripcion= f"Mi coche es un {self.marca} {self.modelo} de {self.anno}"
        return coche_descripcion

    def cuentaK(self):
        print(f"El cuentakilómetros de mi coche marca {self.cuentakilometros} kilómetros")

Procedamos ahora a introducir/modificar el valor inicial del parámetro cuentakilometros directamente en la definición del objeto mi_coche:

mi_coche=coche("Seat","Toledo",1996)
mi_coche.cuentakilometros = 1200
texto=mi_coche.descripcion()
print(texto)
mi_coche.cuentaK()

Una vez definido el objeto mi_coche (mi_coche=coche("Seat","Toledo",1996)) establecemos el valor del atributo no definido como parámetro (y en consecuencia no establecido ya como valor en la definición del objeto) mediante la expresión basada en la sintaxis del punto (mi_coche.cuentakilometros = 1200). Posteriormente llamaremos a este método de forma directa (mi_coche.cuentaK()), ya que originalmente no cuenta con la instrucción return.

La segunda opción consiste en modificar el atributo mediante un método específico, el siguiente:

    def cuentaK(self, km_actuales):
        self.cuentakilometros = km_actuales
   print(f"El cuentakilómetros de mi coche marca {self.cuentakilometros} kilómetros")

... que asocia el valor dado al atributo (km_actuales) al parámetro cuentakilometros que después se imprime por pantalla mediante la instrucción print()

mi_coche.cuentaK(1200)

Finalmente veamos cómo se concreta el método que permite incrementar el valor del parámetro, añadiéndolo como método nuevo.

class coche:
    def __init__(self,marca,modelo,anno):
        self.marca=marca
        self.modelo=modelo
        self.anno=anno
        self.cuentakilometros=0

    def descripcion(self):
        coche_descripcion= f"Mi coche es un {self.marca} {self.modelo} de {self.anno}"
        return coche_descripcion

    def cuentaK(self,kilometraje):
        self.cuentakilometros =kilometraje

    def leer_cuentaK(self):
        print(f"Este coche ha recorrido {self.cuentakilometros} Km")

    def incrementarKm(self,km):
        self.cuentakilometros += km

Observa que he eliminado la instrucción print() del método cuentaK() primitivo y he creado un método específico de escritura de los Km que lleva recorridos el coche (leer_cuentaK()). De este modo independizamos ambos procesos (dar valor al atributo cuentakilometros y escribir el resultado. El objetivo es reutilizar este método cuando incrementemos los kilómetros, que es lo que hacemos con el método incrementarKm()

Este es el método que nos permite incrementar los kilómetros inicialmente establecidos (mediante el método cuentaK()). Observa que incrementarKm() tiene dos parámetros: el obligatorio self y km, que serán los km que incrementemos cada vez que usemos el método. La instrucción de este método consiste en añadir km al dato que contiene el atributo cuentakilometros.

Desde el lado de la creación de un objeto (asociado a la variable mi_coche)...

mi_coche=coche("Seat","Toledo",1996)
texto=mi_coche.descripcion()
print(texto)
mi_coche.cuentaK(1200)
mi_coche.leer_cuentaK()
mi_coche.incrementarKm(100)
mi_coche.leer_cuentaK()

... podemos apreciar el uso de los cuatro métodos de la clase, incluyendo el uso repetido del método leer_cuentaK(): primero para informar de los km del cuentakilómetros y después para informar del resultante tras el incremento.

POO

Atributos

Aunque ya vimos que son los atributos, no está demás completar una información que resulta ser parcial, ya que la definición de atributo no se limita a lo expuesto... y tiene repercusiones.

Sigo en esto la explicación de Matthes, E (2021:186-191) en lo que deriva de las formas en que se puede plantear la modificación de atributos, con lo que, en principio, trataré sobre la definición de atributos y posiblemente en una próxima entrada sobre su modificación.

Los atributos son, como ya sabemos, los datos que caracterizan a una clase y se concretan como tales en los objetos o instancias de esa clase. Como tales datos se guardan en memoria, identificando esa dirección de memoria con un nombre de variable, de ahí que podamos redefinir los atributos como las variables que caracterizan al objeto.

Como tales variables, en el momento de la construcción de la clase (o lo que es lo mismo: en el momento en que utilizamos esa función especial a la que llamamos constructor, y que en Python se expresa como función __init__()) tenemos tres opciones para definir las variables/atributos:
  • Opción A, la utilizada en la entrada referenciada al inicio de la actual siguiendo a Matthes, E(2021:182-186), estableciendo los atributos como parámetros de la función, tras el parámetro self. 
  • Opción B, la que utiliza Cuevas, A (2016:186-192) en su completa y complicada explicación de cómo se construye una clase: incluyéndolas como parte del desarrollo de la función __init__() pero sin incluirlas como parámetros de la función-constructor.
  • Y la opción C, la mezcla de las anteriores: incluyendo unos como parámetros y otros según B.
Todas ellas (las opciones) son válidas y es posible encontrarlas como explicación en diferentes documentos, incluyendo los vídeos de You Tube. Lo que no siempre encontrarás es una explicación de por qué estas diferencias y qué implicaciones tiene. Vamos a hablar a continuación de esas diferencias y sus implicaciones.

En primer lugar, no existe (que yo sepa) implicación respecto al funcionamiento de la clase y de los objetos derivados de ella: tan clase y tan objeto) es una que utilice parámetros en el constructor como que no los use (al margen de self, claro, que es obligado). Pero las implicaciones son evidentes: si usamos parámetros "atributivos" en __init__() esto va a tener consecuencias en la sintaxis de escritura de la clase y de los objetos:
  • En cómo se escribe la función __init__():
    • Lo obvio: con/sin parámetros
    • Y en la forma en que inicializa el parámetro
  • Y en cómo se definen los atributos del objeto
  • Y, finalmente, ¿en cómo se aborda el cambio de esos atributos?
Veamos cada una de estas cuestiones en un ejemplo concreto como puede ser la clase perro utilizado en otra entrada. En ella se utilizaba exclusivamente la opción A.
def __init__(self,nombre,edad):
self.nombre = nombre
self.edad = edad 
  • Al declarar los atributos como parámetros (nombre, edad), inicializarlos consiste en referenciarlos a la propia clase mediante la expresión self.nombre = nombre
  • Por lo que crear un objeto exige responder a la lógica de la relación parámetro-argumento que ya vimos en las funciones, incluyendo el dato en que se concreta el atributo: MiPerro(Atenea,10)
Frente a esto, la opción B resolvería la función __init__() y la inicialización de los atributos como sigue:

def __init__(self):
self.nombre = ""
self.edad = 0 
  • La inicialización de los atributos (de clase) no es una auto-referencia como cuando los declaramos como parámetros (self.nombre = nombre). Aunque seguimos utilizando la auto-referencia y la sintaxis del punto (de pertenencia jerárquica, podríamos decir) (self.nombre), ya no podemos remitir a la propia variable (que después se concretará como dato-argumento en la declaración del objeto); debemos inicializarlo con un valor (que puede ser 0 y su equivalente: cadena vacía, como es el caso self.nombre = "")
  • Al crear el objeto ya no podemos incluirlos como argumentos en la función correspondiente, como si sucedía en el modelo A (perrita = MiPerro("Atenea",10))
  • Mejor habría que decir "ni debemos", ya que en A, no emplear estos argumentos genera error. En B sucede lo mismo si lo hacemos (ya que no existen como parámetros en __init__()): perrita = MiPerro() es ahora la forma de crear el objeto como perteneciente a la clase MiPerro, el cual queda identificado mediante la variable que lo referencia (perrita) ya que, en caso de no realizarse esta asignación del objeto a la variable, se crearía el objeto, pero como objeto anónimo. Obsérvese que tras el nombre de la clase se sitúan dos paréntesis [MiPerro()], mientras que en la identificación de la clase no se hace uso de tales (class MiPerro:). Por contra, no se utilizan los dos puntos (:), que sí lo están en la definición de la clase (y de las funciones): en resumen, estamos usando la sintaxis de la llamada a una función.
  • La alternativa es asignar contenido a los argumentos utilizando la sintaxis del punto, tomando como referencia el nombre del objeto al que se asocia el argumento en la definición de la clase [perrita.nombre = "Azucena"]. De ello se deriva que mientras en A la creación del objeto y la asignación de valores a los atributos se resuelve como parte de la llamada a la función (identificación como miembro de la clase...
perrita = MiPerro("Atenea",10)

... en B primero se define la relación de pertenencia del objeto a la clase y después se asignan datos/valores a los atributos, que se identifican como tales (y no come meras variables) por medio de la sintaxis del punto:

perrita = MiPerro()
perrita.nombre = "Azucena"
perrita.edad = 10 

[Nótese la ausencia de sangrado y lo que esto supone entre la línea de creación del objeto y las de datación de los atributos]

Los resultados de ambos procedimientos (A y B) son los mismos si aplicamos las mismas demandas:

print(f"Nombre: {perrita.nombre}")
print(f"Edad: {perrita.edad}")
print(f"Mi perrita se llama {perrita.nombre} y tiene {perrita.edad} años")
perrita.sentarse()

Nombre: Atenea
Edad: 10
Mi perrita se llama Atenea y tiene 10 años
Atenea se sienta cuando se lo ordeno (y quiere, claro).

... y cabe entender que el uso del modelo mixto (C) supone aplicar ambos procedimiento tanto en la inicialización de los atributos como en la creación del objeto. Un ejemplo sencillito, añadiendo sexo como tercer parámetro. Supongo que intuyes cual puede ser el resultado...

class MiPerro:
    def __init__(self,nombre,edad):
        self.nombre = nombre
        self.edad = edad
        self.sexo =""
    def sentarse(self):
        print(f"{self.nombre} se sienta cuando se lo ordeno (y quiere, claro).")
perro = MiPerro("Casimiro",10)
perro.sexo = "perrito"
print(f"Nombre: {perro.nombre}")
print(f"Edad: {perro.edad}")
print(f"Mi {perro.sexo} se llama {perro.nombre} y tiene {perro.edad} años")
perro.sentarse()