Mutables e Inmutables


Mutabilidad e inmutabilidad en Python (y en otros lenguajes de programación), que las variables puedan cambiar (mutables) o no (inmutables) ¿Acaso no puede cambiar el contenido de cualquier variable, salvo que sean “finales” (esto es más de Java)? Sí, que sean “finales” quiere decir que la variable va a ser una constante y que no va a cambiar nunca a lo largo de la ejecución del código del programa. Realmente el valor de las variables por “debajo” cambian unas sí y otras no aunque parezca que realmente cambian.

Nota sobre los lenguajes a lo que se aplica: Aunque aquí pondré ejemplos de Python, se aplica de manera muy parecida o exactamente igual en la mayoría (por no decir «todos») de los lenguajes; como en Java, JavaScript, PHP, etc.

Código en GitHub (Recomiendo primero leer el artículo y descargar/clonar el código al final): https://github.com/Invarato/JarrobaPython/tree/master/mutables_e_inmutables

Antes de empezar veamos un ejemplo muy sencillo en Python para ir descubriendo detalles:

mi_texto = "un texto"

Podremos cambiar el valor de la variable añadiendo algo más:

mi_texto = mi_texto + " y algo más"

Por lo que al imprimir la variable por pantalla:

print(mi_texto)

Mostrará:

un texto y algo más

Viendo que el valor de tipo String de la variable mi_texto se puede cambiar, entonces te pregunto ¿Sting es mutable? Pues no, String es Inmutable porque en memoria RAM no ha ampliado el “un texto” guardado previamente, sino que lo ha copiado junto con el añadido de “ y algo más” para guardar al completo “un texto y algo más” en otro lado de la memoria RAM, y la variable apuntará al último valor guardado. Es decir, un String siempre se va a crear de nuevo (inmutable) aunque nosotros creamos que se modifica (falsa creencia de mutabilidad). Veamos esto con detalle.

Inmutables

Los inmutables son los más sencillos de utilizar en programación (suelen ser los tipos simples de datos: String, Integer, Boolean, etc.) pues hacen exactamente lo que se espera que hagan en cada momento, y paradójicamente para funcionar así son los que más castigan a la memoria (no están optimizados para ocupar menos memoria al copiarse, y degradan más la vida útil de la memoria al escribirse más veces).

Un objeto inmutable una vez creado, como su propio nombre indica, su estado no puede cambiar una vez ha sido creado ¿Pero si he cambiado muchas veces valores tipo String o int después de crearlos? En apariencia, pero no cambian nada y te lo explico a continuación (sugiero que a medida que leas cada uno de los próximos párrafos te fijes en la imagen que lo acompaña debajo; en las siguientes imágenes se lee primero desde la izquierda la “Consola de Python” con una flecha verde que indica la línea de código añadida, y luego la parte de la derecha con la memoria y sus ocupantes).

Cuando declaramos una variable (en la siguiente imagen “mi_variable”) en Python (o cualquier otro lenguaje de programación) se crea en memoria una variable en una dirección de memoria (en la imagen se guardará en la dirección de memoria “id: 0”). Si luego a continuación le asignamos (la variable es un cursor o puntero que “apuntará a la dirección de memoria de su valor”) un valor mutable, que para este ejemplo será un texto (String, en la imagen “algo, algo más”) y se creará en otra posición de memoria (en la imagen en la dirección de memoria “id: 1”); entonces la variable anterior apuntará al objeto mutable que contiene su valor.

Aunque podría hacer un ejemplo simple, aquí te voy a enseñar dos casos de la inmutabilidad (aunque en el fondo es lo mismo, pero aclara muchos conceptos este primer caso). El primer caso de inmutabilidad, y el que creo que es más importante porque suel ser el que más confunde, es cuando se pasa como parámetro el valor (de una variable) inmutable a una función. Cuando se pasa el valor inmutable como parámetro a una función (en la imagen a la variable “var_de_func” que está en la dirección de memoria “Id:2”), inmediatamente dicho valor se copia a otra dirección de la memoria (en la imagen “Id: 3”); por lo que podrás deducir que si se modifica dentro de la función habrá que extraerlo (normalemnte con el “return” dentro de la función) para poder utilizarlo fuera de la función. Por tanto, lo que pase dentro de la función se quedará dentro de la función (como dice el dicho “lo que pase en las Vegas se queda en las Vegas” 😛 ) y no afectará a variables fuera de la función (salvo que se devuelva con “return”).

El segundo caso de inmutabilidad no es tan apreciable y pasa más desapercibido. Esto es al modificarse un objeto de un valor inmutable, lo que hace realmente es copiarse y guardarse la modificación propiamente en otra dirección de memoria (en la imagen “Id: 4”), haciendo que la variable apunte a este nuevo objeto y borrando el objeto que no es apuntado (normalmente el borrado lo van a hacer los recolectores de basura o en inglés “garbage collector”, que se encargan de buscar objetos a los que no apunte nadie y liberar esa memoria).

De este modo, si por ejemplo al final mostrar por pantalla el valor guardado de “mi_variable” nos devolverá lo que está guardado en la posición de memoria a la que apunta (que es lo que había al principio, pues no se ha tocado):

O si mostrásemos por pantalla lo que hay dentro de la función nos imprimiría a donde apunta la variable “var_de_func”:

En resumen, un valor de tipo inmutable no va a cambiar nunca una vez guardado en una posición de memoria y si se tiene que utilizar se copia.

Identificador de la posición de memoria en Python

En Python podemos utilizar el método incorporado (Built-in) “id()” para que nos devuelva el identificador del objeto de la variable que le pasemos. Este número identificador que devuelve “id()” es la dirección del objeto en memoria (cada posición de memoria es única, por lo que se puede aprovechar y utilizar como identificador único, es lo que Python hace).

mi_texto = "un texto"

identificador = id(mi_texto)

print("Id de mi_texto: {}".format(identificador))

Imprimirá la posición de memoria que ocupe, en mi caso:

Id de mi_texto: 31818126

Más información de este método en: https://docs.python.org/3/library/functions.html#id

Ejemplo de código de Inmutable

Pruébalo tú mismo:

if __name__ == "__main__":
    mi_variable_inmutable = "algo, algo más"
    print("mi_variable_inmutable (id: {}): {}".format(id(mi_variable_inmutable), mi_variable_inmutable))

    def mi_funcion(var_de_func):
        print("var_de_func antes de retornar (id: {}): {}".format(id(var_de_func), var_de_func))
        var_de_func += ", añadido"
        print("var_de_func (id: {}): {}".format(id(var_de_func), var_de_func))
        return var_de_func

    retorno_de_funcion = mi_funcion(mi_variable_inmutable)
    print("retorno_de_funcion (id: {}): {}".format(id(retorno_de_funcion),retorno_de_funcion))

    print("mi_variable_inmutable después de función (id: {}): {}".format(id(mi_variable_inmutable), mi_variable_inmutable))

Imprime:

mi_variable_inmutable (id: 48154248): algo, algo más
var_de_func antes de retornar (id: 48154248): algo, algo más
var_de_func (id: 52186528): algo, algo más, añadido
retorno_de_funcion (id: 52186528): algo, algo más, añadido
mi_variable_inmutable después de función (id: 48356248): algo, algo más

Estudiando el resultado, sobre todo la dirección de memoria dada por “id()” vemos que efectivamente se han creado tres objetos:

  1. Al empezar el programa mi_variable_inmutable que se mantiene hasta el final
  2. Al pasarse como parámetro a var_de_func
  3. Al modificarse var_de_func (luego se devuelve este último en el “return”)

Mutables

Los mutables son objetos que, una vez creados, su estado puede cambiar en el futuro.

Los mutables son los más “complejos” de utilizar en programación (suelen ser las estructuras de datos como: dict, list, etc) y no solo porque son más complejos porque son estructuras que tienen cosas, sino que suelen liar con el tema de los punteros y, paradójicamente, son los que menos perjudican a la memoria (se escriben una sola vez y se reutilizan siempre). Hay que decir que los mutables están diseñados así aposta, porque copiar una estructura de datos entera (aunque se puede) tardaría mucho e implicaría utilizar mucha memoria para seguramente no aprovechar la copia (no es lo mismo copiar un objeto String que copiar una lista con millones de objetos String, para luego no haber necesitado la copia; llega a ser un derroche de procesador y de memoria).

Como hicimos antes con Imutables, ahora veremos un ejemplo de Mutable, concretamente un listado de valores String. Se guardará en un espacio de memoria (en la imagen “Id: 0”) la variable “mi_variable” que apunta a otra posición de memoria (en la imagen “Id: 1”) donde estará nuestro objeto Inmutable de tipo listado. Y para empezar inicializaremos el listado con dos Strings (en la imagen apunta a “Id: 90” y “Id: 91”).

Cuando se pasa el valor mutable como parámetro a una función (en la imagen a la variable “var_de_func” que está en la dirección de memoria “Id:2”), la variable apuntará al objeto mutable (en la imagen “Id: 1”), por tanto NO se copia. Aquí ya se pueden deducir algunos problemas (cuando se modifique el objeto dentro de la función se modificará fuera) y ventajas (no ocupa más memoria y la “copia” de un mutable gigante es inmediata).

Así, cuando se modifique el objeto mutable desde dentro de la función, se modificará para todos (dentro y fuera de la función). En este ejemplo añadiremos un nuevo valor String al listado.

Si imprimimos mi_variable desde fuera de la función comprobaremos que efectivamente “alguien” ha modificado nuestro objeto mutable.

Por otro lado, si imprimimos var_de_func dentro de la función, como apunta al mismo objeto, imprimirá lo mismo.

En resumen, un valor de tipo mutable se puede cambiar en un futuro una vez guardado en una posición de memoria y si se tiene que utilizar se utiliza el mismo objeto siempre apuntado desde diferentes variables.

Ejemplo de código de Mutable

Pruébalo tú mismo:

if __name__ == "__main__":
  def mi_funcion(var_de_func):
        print("var_de_func (id: {}): {}".format(id(var_de_func), var_de_func))
        var_de_func += ["añadido"]
        print("var_de_func antes de retornar (id: {}): {}".format(id(var_de_func), var_de_func))
        return var_de_func

    mi_variable_mutable = ["algo", "algo más"]
    print("mi_variable_mutable (id: {}): {}".format(id(mi_variable_mutable), mi_variable_mutable))

    retorno_de_funcion = mi_funcion(mi_variable_mutable)
    print("retorno_de_funcion (id: {}): {}".format(id(retorno_de_funcion), retorno_de_funcion))

    print("mi_variable_mutable después de función (id: {}):\t.\t{}".format(id(mi_variable_mutable), mi_variable_mutable))

Imprimirá la posición de memoria que ocupe, en mi caso:

mi_variable_mutable (id: 52183808): ['algo', 'algo más']
var_de_func (id: 52183808): ['algo', 'algo más']
var_de_func antes de retornar (id: 52183808): ['algo', 'algo más', 'añadido']
retorno_de_funcion (id: 52183808): ['algo', 'algo más', 'añadido']
mi_variable_mutable después de función (id: 52183808): ['algo', 'algo más', 'añadido']

Estudiando el resultado, sobre todo la dirección de memoria dada por “id()”, vemos que no se ha creado más objeto que el primero y todas las modificaciones se han realizado sobre el mismo objeto.

Copiar el objeto Mutable en otra dirección de memoria

Antes de nada, avisar que NO es hacer inmutable a un objeto mutable.

Es muy sencillo copiar un objeto mutable en otra dirección de memoria, simplemente utilizando su método constructor (para un listado «list()», para un diccionario «dict()», para un conjunto «set()») y pasando como parámetro al propio objeto mutable. Por ejemplo:

mi_variable_mutable = ["valor 1", "valor 2"]
print("mi_variable_mutable (id: {}): {}".format(id(mi_variable_mutable), mi_variable_mutable))

mi_variable_mutable_copiado = list(mi_variable_mutable)
print("mi_variable_mutable_copiado (id: {}): {}".format(id(mi_variable_mutable_copiado), mi_variable_mutable_copiado))

Al imprimir el resultado podemos ver como conteniendo lo mismo la dirección de memoria es diferente:

mi_variable_mutable (id: 48398336): ['valor 1', 'valor 2']
mi_variable_mutable_copiado (id: 51397696): ['valor 1', 'valor 2']

Tipos en Python clasificados en Inmutables y Mutables

Inmutables:

  • Strings (Texto)
  • Int (Número entero)
  • Float (Número de coma flotante)
  • Decimal (Número decimal)
  • Complex (Complejo)
  • Bool (Booleano)
  • Tuple (Tupla): Actúa como una lista inmutable siempre que sus elementos sean Resumibles (“Hashables”).
  • Frozenset (Conjunto congelado): Actúa como un conjunto inmutable siempre que sus elementos sean Resumibles (“Hashables”)
  • Bytes
  • Range (Rango)
  • None (Nulo)

Mutables:

  • List (Listado): Admite cualquier tipo de valores
  • Dict (Diccionario): Admite solo claves Resumibles (“Hashables”) y valores de cualquier tipo
  • Set (Conjunto): Admite solo valores Resumibles (“Hashables”)
  • Bytearray (array de bits): Entre otros usos, se puede utilizar como un String mutable.
  • MemoryView (Vista de la memoria): Referencia a objetos
  • Clase definida por el programador. Más información en el artículo sobre Hashable.

Más información de cada tipo en la documentación oficial en: https://docs.python.org/3/library/stdtypes.html

Y si quieres ampliar más tu conocimiento sobre Mutables e Inmutables, te recomiendo que eches un vistazo al artículo sobre Resumibles (Hashables).

Bibliografía

Comparte esta entrada en:
Safe Creative #1401310112503
Mutables e Inmutables por "www.jarroba.com" esta bajo una licencia Creative Commons
Reconocimiento-NoComercial-CompartirIgual 3.0 Unported License.
Creado a partir de la obra en www.jarroba.com

3 thoughts on “Mutables e Inmutables”

  1. Hola, muchas gracias por compartir esta información con todos. Me sirve mucho y aprendí bastante husmeando en esta página. Sigue así!

  2. Hola, no me ha quedado muy claro el concepto de que una clase puede ser inmutable si sobreescribimos el método __hash__, puesto que lo he intentado y aun puedo cambiar sus valores, eso no va con el concepto de inmutable (que no va a cambiar nunca a lo largo de la ejecución del código), ademas he comparado su comportamiento con una clase normal y se comportan igual, también he podido usar ambas como llaves de diccionarios.

    1. Hola Fernando.

      Por defecto una clase que hereda de Object es hashable, por tanto inmutable (porque hereda un método __hash__ ya escrito por defecto que toma el valor de id() ). Por esta razón, tanto si sobrescribes __hash__ de tu clase personalizada como si no puedes utilizar el objeto como clave de diccionario.

      Y gracias por la observación, pues hay una errata en el artículo en la parte de los tipos, voy a cambiarlo para que sea correcto.

      Más información en https://docs.python.org/3/glossary.html#term-hashable
      Y en https://jarroba.com/resumibles-hashables-en-python/

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies

ACEPTAR
Aviso de cookies