Python: El sagrado misterio de la inmutabilidad

Objetivo: comprender los conceptos críticos de mutabilidad, inmutabilidad y referencia en Python.

Estás ante un artículo esencial en tu formación como programador en Python. Te llevará sólo cinco minutos leerlo, pero puede dar luz sobre conceptos que no resultan ni mucho menos obvios cuando se comienza con Python, particularmente si procedes de otros lenguajes de programación.

Por lo general, cuando empezamos a estudiar un lenguaje de programación, se nos cuenta que existen distintos tipos de datos, tales como numéricos, strings, etc.

En Python se nos enseña algo más: determinados tipos son inmutables y otros mutables. Los números, los strings y las tuplas son inmutables. Por el contrario, las listas son mutables.

Ahí ya empiezan a sacudirnos algo; pero nuestro cerebro, al que no le gustan las dudas, intenta buscar una respuesta coherente para no quedarse atascado y seguir avanzando. Claro: los números, los strings y las tuplas no se pueden modificar, mientras que las listas .

Bueno, bueno, un momento:

>>> a = 5
>>> b = 'pradera'

La variable a contiene un número; la variable b contiene un string. Hemos dicho que los números y los strings son inmutables. ¿Significa eso que no podemos modificar ni a ni b?

>>> a = 6
>>> b = 'casa'
>>> a
6
>>> b
'casa'

Pues sí, se pueden modificar (de no ser así Python tendría un serio problema), por lo que eso de la inmutabilidad debe de significar algo diferente. Decir que un tupla es inmutable, pero una lista no, ¿significará acaso que ésta tiene cosquillas pero la otra no?

Más adelante cuando estudiamos las listas y las tuplas creemos haber comprendido, al fin, el misterio.

>>> lista = [1, 2, 3, 4]
>>> lista[0] = 5
>>> lista
[5, 2, 3, 4]

Bien: las listas, mutables, se pueden modificar.

>>> tupla = (1, 2, 3, 4)
>>> tupla[0] = 5
Traceback (most recent call last):
  File "<pyshell#55>", line 1, in <module>
    tupla[0] = 5
TypeError: 'tuple' object does not support item assignment

En cambio, las tuplas, inmutables, no, como podemos comprobar.

Aprendemos también que los strings también son secuencias, como las listas y las tuplas y, como tales, podemos acceder a sus caracteres individuales:

>>> serie = 'la casa de la pradera'
>>> serie[3]
'c'

Pero no podemos modificarlos:

>>> serie[3] = 'k'
Traceback (most recent call last):
  File "<pyshell#60>", line 1, in <module>
    serie[3] = 'k'
TypeError: 'str' object does not support item assignment

Un error semejante al que recibimos al tratar de modificar una tupla. Esto hace que cuadre todo: los strings son inmutables, también.

Pero ¿cuadra realmente todo? Parece que sí, aunque no aparenta tener mucho sentido práctico eso de la inmutabilidad de los números.

Tarde o temprano, cualquier estudiante de Python que ya ha programado en otros lenguajes se enfrenta al siguiente misterio.

Observémoslo muy despacio:

>>> x = 5
>>> y = x

Pregunta: ¿cuánto crees que vale y?

En efecto, 5:

>>> y
5

Modifiquemos ahora el valor de x:

>>> x = 6

¿Cuánto crees que valdrá ahora y?

Vamos a ver, no me asustes: x e y son dos variables diferentes, cada una con su propio contenido; por lo tanto y tiene que seguir valiendo 5, aunque hayas modificado x, ¿no?

En efecto, así es:

>>> y
5

Veamos otro ejemplo. Ahora x es una lista:

>>> x = [1, 2, 3, 4]
>>> y = x
>>> y
[1, 2, 3, 4]

Voy a modificar el valor de x:

>>> x[0] = 5
>>> x
[5, 2, 3, 4]

¿Cuánto crees que valdrá ahora y?

Por la lógica anterior, x e y son dos variables diferentes, cada una con su propio contenido, por lo que cabría esperar que el valor de y fuera [1, 2, 3, 4].

Pero no es así:

>>> y
[5, 2, 3, 4]

Esto rompe completamente nuestros esquemas. Si somos programadores en otros lenguajes una alarma nos sacude inmediatamente.

¿Por qué al modificar x ha cambiado y también?

De acuerdo: los números son inmutables, pero las listas no. ¿Pero qué tiene que ver esto con el hecho de que se haya modificado el valor de y?

Para resolver el misterio hace falta un una nueva pieza clave: el concepto de referencia.

Veamos lo que está sucediendo realmente entre bastidores:

>>> x = 5

En Python, todo son objetos. Se acaba de crear uno nuevo: un número de valor 5.

Este objeto, que reside en algún lugar de la memoria, posee un identificador único que lo diferencia del resto de los objetos. Piensa en él como si fuera su DNI (es, en realidad, la dirección de memoria en la que reside el objeto).

Para conocer ese identificador, Python dispone de la función id():

>>> id(5)
137396064

Y ahora el concepto crítico: la variable x no es más que una referencia a ese objeto. A efectos prácticos, es como si se tratara de un puntero (empleando conceptos de otros lenguajes) a ese objeto. No contiene su valor, sino una referencia a él.

La función id(), aplicada a una variable, nos devuelve el id del objeto al que referencia, por lo que obtenemos el mismo valor que antes:

>>> id(x)
137396064

Proseguimos; asignamos a la variable y el valor de x:

>>> y = x

Observa ahora cuidadosamente qué pasa al preguntar por el id del objeto al que referencia y:

>>> id(y)
137396064

Exactamente el mismo al que referencia x. Tanto x como y apuntan al mismo objeto.

Este es un concepto radicalmente distinto al de las variables en otros lenguajes, en los que el contenido de x y de y estaría en diferentes direcciones de memoria.

A continuación, modificamos x:

>>> x = 6
>>> id(x)
137396080

Se ha creado un nuevo objeto, de valor 6, y ahora x apunta a ese valor. Esto también es muy diferente a lo que sucedería en otros lenguajes: la variable x seguiría en la misma dirección de memoria, habiendo modificado únicamente su contenido.

Fijémonos que, como es de esperar, y sigue apuntando al objeto primero:

>>> id(y)
137396064
>>> y
5

Analicemos ahora qué sucede en el caso de las listas:

>>> x = [1, 2, 3, 4]
>>> y = x
>>> id(x)
170964396
>>> id(y)
170964396

De momento, todo es exactamente igual que antes: se ha creado un único objeto, la lista [1, 2, 3, 4], que está referenciado por dos variables, x e y.

Si ahora modificamos directamente el objeto (al ser mutable, podemos hacerlo):

>>> x[0] = 5
>>> id(x)
170964396
>>> id(y)
170964396

comprobamos que tanto x como y siguen siendo referencias al mismo objeto.

Por lo tanto y tiene el mismo valor que x:

>>> y
[5, 2, 3, 4]

En el ejemplo anterior, cuando hicimos x = 6, se creó un nuevo objeto. En el caso de la lista, hemos modificado in situ el propio objeto, aprovechándonos de su mutabilidad. Es como si un cirujano hubiese realizado una operación sobre el objeto.

La situación habría sido muy distinta si hubiéramos hecho esto otro:

>>> x = [1, 2, 3, 4]
>>> y = x
>>> x = [5, 2, 3, 4]
>>> y
[1, 2, 3, 4]
>>> id(x)
170489580
>>> id(y)
170791756

Fíjate en la importante diferencia: al hacer x = [5, 2, 3, 4] hemos creado un objeto diferente, de modo que x e y apuntan a objetos distintos y por eso ahora sus valores difieren. En el caso anterior, al hacer x[0] = 5 modificamos directamente el mismo objeto y no se creó otro nuevo.

Espero que todo resulte más claro ahora y entiendas la diferencia esencial que hay en lo que respecta al concepto de variable respecto a otros lenguajes de programación. Si es así, felicítate, pues habrás cambiado de nivel en tu proceso de aprendizaje de Python.

Javier Montero Gabarró


http://elclubdelautodidacta.es/wp/2012/09/python-el-sagrado-misterio-de-la-inmutabilidad/


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

28 opiniones en “Python: El sagrado misterio de la inmutabilidad”

  1. Uf, me estaba volviendo loco con una función que me modificaba el valor de una lista. Gracias mil, soy nuevo en esto. Un saludo

  2. Continuando con tu ejemplo: si volvemos asignar a x [1, 2, 3, 4] Python crea otro objeto

    ..
    >>> x = [1, 2, 3, 4]
    >>> id(x) == id(y)
    False
    >>> for i in range(4): id(x[i]) == id(y[i])

    True
    True
    True
    True

    Tenemos dos listas distintas cuyo contenido es el mismo

  3. Muchisímas gracias, está tremendamente explicado, perfectamente entendible.

    En otras web explican sólo la declaración de listas, tuplas y demás, aquí entendi parte de la filosofía python… 🙂

    1. Me temo que Python no está pensado para trabajar con punteros directamente, de modo que no es posible. Es una cuestión de diseño.
      Si realmente tienes interés en hacerlo, echa un vistazo al módulo ctypes, que proporciona medios para comunicar Python y C (siempre y cuando estés usando una implementación de Python basada en C, desde luego).

  4. hola Javier. Muy buenos tus artículos. Hace 2 días me tome el placer de pasarlos a un Word, imprimirlos y estudiarlos complementariamente a otros apuntes de PYTHON. Poco a poco cada vez avanzo mas. Pero donde mas me atasco es a la hora de reutilizar variables, y creo q puede ser por el tema q acabas de exponer…
    ¿Me echas una mano con este ejemplo simple?

    def muestra_a():
    a=100
    print(a)

    b=muestra_a()
    print(b)

    el resultado es un “None”.¿Porque?¿Porque no me deja reutilizar la variable b?

    Gracias. Espero que tu respuesta sirva también para muchos 🙂

    1. Me alegro de que te sean de utilidad, Fer!
      El problema en tu código es que la función muestra_a() NO devuelve ningún valor. Añade simplemente:
      return a
      y b recibirá ese valor.
      Saludos

  5. Este fue el primero de los artículos de esta serie que cayo en mis manos y uno de mis primerísimos en mi acercamiento a Python ¡Afortunada doblemente! Pasa el tiempo y reparece de cuando en cuando en mi semiconsciente para ser rumiado. Y….

    El hábito no hace al fraile, pero… condiciona su comportamiento. El nombre que demos a las cosas condiciona tremendamente nuestra visión sobre ellas. Javier titula “el misterio de la inmutabilidad”
    En todas las fuentes de Python se habla de “mutable/inmutable” ¿es esta denominación adecuada? Siento que “el misterio” hubiera resultado menos estridente si se hubiera clasificado a los constructores de tipo como “estáticos/dinámicos”, al menos para los programadores que se acercan a este lenguaje procedentes de otros.
    Resulta anacrónico “variable inmutable”, redundante “variable mutable”. Intuitivo que una “variable dinámica” puede cambiar de tamaño y de valor, y la asocio a una referencia. Variable estática me sugiere que tiene algunas limitaciones en cuanto a la variabilidad (al menos en cuanto a tamaño)
    ¿Luchar contra el entorno? ¿cambiar la denominación aceptada generalmente? No digo que sea fácil, pero creo que vale la pena intentarlo. Los que pretendemos ayudar a otros a aprender, tenemos la responsabilidad de facilitar la digestión. Uno de los principales mecanismos de aprendizaje es apreciando diferencia/semejanza que conduce a la clasificación.
    ¿No llama vuestra atención lista/tupla y sin embargo set/frozenset? ¿porqué no frozenlista?
    Mas difícil cambiar la denominación “frozen” que la de inmutable, pues forma parte de la sintaxis del lenguaje. Propongo entonces llamarles constructores de tipos dinámicos/congelados. Inmediatamente surge ¿frozendict?, ya, ya se que están descartados http://legacy.python.org/dev/peps/pep-0416/, pero es un eslabón perdido en mi mapa conceptual y quiero saber el motivo de su ausencia.

    El tema arrastra mi curiosidad hacia la estructuración del lenguaje ¿es cadena una clase heredera de tupla? y la gestión interna de la memoria en Python.

    Javier: ¿me diriges a un sitio en que la expliquen claramente?

    Un abrazo

    1. De acuerdo completamente con lo de que el nombre condiciona nuestra visión. No obstante, a mí no me parecen desafortunados los términos mutable e inmutable para designar objetos que no pueden ser modificados después de su creación. Java, de hecho, emplea la misma terminología.
      Ojo con lo de “variable dinámica”. En Python todas lo son, independientemente de que referencien un objeto mutable o inmutable. Cualquier variable puede modificar dinámicamente no sólo su valor, sino el tipo al que referencia. El concepto de “mutabilidad no va ligado al de “variable”.
      Con lo de frozen (el otro día vi la película, me encantó), creo que se eligió la palabra frozenset porque no había nada mejor. Tupla y lista me parecen dos términos perfectos para dos conceptos que, aunque aparentemente semejantes, muy diferentes. En matemáticas el término tupla es muy usado y su notación es la misma que las tuplas de Python. No podría imaginarme nunca una frozenlist en lugar de tupla. Por otro lado, un frozenset es un artilugio verdaderemente congelado que es plenamente merecedor de ese calificativo, pues no sólo el contenedor es inmutable, sino también su contenido recursivamente. En una tupla sólo el contenedor es estrictamente inmutable, los objetos de que consta no tienen por qué serlo.
      El tema de los diccionarios inmutables tiene sus detractores y defensores, como ya has comprobado…
      No, las cadenas no se heredan de las tuplas, son tipos built-in completamente independientes (prueba a hacer un issubclass(str, tuple).
      Leer las PEPS te enseñará mucho sobre las interioridades de Python. Pero tarde o temprano tendrás que enfrentarte con el API de C si realmente quieres comprender los detalles más íntimos (en el supuesto de que uses CPython, la implementación más común) o incluso el propio código fuente.
      Pero no olvides que ante todo, si algo caracteriza a Python, diferenciándolo de la mayoría de los otros lenguajes, es su pragmatismo. Y ese es, en mi opinión, el verdadero valor a inculcar a los futuros programadores en Python.
      Un abrazo!

  6. Hola, me ha quedado una duda en la parte en la que explicas la inmutabilidad de los números. Dices:

    >>>x = 5
    >>> id(x)
    137396064
    >>>y = x
    >>> id(y)
    137396064 #misma referencia, mismo objeto
    >>>x = 6
    >>> id(x)
    137396080
    “””Entiendo que se crea un nuevo objeto con una referencia distinta a la que anteriormente tenía la variable ‘x’. Y dices a continuación: “la variable x seguiría en la misma dirección de memoria, habiendo modificado únicamente su contenido.”, mi pregunta es: ¿en qué dirección de memoria está el objeto refenciado por ‘y’?. “””

    Variable Referencia Objeto Valor Dir. memoria
    x 137396064 Obj1 5 dir.mem.obj1
    y 137396064 Obj1 5 ¿ dir.mem.obj1 ? *
    x 137396080 Obj2 6 dir.mem.obj.1

    *Para que haya inmutabilidad entiendo que deben ser objetos distintos y ocupar distinto espacio de memoria.

    Saludos.

    1. Qué tal, Ulises:
      El texto dice que, en otros lenguajes, “la variable x seguiría en la misma dirección de memoria, habiendo únicamente modificado su contenido”. En Python, por el contrario, no es así: x ahora referencia otra dirección de memoria.
      La variable y continúa referenciando la dirección antigua que tenía x: 137396064, mientras que ahora x se encuentra en la dirección 137396080.
      El valor devuelto por id() es precisamente la dirección de memoria (en CPython, la distribución más común de Python).
      Saludos

  7. Hola Javier,

    te felicito por tu gran explicación. La verdad que tuve la suerte de caer aquí rápido, si no, creo que me hubiera vuelto completamente loco. Se me queda una duda en el tintero.

    Supongamos lo siguiente:

    x=6

    id(6)=27898080
    id(x)=27898080

    ¿exactamente, que es lo que hace la función id()?

    – En el caso de id(6) me pasa su dirección en memoria ?
    – En el caso de id(x) me pasa la referencia hacia la que apunta el objeto x ?

    Un saludo y de nuevo muchas gracias.

    1. Buenas tardes, Sergio:

      Comprender bien el concepto inmutabilidad es, en efecto, clave para entender cómo funciona Python.

      A la duda:

      La función id() devuelve una “dirección” única asociada al objeto. Es la butaca en el cine en la que se sienta el objeto. En la implementación CPython, la más usual, es precisamente la dirección de memoria en la que se localiza el objeto, pero no necesariamente tendría que ser así y otras implementaciones eligen otros esquemas.
      id(6) te devuelve la “dirección” (de memoria en CPython) en la que se encuentra el objeto “6”. id(x) te devuelve la “dirección” (de memoria en CPython) en la que se encuentra el objeto referenciado por el identificador x. Obviamente es la misma dirección, puesto que x está “apuntando” al objeto “6” tras la instrucción de asignación.
      Saludos

      1. Buenas de nuevo Javier,

        antes que nada disculpa la demora en mi contestación, pero he estado muy ocupado y quería tomarme un tiempo para escribirte adecuadamente.

        Con tu contestación me formo el siguiente esquema de funcionamiento (hablando siempre de CPython)(siguiendo con el ejemplo de “x=6”):

        – id(6) devuelve la dirección de memoria de él mismo, la butaca del cine sobre la cual éste se sienta (por cierto, excelente ejemplo).

        – id(x) devuelve la dirección de memoria del objeto al que hace referencia, en este caso de 6.

        me surge entonces la pregunta, ¿tiene sentido hablar de la posición de memoria de la misma variable “x”?

        un cordial saludo y gracias

        1. Qué tal, Sergio:

          No, eso que dices no tiene ningún sentido en Python. Los detalles íntimos de cómo gestiona Python los espacios de nombres no tienen utilidad para el programador.

          Saludos

  8. Los designios del señor son insondeables….!!!

    Hola, Comencé a hacer algunos experimentos, solo por curiosidad… jejeje:

    y = 44
    y = 1+y
    print(y)
    print(id(y))
    print(id(45))
    print(y is 45)
    if y is 45:
    print(“son el mismo objeto”)
    else:
    print(“son objetos diferentes”)
    if (y==45):
    print(“valores iguales”)
    else:
    print(“valores diferentes”)

    regresa:

    45
    1477440656
    1477440656
    True
    son el mismo objeto
    valores iguales

    que va de acuerdo con el asunto de que un numero entero es inmutable. Pero…

    En el caso de los números de punto flotante el panorama es muy distinto!

    y = 44.0
    y = 1.0+y
    print(y)
    print(id(y))
    print(id(45.0))
    if y is 45.0:
    print(“son el mismo objeto”)
    else:
    print(“son objetos diferentes”)

    if (y==45.0):
    print(“valores iguales”)
    else:
    print(“valores diferentes”)

    regresa:

    45.0
    164347976
    164348192
    son objetos diferentes
    valores iguales

    En el campo de los numero enteros python un numero 45 es un único objeto, como lo demuestra:

    a = 45
    b = 45
    print(id(a),’ ‘,id(b),’ ‘,id(45))

    que imprime:

    1477440656 1477440656 1477440656

    Sin embargo en el caso de números de punto flotante su representación es imprecisa por naturaleza, por lo que python genera distintos objetos aunque desde nuestro punto de vista se trata del mismo numero y por tanto deberían ser el mismo objeto:

    a = 45.0
    b = 45.0
    print(id(a),’ ‘,id(b),’ ‘,id(45.0))

    164347976 164348072 164348096

    aunque la asignacion

    c = a

    asigna a c una referencia al objeto referenciado por a:

    a = 45.0
    b = 45.0
    c = b
    print(id(a),’ ‘,id(b),’ ‘,id(c),’ ‘,id(45.0))

    164348072 159190928 159190928 164347976

    wowwwwww corolario: si camina como pato, grazna como pato, vuela como pato… posiblemente no sea un pato …. jejeje

    1. Qué tal, Gabriel:
      Pues para que alucines aún más, prueba el siguiente ejemplo:
      >>> a = 256
      >>> b = 256
      >>> id(a)
      501434112
      >>> id(b)
      501434112
      Esto es lo que esperas, desde luego. Pero prueba a utilizar un número mayor que 256:
      >>> a = 257
      >>> b = 257
      >>> id(a)
      38248384
      >>> id(b)
      38785712
      ¡Sorpresa!
      El id coincide para un conjunto limitado de enteros. Python, por comodidad, considera que ese conjunto de valores son los más frecuentemente usados y se ahorra regenerar un nuevo objeto cada vez que aparece de nuevo. En el caso de los float no se molesta ni siquiera en intentarlo, dada la virtual infinitud de valores entre cualquier rango.
      Saludos

Deja un comentario