Python Language
Mutable vs Inmutable (y Hashable) en Python
Buscar..
Mutable vs inmutable
Hay dos tipos de tipos en Python. Tipos inmutables y tipos mutables.
Inmutables
Un objeto de un tipo inmutable no puede ser cambiado. Cualquier intento de modificar el objeto dará lugar a que se cree una copia.
Esta categoría incluye: enteros, flotadores, complejos, cadenas, bytes, tuplas, rangos y conjuntos de imágenes.
Para resaltar esta propiedad, vamos a jugar con el id
incorporado. Esta función devuelve el identificador único del objeto pasado como parámetro. Si el id es el mismo, este es el mismo objeto. Si cambia, entonces este es otro objeto. (Algunos dicen que esta es realmente la dirección de memoria del objeto, pero ten cuidado con ellos, son del lado oscuro de la fuerza ...)
>>> a = 1
>>> id(a)
140128142243264
>>> a += 2
>>> a
3
>>> id(a)
140128142243328
Está bien, 1 no es 3 ... Noticias de última hora ... Tal vez no. Sin embargo, este comportamiento a menudo se olvida cuando se trata de tipos más complejos, especialmente de cadenas.
>>> stack = "Overflow"
>>> stack
'Overflow'
>>> id(stack)
140128123955504
>>> stack += " rocks!"
>>> stack
'Overflow rocks!'
Jajaja ¿Ver? ¡Podemos modificarlo!
>>> id(stack)
140128123911472
No. Si bien parece que podemos cambiar la cadena nombrada por la stack
variables, lo que realmente hacemos es crear un nuevo objeto para contener el resultado de la concatenación. Nos engañan porque en el proceso, el objeto antiguo no va a ninguna parte, por lo que se destruye. En otra situación, eso habría sido más obvio:
>>> stack = "Stack"
>>> stackoverflow = stack + "Overflow"
>>> id(stack)
140128069348184
>>> id(stackoverflow)
140128123911480
En este caso, está claro que si queremos conservar la primera cadena, necesitamos una copia. ¿Pero es eso tan obvio para otros tipos?
Ejercicio
Ahora, sabiendo cómo funcionan los tipos inmutables, ¿qué diría usted con el siguiente código? ¿Es sabio?
s = ""
for i in range(1, 1000):
s += str(i)
s += ","
Mutables
Un objeto de un tipo mutable se puede cambiar y se cambia in situ . No se realizan copias implícitas.
Esta categoría incluye: listas, diccionarios, bytearrays y sets.
Sigamos jugando con nuestra pequeña función de id
.
>>> b = bytearray(b'Stack')
>>> b
bytearray(b'Stack')
>>> b = bytearray(b'Stack')
>>> id(b)
140128030688288
>>> b += b'Overflow'
>>> b
bytearray(b'StackOverflow')
>>> id(b)
140128030688288
(Como nota al margen, uso bytes que contienen datos ASCII para aclarar mi punto, pero recuerde que los bytes no están diseñados para contener datos textuales. Que la fuerza me perdone).
¿Que tenemos? Creamos un bytearray, lo modificamos y usando el id
, podemos asegurarnos de que este es el mismo objeto, modificado. No es una copia de eso.
Por supuesto, si un objeto se va a modificar con frecuencia, un tipo mutable hace un trabajo mucho mejor que un tipo inmutable. Desafortunadamente, la realidad de esta propiedad a menudo se olvida cuando más duele.
>>> c = b
>>> c += b' rocks!'
>>> c
bytearray(b'StackOverflow rocks!')
Bueno...
>>> b
bytearray(b'StackOverflow rocks!')
Waiiit un segundo ...
>>> id(c) == id(b)
True
En efecto. c
no es una copia de b
. c
es b
.
Ejercicio
Ahora que entiendes mejor qué efecto secundario implica un tipo mutable, ¿puedes explicar qué está mal en este ejemplo?
>>> ll = [ [] ]*4 # Create a list of 4 lists to contain our results
>>> ll
[[], [], [], []]
>>> ll[0].append(23) # Add result 23 to first list
>>> ll
[[23], [23], [23], [23]]
>>> # Oops...
Mutables e inmutables como argumentos
Uno de los principales casos de uso cuando un desarrollador necesita tener en cuenta la mutabilidad es cuando pasa argumentos a una función. Esto es muy importante, ya que esto determinará la capacidad de la función para modificar objetos que no pertenecen a su alcance, o en otras palabras, si la función tiene efectos secundarios. Esto también es importante para comprender dónde debe estar disponible el resultado de una función.
>>> def list_add3(lin):
lin += [3]
return lin
>>> a = [1, 2, 3]
>>> b = list_add3(a)
>>> b
[1, 2, 3, 3]
>>> a
[1, 2, 3, 3]
Aquí, el error es pensar que lin
, como parámetro de la función, puede modificarse localmente. En su lugar, lin
y a
referencia del mismo objeto. Como este objeto es mutable, la modificación se realiza in situ, lo que significa que el objeto al que hacen referencia tanto lin
como a
se modifica. No es necesario devolver lin
, porque ya tenemos una referencia a este objeto en forma de a
. a
y b
terminan haciendo referencia al mismo objeto.
Esto no es lo mismo para las tuplas.
>>> def tuple_add3(tin):
tin += (3,)
return tin
>>> a = (1, 2, 3)
>>> b = tuple_add3(a)
>>> b
(1, 2, 3, 3)
>>> a
(1, 2, 3)
Al comienzo de la función, tin
y a
referencia del mismo objeto. Pero este es un objeto inmutable. Así que cuando la función intenta modificarla, tin
recibir un nuevo objeto con la modificación, mientras que a
mantiene una referencia al objeto original. En este caso, devolver tin
es obligatorio, o el nuevo objeto se perdería.
Ejercicio
>>> def yoda(prologue, sentence):
sentence.reverse()
prologue += " ".join(sentence)
return prologue
>>> focused = ["You must", "stay focused"]
>>> saying = "Yoda said: "
>>> yoda_sentence = yoda(saying, focused)
Nota: el reverse
opera en el lugar.
¿Qué opinas de esta función? ¿Tiene efectos secundarios? ¿Es necesaria la devolución? Después de la llamada, ¿cuál es el valor de saying
? De focused
? ¿Qué sucede si se vuelve a llamar a la función con los mismos parámetros?