viernes, 21 de febrero de 2014

Python exquisite I, listas, generadores y conjuntos



Python es un lenguaje claro, sencillo de aprender y que te permite dedicar casi todo el esfuerzo a los algoritmos sin preocuparte en exceso de los problemas formales del lenguaje. No obstante, llega un momento en el que uno empieza a desear escribir código más eficiente y elegante. Comienzo, con esta entrada, a tratar algunos de los detalles menos evidentes del lenguaje, con la finalidad de profundizar aún más en Python. Si el tiempo me lo permite, espero que sea el inicio de una larga serie de entradas del mismo tipo.


Empecemos con las listas. Se pueden meter cosas de cierta complejidad en una lista, por ejemplo, una lista de pares:


pares = [num for num in range(1,100000) if num%2 == 0] 

Por su puesto el contenido podría complicarse mucho más, pero no es este el tema de discusión de esta entrada. Pensemos en términos de memoria. Hay que tener en cuenta que la lista anterior contiene 49999 números: 

>>> getsizeof(pares)/1024
396
 
Es decir, ocupa 396 KB. Si queremos simplemente sumar toda la lista de números, la cosa seria tan sencilla como hacer:

>>> sum(pares)
2499950000
 
Pero hay otra forma de realizar este cálculo, sin tener que ocupar tanta memoria. Para ello usamos un generador, que en apariencia hace lo mismo que la lista.

 
>>> paresGen = (num for num in range(1,100000) if num%2 == 0) 
>>> getsizeof(paresGen)/1024
80
>>> sum(paresGen)
2499950000

El tamaño es de tan solo 80 Bytes, frente a los 396 KB de la lista y el resultado de la suma es el esperado, como no podía ser de otra forma.

En líneas generales un generador nos proporciona una función que se comporta como un iterador, por lo que puede ser usado en un bucle. Por otro lado tenemos xrange y range, que funcionan como un generador el primero y una lista el segundo.

list = range(1,10)
>>> getsizeof(list)
144
>>> genX = xrange(1,10)
>>> getsizeof(genX)
40

Se puede pasar el generador a una lista mediante list, de la siguiente forma:

newList = list(xrange(1,10000))


Una advertencia sobre el uso de generadores; un generador no contiene realmente todos los elementos, por lo que si queremos usar los elementos hay que iterar sobre ellos. Las funciones Build-In, tal como min(), max(), sum(),... iteran sobre el generador al emplearlas, pero una vez usado el iterador no se reinicia, por lo que si usamos dos veces la función nos dará un error al trabajar sobre un objeto vacío.

genB = (num for num in range(1,1000) if num%2 == 0)
>>> min(genB)
2
>>> min(genB)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: min() arg is an empty sequence

En líneas generales xrange no presenta grades ventajas sobre la lista tradicional, excepto que tengamos un rango realmente grande y/o una máquina con escasa memoria. Para algunos detalles más ver XRange Type.

Los conjuntos (set) son muy similares a las listas pero no toman duplicados ni ordenan los elementos. Otra particularidad es que los conjuntos no tienen por que mostrarse en el mismo orden en el que se introdujeron. Su uso es útil, por ejemplo, cuando se desea saber si un elemento esta en el conjunto. Vemos un ejemplo:

  
>>> def list_letters(letters):
...     return set(letters.lower())
... 
letters = list_letters('abcdeerfsdcad')
>>> letters
set(['a', 'c', 'b', 'e', 'd', 'f', 's', 'r'])
>>> 'a' in letters
True
>>> 'z' in letters
False

Para añadir elementos podemos emplear add, no hay posibilidad de usar append  por razones obvias ya que esta última función incorpora los elementos al final del conjunto, por lo que el orden sería importante, orden que no permite set. De igual manera se puede emplear remove para quitar elementos. Hay que tener precaución con remove ya que si el elemento a eliminar no se encuentra en el conjunto tendremos un error KeyError que deberíamos manejar. Si no deseamos estar atento del manejo de este error, podemos emplear discard que realiza la misma función pero no da error alguno. Hay otras funciones interesantes que no vamos a describir aquí. Resulta más interesante describir algunas operaciones del álgebra de conjuntos. A continuación la unión, intersección y diferencia (A/B):


>>> {1,2,3} | {1,5,6}
set([1, 2, 3, 5, 6])
>>> {1,2,3} & {1,5,6}
set([1])
>>> U = {1,2,3} & {1,5,6}
>>> I = {1,2,3} & {1,5,6}
>>> {1,2,3} - {1,3,7}
set([2])
 
También la diferencia simétrica (symmetric_difference), operación que consiste en la unión de A y B menos la intersección de ambos.

>>> {2,4,6,8}.symmetric_difference({2,6,10})
set([8, 10, 4])

Se puede preguntar si un conjunto A es subconjunto de un tercero B, o si por el contrario es un superconjunto del anterior.

>>> {1,2,3,4}.issubset({1,2,3,4,5,6})
True
>>> {0,1,3,7,8}.issuperset({0,7,8})
True 

Hasta aquí un primer resumen de nociones no necesariamente básicas de Python. La siguiente entrada de Python exquisite tratará sobre clases.

No hay comentarios:

Publicar un comentario