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.

Python: Conjuntos inmutables con frozenset

Objetivo: presentar el tipo frozenset para la implementación de conjuntos inmutables.

Al igual que las listas encuentran en las tuplas su versión inmutable, los conjuntos, que como vimos quedaban representados por el tipo set, hallan su inalterabilidad a través del tipo frozenset.

Podríamos traducir frozenset por algo así como conjunto congelado, término que muestra claramente su carácter estático. Podemos pensar en un frozenset como en un set en el que no podemos modificar su composición una vez creado.

Veamos cómo creamos un frozenset:

>>> c1 = frozenset({'pradera', 2, 'pimiento'})
>>> c1
frozenset({'pimiento', 2, 'pradera'})

Presta mucha atención: empleamos el constructor frozenset, facilitando, como argumento, entre paréntesis, un conjunto definido explicitamente.

En general, el argumento no tiene por qué ser un conjunto, sino que sirve cualquier tipo de objeto iterable, como una lista. Más adelante hablaremos formalmente de qué significa exactamente eso de ser iterable, pero por el momento quédate con que un iterable es cualquier objeto que puede ser recorrido de principio a fin dentro de un bucle for.

Por lo tanto, la definición anterior también podrías haberla hecho así:

>>> c1 = frozenset(['pradera', 2, 'pimiento'])
>>> c1
frozenset({'pimiento', 2, 'pradera'})

El resultado, como ves, es el mismo empleando corchetes (listas) que llaves (conjuntos). O incluso si facilitas unta tupla como argumento:

>>> c1 = frozenset(('pradera', 2, 'pimiento'))
>>> c1
frozenset({'pimiento', 2, 'pradera'})

Date cuenta de que, aunque estemos facilitando varios datos a través de un conjunto, lista o tupla, en realidad dentro de frozenset hay un único argumento. El siguiente código sería erróneo:

>>> frozenset('pimiento', 2, 'pradera')
Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    frozenset('pimiento', 2, 'pradera')
TypeError: frozenset expected at most 1 arguments, got 3

ya que hemos introducido tres argumentos y, como informa el código de error, cómo máximo admite uno.

En efecto: como máximo un argumento. O, lo que es lo mismo, uno o ninguno.

>>> c2 = frozenset()
>>> c2
frozenset()

Como vemos, cuando no facilitamos ningún argumento, creamos un frozenset vacío.

Las cadenas de caracteres también son iterables, pues pueden ser recorridas de principio a fin con un bucle for. Por lo tanto, también nos pueden servir para generar un frozenset:

>>> c3 = frozenset('pradera')
>>> c3
frozenset({'a', 'p', 'r', 'e', 'd'})

Observa qué ha pasado: se ha descompuesto la cadena en sus caracteres individuales, eliminando las repeticiones. Esto es algo natural, ya que los conjuntos no pueden tener elementos repetidos, como vimos en los últimos artículos.

Esta manera de crear un frozenset a través de su constructor también es válida para el tipo set.

>>> c4 = set('la casa de la pradera')
>>> c4
{'a', ' ', 'c', 'e', 'd', 'l', 'p', 's', 'r'}

Una forma muy rápida de crear un conjunto a partir de un string, como podemos ver.

Set y frozenset son hermanos, por lo que puedes emplear muchos de los operadores y métodos que ya aprediste con set. Realizemos, por ejemplo, una unión:

>>> c1 = frozenset({1, 2, 3})
>>> c2 = frozenset({4, 5, 6})
>>> c1 | c2
frozenset({1, 2, 3, 4, 5, 6})

En cambio, ya que los frozenset son inmutables y no podemos modificarlos, no aceptan aquellos métodos que traten de alterar su composición. Intentemos agregar un nuevo elemento a c1 y veamos qué sucede:

>>> c1.add(5)
Traceback (most recent call last):
  File "<pyshell#20>", line 1, in <module>
    c1.add(5)
AttributeError: 'frozenset' object has no attribute 'add'

El mensaje es rotundo: el objeto frozenset no tiene método add.

¿Por qué usar un frozenset en lugar de un set? Una razón importante puede ser para garantizar que nuestro código no alterará una estructura de datos que no debamos modificar. Además, como veremos en breve cuando comencemos con los diccionarios, las claves de estos han de ser de un tipo inmutable, como son los strings, tuplas o frozensets, pero no servirían ni las listas ni los sets, que son modificables.

El tema de la mutabilidad en Python puede resultar un tanto misterioso si estás habituado a otros lenguajes de programación. Merecerá que se le dedique un artículo monográfico…

Javier Montero Gabarró


Python: Conjuntos inmutables con frozenset


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.

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, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies