Skip to content

Instantly share code, notes, and snippets.

@chemacortes
Last active December 15, 2015 07:39
Show Gist options
  • Save chemacortes/5224623 to your computer and use it in GitHub Desktop.
Save chemacortes/5224623 to your computer and use it in GitHub Desktop.
¿Tiene sentido tener "duplicados" algunos tipos en sus dos versiones, mutable e inmutable?
Display the source blob
Display the rendered blob
Raw
{
"metadata": {
"name": "Mutable o inmutable"
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<style>\n",
".rendered_html { font-size: 110%; font-family: \"DejaVu Sans Mono Book\",monospace; }\n",
"</style>"
]
},
{
"cell_type": "heading",
"level": 1,
"metadata": {},
"source": [
"Mutable o inmutable, he ah\u00ed el dilema"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Quien se enfrenta a la documentaci\u00f3n de python por primera vez se pregunta porqu\u00e9 esa insistencia en mantener tipos de datos duplicados en versiones mutables e inmutables. Tenemos listas y tuplas que casi hacen lo mismo. En python3, tenemos el tipo inmutable `bytes` y el mutable `bytearray`. \u00bfQu\u00e9 sentido tiene tener *\"duplicados\"* algunos tipos en sus dos versiones? La \u00fanica explicaci\u00f3n que se puede encontrar en la documentaci\u00f3n es que los tipos inmutables son m\u00e1s apropiados para usarlos como \u00edndices en diccionarios. No parece mucha ventaja para la complejidad que aporta.\n",
"\n",
"En este art\u00edculo veremos qu\u00e9 implica la *mutabilidad* de un tipo de dato y en qu\u00e9 puede sernos \u00fatil usar un tipo mutable u otro inmutable."
]
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"\u00bfQu\u00e9 es lo que cambia?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Antes de explicar nada, veamos si somos capaces de saber qu\u00e9 est\u00e1 cambiando. Veamos dos c\u00f3digos muy similares:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
">>> a=(1,2,3,4)\n",
">>> a+=(5,6,7)\n",
">>> print(a)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"(1, 2, 3, 4, 5, 6, 7)\n"
]
}
],
"prompt_number": 36
},
{
"cell_type": "code",
"collapsed": false,
"input": [
">>> a=[1,2,3,4]\n",
">>> a+=[5,6,7]\n",
">>> print( a )"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"[1, 2, 3, 4, 5, 6, 7]\n"
]
}
],
"prompt_number": 37
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Parece que ambos c\u00f3digos hagan lo mismo: a\u00f1adir un fragmento, en sus versiones tupla y lista, respectivamente. Vamos a analizarlo mejor. Para saber qu\u00e9 pasa, usemos la funci\u00f3n `id()`. Esta funci\u00f3n devuelve un identificador de un objeto de tal modo que si dos objetos tienen el mismo identificador, entonces son el mismo objeto."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
">>> a=(1,2,3,4)\n",
">>> print(id(a))\n",
">>> a+=(5,6,7)\n",
">>> print(id(a))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"192021604\n",
"189519828\n"
]
}
],
"prompt_number": 38
},
{
"cell_type": "code",
"collapsed": false,
"input": [
">>> a=[1,2,3,4]\n",
">>> print(id(a))\n",
">>> a+=[5,6,7]\n",
">>> print(id(a))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"189780876\n",
"189780876\n"
]
}
],
"prompt_number": 39
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"En la versi\u00f3n tupla, se ha creado una nueva tupla para realizar la operaci\u00f3n, mientras que en la versi\u00f3n lista se ha usado la misma lista, modific\u00e1ndose con el resultado. Si cambiamos el operador `+=` por una versi\u00f3n m\u00e1s expl\u00edcita tal vez se vea mejor:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
">>> a=(1,2,3,4)\n",
">>> a=a+(5,6,7)"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 40
},
{
"cell_type": "code",
"collapsed": false,
"input": [
">>> a=[1,2,3,4]\n",
">>> a.extend([5,6,7])"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 41
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Al operar con tuplas, los operandos no cambian de valor, cre\u00e1ndose una nueva tupla como resultado de la operaci\u00f3n. Podr\u00edamos sustituir toda la operaci\u00f3n por el resultado final y el c\u00f3digo funcionar\u00eda igual. En el caso de las listas, la lista se modifica *\"in situ\"* durante la operaci\u00f3n. En estos casos, cambiar la expresi\u00f3n por el resultado final no garantiza que el programa funcione igual. Se necesita pasar por todos y cada uno de los estados intermedios para asegurar que todo funcione igual.\n",
"\n",
"Esta propiedad de poder cambiar una expresi\u00f3n por su resultado final es conocida por [Transparencia referencial][1] en programaci\u00f3n funcional. Por lo general, los tipos inmutables se adec\u00faan mejor a operaciones de c\u00e1lculo donde el resultado final depende \u00fanicamente de los argumentos de entrada. Por otro lado, los tipos mutables son \u00fatiles para salvaguardar estados intermedios necesarios para la toma de decisiones durante la ejecuci\u00f3n de un programa.\n",
"\n",
"Por lo general, se saber elegir un tipo mutable o su hom\u00f3logo inmutable es todo un arte. Ante la duda, los tipos inmutables son m\u00e1s f\u00e1ciles de rastrear. As\u00ed mismo, veremos en pr\u00f3ximos art\u00edculos que los tipos inmutables ayudan bastante en programaci\u00f3n concurrente, por si est\u00e1s pensando en programaci\u00f3n multiproceso.\n",
"\n",
"[1]: http://en.wikipedia.org/wiki/Referential_transparency_(computer_science) \"Referential Transparency\""
]
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Ejemplo de tipos propios"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"La mutabilidad e inmutabilidad va m\u00e1s all\u00e1 de los tipos est\u00e1ndar de python. Nosotros mismos podemos hacer nuestros propias clases mutables o inmutables, seg\u00fan nuestras necesidades.\n",
"\n",
"Pongamos que creamos una clase `Point` para definir puntos, junto con unas sencillas operaciones para sumar, restar y desplazar. Nuestra idea es poder usar estos objetos en expresiones, por lo que es pr\u00e1ctica com\u00fan que toda operaci\u00f3n devuelva el resultado como un punto para seguir operando.\n",
"\n",
"Una versi\u00f3n *\"mutable\"* del objeto ser\u00eda as\u00ed: "
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"class PointMutable(object):\n",
" def __init__(self, x, y):\n",
" self.x=x\n",
" self.y=y\n",
"\n",
" def __repr__(self):\n",
" return \"<Point(%d,%d)>\"%(self.x,self.y)\n",
"\n",
" def __sub__(self, other):\n",
" self.x-=other.x\n",
" self.y-=other.y\n",
" return self\n",
" \n",
" def __add__(self, other):\n",
" self.x+=other.x\n",
" self.y+=other.y\n",
" return self\n",
"\n",
" def move(self, dx, dy):\n",
" self.x+=dx\n",
" self.y+=dy\n",
" return self"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 42
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"En todas las operaciones, operamos el objeto consigo mismo y lo retornamos como resultados. Si probamos, vemos que no funciona tal como se esperaba:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
">>> p1=PointMutable(1,1)\n",
">>> p2=PointMutable(-1,1)\n",
">>> print p1.move(1,1) - (p1+p2).move(2,2)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"<Point(0,0)>\n"
]
}
],
"prompt_number": 43
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Devuelve `<Point<0,0>` independientemente de los valores iniciales y de los desplazamientos que demos. Al ser nuestro objeto mutable, cada operaci\u00f3n lo va cambiando. Al final, toda la expresi\u00f3n se reduce a una simple resta `p1-p1`, que ser\u00eda la \u00faltima operaci\u00f3n y que da siempre `<Point(0,0)>`. No parece que sea el resultado esperado.\n",
"\n",
"Debemos adoptar una t\u00e1ctica m\u00e1s defensiva: el objeto nunca debe cambiar durante el c\u00e1lculo. Como resultado de cada operaci\u00f3n deberemos devolver una nueva instancia y que el estado de \u00e9sta (o sea, sus atributos) no se alteren a lo largo del c\u00e1lculo:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"class PointInmutable(object):\n",
" def __init__(self, x, y):\n",
" self.x=x\n",
" self.y=y\n",
"\n",
" def __repr__(self):\n",
" return \"<Point(%d,%d)>\"%(self.x,self.y)\n",
"\n",
" def __sub__(self, other):\n",
" return PointInmutable(self.x-other.x,self.y-other.y)\n",
" \n",
" def __add__(self, other):\n",
" return PointInmutable(self.x+other.x,self.y+other.y)\n",
"\n",
" def move(self, dx, dy):\n",
" return PointInmutable(self.x+dx,self.y+dy)"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 44
},
{
"cell_type": "code",
"collapsed": false,
"input": [
">>> p1=PointInmutable(1,1)\n",
">>> p2=PointInmutable(-1,1)\n",
">>> print p1.move(1,1) - (p1+p2).move(2,2)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"<Point(0,-2)>\n"
]
}
],
"prompt_number": 45
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Siendo perfeccionistas, deber\u00edamos blindar mejor los atributos de la clase para hacerlos de *s\u00f3lo lectura* mediante `properties`.\n",
"\n",
"En este ejemplo hemos podido ver los resultados imprevisibles que podemos tener si abusamos de la mutabilidad. Estos problemas se ven incrementados si hubiera varios hilos de ejecuci\u00f3n y cada hilo estuviera modificando las mismas variables comunes. Lamentablemente, es un caso bastante com\u00fan debido a una mala previsi\u00f3n a la hora de iniciar un proyecto de desarrollo. Pero \u00e9sto lo veremos en un pr\u00f3ximo art\u00edculo."
]
}
],
"metadata": {}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment