Python 3: es de colección

miércoles, 10 de diciembre de 2008
Otro de los sets de cambios incluídos en la nueva versión de Python afecta a las colecciones. Entran en esta categoría los diccionarios, conjuntos y secuencias.

Sets y Frozensets son ahora builtin

En Python 3, set es un builtin (ya no es necesario importarlo del módulo sets). Set es una estructura de datos que representa un conjunto de elementos sin un orden específico. La implementación utiliza una tabla de hash, al igual que los diccionarios.

El tipo set es mutable (una misma instancia puede recibir modificaciones), al igual que las listas. Como la listas en Python tienen un equivalente inmutable (las tuplas), los conjuntos lo tienen también en el tipo frozenset. Cabe aclarar que estos tipos ya existían en Python 2.x, pero en Python 3 fueron promovidos a la librería estándar y ya no es necesario importar ningún módulo para utilizarlos.
>>> s = set()          # creo un set vacío
>>> for i in [1,2,3]:
...     s.add(i)
...
>>> 3 in s
True
>>> 4 in s
False
>>> s.remove(2)
>>> s
{1, 3}
>>> fs = frozenset()   # creo un frozenset vacío
>>> fs2 = fs.union(s)  # lo uno con el set {1, 3}
>>> fs2
frozenset({1, 3})      # obtengo un nuevo frozenset
>>> fs
frozenset()            # fs se mantiene intacto
Hay más información en la documentación oficial de set y frozenset.

Nuevos literales y expresiones por comprensión

Set viene acompañado de un literal para su creación: no es necesario construirlo agregando tediosamente elementos a un set vacío como hice arriba. Sin embargo, como la sintaxis es similar a la del literal de diccionarios, hay que tener cuidado al crear uno vacío! El constructor de set vacío es set(), ya que {} está reservado para los diccionarios.
>>> s = {1, 2, 3, 4}           # creo un conjunto
>>> d = {1: 'uno', 2: 'dos'}   # creo un diccionario
>>> s = {}                     # ojo! crea un diccionario
>>> type(s).__name__
'dict'
>>> s = set()
Pero todavía hay más! Python 3 finalmente incorpora diccionarios por comprensión, y ya que estamos en la volada, conjuntos por comprensión. La sintaxis es exactamente la que uno esperaría, muy similar a la de listas por comprensión. Es curioso mencionar que la propuesta de diccionarios por comprensión proviene del PEP 274 (de 2001!). En 2008, Python 3 incorpora esta deseable funcionalidad: se terminaron los días de usar una lista porque es más cómodo!
>>> numeros = [1,2,3,4,5,6,7,8,9,10]
>>> def esPar(x):
...     return x % 2 == 0
...
>>> pares = {x for x in numeros if esPar(x)}
>>> pares
{8, 2, 4, 10, 6}
>>> cuad_impares = {x: x*x for x in numeros if not esPar(x)}
>>> cuad_impares
{1: 1, 3: 9, 9: 81, 5: 25, 7: 49}

Novedades en diccionarios

En primer lugar hay que notar que el método has_key() de diccionarios fue eliminado. Este método era redundante; estas dos expresiones eran equivalentes en Python 2.x, introduciendo una ambigüedad innecesaria:
>>> dicc.has_key('clave')
True
>>> 'clave' in dicc
True
En Python 2.x los métodos keys(), values() y items() de un diccionario devolvían listas contiendo los datos del diccionario (sus claves, sus valores, y los pares clave-valor respectivamente). Asimismo, existían funciones análogas iterkeys, iteritems y itervalues. Estas últimas devolvían iteradores sobre las mismas secuencias que las anteriores.

Python 3 introduce el concepto de vista. Una vista es un objeto intermedio entre el diccionario y los elementos devueltos por los métodos descritos en el párrafo anterior. Los mismos objetos que usábamos en Python 2.x pueden obtenerse ahora a partir de la vista en lugar del diccionario propiamente dicho. Veamos como:
>>> d = {1:'uno', 2:'dos', 3:'tres'}    # armo un diccionario

# Python 2.x
>>> it = d.iterkeys()  # obtengo un iterador sobre las claves
>>> keys = d.keys()    # obtengo una lista de claves
>>> keys
[1, 2, 3]

# Python 3
>>> v = d.keys()    # obtengo la "vista" del diccionario d
>>> type(v).__name__
'dict_keys'
>>> it = iter(v)    # construyo un iterador a partir de la vista (no del dict!)
>>> keys = list(v)  # construyo una lista a partir de la vista
La utilidad de la vista no es aparente aún: sin embargo, la vista introduce una nueva posibilidad. Es claro que la lista obtenida a partir de keys() en Python 2.x está desconectada del diccionario: toda modificación al diccionario posterior a la creación de la lista no se ve reflejada en la misma. Los iteradores se comportan de forma similar: si se modifica el diccionario durante la iteración, se produce una excepción.
>>> d = {1:'uno', 2:'dos', 3:'tres'}
>>> it = d.iterkeys()
>>> del(d[2])
>>> for key in it:
...     print key
... 
Traceback (most recent call last):
RuntimeError: dictionary changed size during iteration
Lo curioso de la vista es que ella también es iterable! (además del iterador que devuelve al aplicarle iter()). Y esta iteración si soporta modificaciones durante su ejecución. De modo que en Python 3, podríamos hacer esto:
>>> v = d.keys()
>>> del(d[2])
>>> for key in v:
...     print(key)
...
1
3
Si obtenemos a partir de la vista el iterador "tipo Python 2.x", tendremos el mismo comportamiento que antes (se emite una excepción al modificar el tamaño del diccionario sobre el que se está iterando).

En resumen, Python 3 incorpora el tipo set a la librería estándar, y notaciones literales y por comprensión tanto para conjuntos como para diccionarios. Estas facilidades hacen más ameno el uso de los tipos de datos correctos para cada tarea, en casos en los que muchas veces se utilizaba una lista por comodidad de notación. Por último, las vistas introducen funcionalidad adicional a los diccionarios, permitiendo modificarlos mientras se los recorre además de mantener las operaciones existentes.

2 comentarios:

Pablo Antonio dijo...

Nuevamente, muchas gracias. Está todo muy claro.

Qué bueno que hay más expresiones por comprensión; la verdad que son muy cómodas. Una lástima que ya se haya usado { } para los diccionarios (aunque es algo a lo que ya estamos acostumbrados hace tiempo); debería existir algún otro elemento de sintaxis (del estilo de la llave) para distinguirlos con mayor claridad de los sets.

Cuando decís "secuencia", querés decir el tipo "Tuple", ¿no? Me parece que sí, pero pregunto por las dudas.

Otra duda que me surge es si los iteradores tienen ahora algún sentido, existiendo estas nuevas "vistas". Lo único que se me ocurre es que alguien quiera ser avisado en caso de que la colección sobre la que está iterando haya sido modificada o quiera estar seguro de que nadie tocó su colección mientras la iteraba.

Saludos.

GomoX dijo...

Hola Pablo,

Personalmente lo de usar {} tanto para sets como para diccionarios no me parece muy problemático, al fin y al cabo son dos estructuras de datos muy parecidas - un diccionario no es más que un conjunto que a cada elemento asocia otro valor. La implementación es muy similar también.

Sobre lo de secuencia, no está muy claro, usé secuencia de dos maneras:
- para referirme a cualquier conjunto ordenado (tuplas o listas)
- para referirme a listas
Ahí lo corregí y cambié las apariciones del segundo tipo por el término "lista".

Los iteradores obtenidos a partir de la vista calculo que caerán en desuso, salvo casos particulares en los que se requiera esa excepción en caso de modificación. Supongo que se dejaron por compatibilidad hacia atrás con Python 2.x, para que reescribir el código sea cuestión de reemplazar d.iterkeys() por iter(d.keys()) sin mirar más nada.

Saludos ;)

Publicar un comentario