Cómo pensar como un pythonista
Esta página es una traducción del artículo How to think like a Pythonista de Michael Hudson. Las referencias a primera persona en los comentarios se referieren, lógicamente, a él.
En www.aleax.it encontrarán documentación de Python en italiano, escrita por Alex Martelli autor original de parte del artículo que aqui reproduzco. Puede serles útil si dicho idioma les es mas sencillo de entender que el inglés.
–N.de T.
January 2020: This is my very first blog post from April 2002. I published it blogspot.com and ported here as an historical reference.
Hace poco (abril del 2002), un no-iniciado en Python buscaba respuestas en comp.lang.python por ciertos extraños comportamientos del lenguaje. Dada la recurrencia del error entre los recién llegados a Python, reproduzco aquí la duda y las respuestas.
La Pregunta
Hola,
Algo que me gusta mucho de Python es que las sentencias funcionan tal y como uno espera que lo hagan. Tomemos por ejemplo el uso de dict.values() para los diccionarios: Si conservamos el resultado de dict.values() y posteriormente modificamos el diccionario, el resultado conservado permanece inalterado.
>>> dict = {'a':1,'b':2}
>>> list = dict.values()
>>> list
[1, 2]
>>> dict['a']=3
>>> list
[1, 2]
>>> dict
{'a': 3, 'b': 2}
Sin embargo, si el diccionario tiene listas como entradas en el diccionario, obtengo un comportamiento contraintuitivo (que, hace poco, hizo que mi codigo dejara de funcionar): Si modifico el dict, la lista que previamente habia sido creada mediante dict.values() aparece actualizada automágicamente. Un bonito rasgo, ¡pero nada que hubiera esperado!
>>> dict = {'a':[1],'b':[2]}
>>> list = dict.values()
>>> list
[[1], [2]]
>>> dict['a'].append(3)
>>> dict
{'a': [1, 3], 'b': [2]}
>>> list
[[1, 3], [2]]
Parece como si en el primer caso una copia es devuelta mientras en el segundo caso una referencia a la lista es que se devuelve. Muy bien, pero de acuerdo a la filosofía de Python no debería preocuparme si trabajara con listas en un diccionario o cualquier otra cosa. No lo encuentro intuitivo si el comportamiento depende del tipo de valores que pongo en un diccionario.
¿Quién esta equivocado aquí: mi intuición o Python? Si fuera mi intuición ¿Cómo podría entrenar mi pensamiento sobre el modelo de ejecucion de Python para que mi intuicion mejore? ;-)
Mi respuesta
Demás está decir que es la intuicion del interesado la que estuvo en falta, pero el autor lejos está (estuvo) de ser el unico en tener este malentendido.
Suerte para él, otros dos programadores con mucha experiencia en Python, Alex Martelli y yo, nos encontrábamos en un día con un particular humor pedagógico (1), y escribimos algunos largos articulos explicando en forma algo diferente donde estaba equivocado.
Estuve despotricando sobre pensar en terminos de “nombres, objetos y enlaces” (a), algo que no hago lo suficiente, y dibuje algunos diagramas ASCII explicando que es lo hay bajo la superficie de los distintos ejemplos que tenian al autor original confundido.
Algo que me gusta mucho de Python es que las sentencias funcionan tal y como uno espera que lo hagan.
Bueno. Python funciona mucho como yo espero que lo haga, pero no esta claro si eso dice mas sobre mi que sobre Python.
Al final del mensaje, dices:
¿Quien esta equivocado aqui: mi intuición o Python? Si fuera mi intuición ¿Cómo podría entrenar mi pensamiento sobre el modelo de ejecucion de Python para que mi intuicion mejore? ;-)
Eres tú :) Como no puedo leer mi email por el momento[1], no tengo otra forma mejor que perder el tiempo que dibujarle algunos gráficos.
Primero, alguna terminologia. En realidad, lo primero de todo es alguna anti-terminologia; encuentro la palabra “variable” particularmente inútil dentro del contexto de Python. Prefiero “nombres”, “ligaduras” y “objetos”.
Los nombres tienen esta forma:
Los nombres viven en los espacios de nombres (namespaces), pero no es de importancia para el tema que nos ocupa dado que el unico un espacio de nombre en juego es el asociado con el ciclo leer-evaluar-mostrar del interprete. De hecho los nombres son solo jugadores menores en este drama; las ligaduras y los objetos son las estrellas.
Las ligaduras tienen esta forma:
La punta izquierda de las ligaduras pueden ser sujetadas a los nombres u otros “lugares” como los atributos de objetos y entradas en listas o en diccionarios. La punta derecha está siempre sujetada a objetos[2]
Los objetos tienen esta forma:
Esto pretende ser la cadena de caracteres “bar”. Otros tipos de objetos serán dibujados en forma diferente, pero espero que entenderá lo que busco.
Tomemos por ejemplo el uso de dict.values() para los diccionarios: Si conservamos el resultado de dict.values() y posteriormente modificamos el diccionario, el resultado conservado permanece inalterado.
>>> dict = {'a':1,'b':2}
Luego de esta sentencia, parecería apropiado dibujar este diagrama:
>>> list = dict.values()
Ahora este:
>>> list [1, 2]
Lo que por supuesto, no es sorpresa.
>>> dict['a']=3
Ahora esto:
>>> list [1, 2] >>> dict {'a': 3, 'b': 2}
Esto último tampoco deberia sorprender; solamente hay que seguir las flechas (ligaduras) del gráfico superior.
Sin embargo, si el diccionario tiene listas como entradas en el diccionario, obtengo un comportamiento contraintuitivo (que, hace poco, hizo que mi codigo dejara de funcionar): Si modifico el dict, la lista que previamente habia sido creada mediante dict.values() aparece actualizada automágicamente. Un bonito rasgo, ¡pero nada que hubiera esperado!
Esto sucede porque no esta pensando en terminos de Nombres, Objetos y Ligaduras.
>>> dict = {'a':[1],'b':[2]}
>>> list = dict.values()
>>> list [[1], [2]]
De nuevo no hay sorpresas aqui.
>>> dict['a'].append(3)
>>> dict {'a': [1, 3], 'b': [2]} >>> list [[1, 3], [2]]
Y ahora esto tampoco deberia sorprender.
Parece como si en el primer caso una copia es devuelta mientras en el segundo caso una referencia a la lista es que se devuelve. Muy bien, pero de acuerdo a la filosofia de Python no deberia preocuparme si trabajara con listas en un diccionario o cualquier otra cosa. No lo encuentro intuitivo si el comportamiento depende del tipo de valores que pongo en un diccionario.
Si luego de haber visto los graficos presentados no se ha dado cuenta de donde vienen sus malosentendidos, no estoy seguro si mucha mas prosa podría ayudar.
Salud, Michael Hudson.
[1] Alguien sabe donde fue el starship? [2] Dispararé a cualquiera que mencione UnboundLocalError en este momento.
Alex tomo una estrategia diferente, verbosa, para explicar que Python no copia cuando no tiene que hacerlo, relatando una anecdota muy bonita sobre una estatua en Boloña y sugieriendo al interesado lecturas de Borges, Calvino, Wittgenstein o Korzibsky.
La respuesta de Alex
Hola,
Algo que me gusta mucho de Python es que las sentencias funcionan tal y como uno espera que lo hagan. Tomemos por ejemplo el uso de dict.values() para los diccionarios: Si conservamos el resultado de dict.values() y posteriormente modificamos el diccionario, el resultado conservado permanece inalterado.
El método .values() de un diccionario retorna una nueva lista de los valores. Esto es de alguna manera inevitable, desde que los diccionarios normalmente no tienen la lista de sus valores, y la tienen que construir al vuelo cuando usted la solicita. No es una copia – es un nuevo objeto lista.
Sin embargo, Python no copia excepto en situaciones donde una copia es específicamente definida para que se haga. El metodo .values() se encuentra en esa situación en una forma vaga, como fue mencionado… un nuevo objeto, en vez de una copia de cualquier otro existente.
En general, siempre que sea posible, Python devuelve referencias a los mismos objetos que ya tiene alrededor, en vez de copialos; si AÚN quiere una copia solicitela – vea el módulo copy si quiere hacerlo en una forma general. Por supuesto, construir nuevos objetos es un caso diferente.
Si esto es contraintuitivo, que así sea – realmente no hay alternativas para el caso general sin imponer un gran coste, haciendo copias de todo solo “por las dudas”. MUCHO mejor es hacer copias solo ante requerimientos explícitos (y objetos nuevos, cuando no existen objetos que podrían ser copiados o referenciados)
Por supuesto que hay casos que estan en el medio – como las rebanadas (slices)
Las secuencias estándar le dan un nuevo objeto cuando solicita una rebanada; esto solo importa para listas (para los objetos inmutables a Ud. no le deberia interesar si obtiene copias o no) Una lista no tiene la capacidad de “compartir una parte de si misma”, y cuando se le solicita una rebanada le devuelve una copia, una nueva lista (en general, por supuesto, también lo hace cuando se le solicita una rebanada-de-todo, lalista[:] – caso limite donde un nuevo objeto pueder ser visto como una copia de uno existente)
El muy justo popular paquete Numeric, por otro lado, define el tipo array que SI es capaz de compartir alguno o todos sus datos entre diferentes objetos array – una rebanada de un array en Numeric comparte sus datos con el array desde se obtuvo la rebanada. Es un nuevo objeto, fíjese:
>>> import Numeric
>>> a=Numeric.array(range(6))
>>> b=a[:]
>>> id(a)
136052568
>>> id(b)
136052728
>>>
pero esos dos objetos distintos a y b sí comparten datos:
>>> a
array([0, 1, 2, 3, 4, 5])
>>> b
array([0, 1, 2, 3, 4, 5])
>>> a[3]=23
>>> b
array([ 0, 1, 2, 23, 4, 5])
>>>
Cada comportamiento tiene detrás un excelente pragmatismo –las listas son mucho mas simples al no tener que preocuparse de compartir datos, los array tienen casos de uso muy diferentes – pero es difícil no sorprenderse cuando dos objetos tan similares difieren en esos detalles.
Pero todas las copias que sí “suceden”, ej. el caso limite de las rebanadas o cualquier otro caso (con UNA excepción que mencionaré mas adelante) son siempre copias playas (shallow).
Python NUNCA se embarca en la ENORME tarea de la copia profunda a menos que muy específicamente se solicite –específicamente con la función deepcopy del modulo copy. La copia PROFUNDA es cosa seria –la función deepcopy tiene que estar alerta de los ciclos, reproducir cualquier identidad de referencias, potencialmente seguir las referencias a cualquier profundidad, recursivamente –tiene que reproducir fielmente el grafo de los objetos referenciando uno a otro con ilimitada complejidad. Funciona, pero por supuesto nunca será tan rápida como la simple tarea de la copia playa (que a su vez nunca será tan rápida como manejar directamente una referencia mas a un objeto existente, siempre que este último curso de acción sea posible)
Aparentemente esto es lo que lo ha atrapado a ud aquí:
Sin embargo, si el diccionario tiene listas como entradas en el diccionario, obtengo un comportamiento contraintuitivo (que, hace poco, hizo que mi codigo dejara de funcionar): Si modifico el dict, la lista que previamente habia sido creada mediante dict.values() aparece actualizada automágicamente. Un bonito rasgo, ¡pero nada que hubiera esperado!
Realmente no – si ud. cambia objetos a los cuales dict refiere (en vez de cambiar el dict en si), luego las otras referencias a los-mismos-objetos permanecen como referencias a esos mismos objetos – si los objetos mutan, ud. ve a los objetos mutados desde cualesquier referencia a ellos que pueda estar usando.
>>> dict = {'a':[1],'b':[2]} >>> list = dict.values() >>> list [[1], [2]]
No use nombres de los tipos built-in como variables: ud. SE quemará algún día si lo hace. dict, list, str, tuple, file, int, long, float, unicode… no use esos identificadores para sus propios propósitos, por mas tentador que pueda ser (una “molestia atractiva”, seguro.) Si no toma el hábito de evitar eso, un día estara intentando (por ejemplo) construir una lista con x=list(‘chau’) y obtendra errores intrigantes… porque habrá reenlazado el idenficador ‘list’ para referir a cierto objeto list en vez de al tipo list en si.
Use alist, somedict, myfile, lo que sea… nada que ver con su problema, solo otro simple consejo !-)
>>> dict['a'].append(3)
Esto no “modifica el diccionario” – el objeto diccionario aún contiene exactamente las mismas referencias, a los objetos con los mismos id (dos objetos cadena, las claves y dos objetos listas, los valores). Está cambiando (mutando) uno de esos objetos, pero ese es otro tema. Podría modificar dicho objeto lista a traves de cualquier referencia a él, ejemplo:
>>> alist=list('ciao')
>>> adict={'a':alist}
>>> adict
{'a': ['c', 'i', 'a', 'o']}
>>> alist.pop()
'o'
>>> adict
{'a': ['c', 'i', 'a']}
>>>
Si ud. quería que el diccionario adict refiriera a una COPIA (una “toma”, si prefiere) de los contenidos de alist, tendría que haberlo dicho:
>>> import copy
>>> alist=list('ciao')
>>> adict={'a':copy.copy(alist)}
>>> adict
{'a': ['c', 'i', 'a', 'o']}
>>> alist.pop()
'o'
>>> adict
{'a': ['c', 'i', 'a', 'o']}
>>>
y luego la cadena-reprensentación del objeto diccionario podría ser aislada de cualquier cambio a la lista que referencia el nombre alist. La cadena que la representa delega parte de su tarea a los objetos a los cuales el objeto diccionario refiere, entonces, si quiere aislarlo, ud. necesita copias –de hecho tal vez profundas (… bueno no, realmente no, pero… :-)
>>> dict {'a': [1, 3], 'b': [2]} >>> list [[1, 3], [2]]
Parece como si en el primer caso una copia es devuelta mientras en el segundo caso una referencia a la lista es que se devuelve.
Nop. SIEMPRE Referencias. .values() no devuelve una referencia a un objeto existente NI una copia a un objeto existente, porque en este caso hay “objeto existente” –por lo que siempre devuelve un NUEVO objeto, construido adrede de acuerdo a sus especificaciones.
… de acuerdo a la filosofia de Python no deberia preocuparme si trabajara con listas en un diccionario o cualquier otra cosa. No lo encuentro intuitivo si el comportamiento depende del tipo de valores que pongo en un diccionario.
No hay tales dependencias. Solo una enorme diferencia entre cambiar un objeto y cambiar (mutar) algún OTRO objeto al que el primero refiere.
En Boloña hace mas de 100 añas teniamos una estatua de un héroe local representado apuntando con su dedo hacia adelante –presumiblemente hacia el futuro, pero dado el lugar donde fue emplazado, los vecinos rápidamente la identificaron como “la estatua que apunta al Hotel Belfiore”. Un día un empresario compró el edificio del hotel y lo reformó – en particular, donde solia estar el hotel ahora hay un restaurante, el Da Carlo.
De pronto, “la estatua que apunta al Hotel Belfiore” devino en “la estatua que apunta al Da Carlo”…! Asombroso, no? Considerando que el mármol no es muy fluido y que el monumento no ha sido movido ni molestado de ninguna forma…?
A propósito, esta es una anécdota real (excepto que no estoy seguro sobre los nombres del hotel y del restaurante –podría equivocarme en ellos), pero aún pienso que puede ayudar aquí. El diccionario, o la estatua, no ha sido modificada de ninguna forma, aun cuando los objetos a los que refiere/apunta pueden haber mutado hasta quedar irreconocibles, y que el nombre con el que la gente lo conoce (la cadena-representación del diccionario) pueda asi cambiar. Aquel nombre o representación estaba y esta refiriendo a un no-intrínseco, no-persistente, característica “casual” de la estatua, o diccionario…
¿Quien esta equivocado aqui: mi intuicion o Python? Si fuera mi intuicion ¿Cómo podría entrenar mi pensamiento sobre el modelo de ejecucion de Python para que mi intuicion mejore? ;-)
Su intuición, que le llevó por el mal camino (Python hace sólo lo que debería hacer), puede ser entrenada de diversas maneras. Las obras de J. L. Borges e I. Calvino, si le gusta la ficción que sea razonablemente sofisticada y hasta muy placentera, son buenas apuestas. Si le gusta la no-ficción escrita por ingenieros luchando duro para disipar algunos de los errores de los filósofos, Wittgenstein y Korzibsky son excelentes.
No estoy bromeando. Me doy cuenta que muchos Pythonistas no se interesan por ninguno de los dos generos. En ese caso, este grupo (comp.lang.python) y sus archivos, los ensayos de GvR y /F, y los fuentes de Python, también pueden ser lecturas interesantes.
Alex
El ensayo de /F al que se refería Alex es probablemente éste (e incluso si no lo es, debería usted leerlo.) Habla sobre algunos de estos temas en un forma mas tersa.
GvR es Guido van Rossum y /F es Fredrik Lundh –N.de T.
Un cliente satisfecho
Y solo para probar que valió la pena hacer todo esto, nuestro no-iniciado se transformó en un cliente satisfecho:
Estiamdo Michael, estimado Alex,
¡¡¡Son excelentes profesores!!!
Michael, realmente me has ayudado en entender el punto con tus gráficos. ¡Un montón de gracias por tu trabajo artístico!
Alex, la anécdota sobre la estatua apuntando al Hotel Belfiore hizo obvio el error de mi intuición! Me gustó y nunca mas volveré a olvidarlo! Gracias por tu respuesta!
Pienso que hoy aprendí muchisísmo en mi camino para convertirme en buen Pythónico!
Espero que ustedes también encuentren útiles estas respuestas.