Cómo usar el módulo collections en Python 3

El autor seleccionó el COVID-19 Relief Fund para que reciba una donación como parte del programa Write for DOnations.

Introducción

Python 3 tiene varias estructuras de datos integradas, incluyendo tuplas, diccionarios y listas. Las estructuras de datos nos proporcionan una forma de organizar y almacenar datos. El módulo collections nos ayuda a completar y manipular las estructuras de datos de forma eficiente.

En este tutorial, veremos tres clases del módulo collections para ayudarle a trabajar con tuples, diccionarios y listas. Usaremos namedtuples para crear tuplas con campos con nombre, defaultdict para agrupar de forma concisa la información en diccionarios, y deque para añadir elementos de forma eficiente a cualquier lado de un objeto lista.

Para este tutorial, trabajaremos principalmente con un inventario de peces que necesitamos modificar a medida que se añaden o retiran peces a un acuario ficticio.

Requisitos previos

Para sacar el máximo partido a este tutorial, se recomienda que esté algo familiarizado con los tipos de datos tupla, diccionario y lista, con su sintaxis y con cómo recuperar datos desde ellos. Puede revisar estos tutoriales para encontrar la información básica necesaria:

  • Comprender Tuplas en Python 3
  • Comprender Diccionarios en Python 3
  • Comprender Listas en Python 3

Añadir campos con nombre a las tuplas

Las tuplas de Python son secuencias de elementos ordenadas de forma inmutable o inalterable. Las tuplas se utilizan frecuentemente para representar datos en columnas; por ejemplo, líneas de un archivo CSV o filas de una base de datos SQL. Un acuario puede mantener un seguimiento de su inventario de peces como una serie de tuplas.

Una tupla de peces individual:

("Sammy", "shark", "tank-a") 

Esta tupla está compuesta por tres elementos de cadena.

Aunque es útil en cierta forma, esta tupla no indica claramente qué representa cada uno de sus campos. En realidad, el elemento 0 es un nombre, el elemento 1 es una especie y el elemento 2 es el depósito.

Explicación de los campos de la tupla de peces:

nombre especie tanque
Sammy tiburón tanque-a

Esta tabla deja claro que cada uno de los tres elementos de la tupla tiene un significado claro.

namedtuple del módulo collections le permite añadir nombres explícitos a cada elemento de una tupla para hacer que estos significados sean claros en su programa Python.

Vamos a usar namedtuple para generar una clase que claramente denomine a cada elemento de la tupla de peces:

from collections import namedtuple  Fish = namedtuple("Fish", ["name", "species", "tank"]) 

from collections import namedtuple proporciona a su programa Python acceso a la función de fábrica namedtuple. La invocación de la función namedtuple() devuelve una clase que está vinculada al nombre Fish. La función namedtuple() tiene dos argumentos: el nombre deseado de nuestra nueva clase "Fish" y una lista de elementos denominados ["name", "species", "tank"].

Podemos usar la clase Fish para representar la tupla de peces anterior:

sammy = Fish("Sammy", "shark", "tank-a")  print(sammy) 

Si ejecuta este código, verá el siguiente resultado:

OutputFish(name='Sammy', species='shark', tank='tank-a') 

sammy se instancia usando la clase Fish. sammy es una tupla con tres elementos claramente nombrados.

Se puede acceder a los campos de sammy por su nombre o con un índice de tupla tradicional:

print(sammy.species) print(sammy[1]) 

Si ejecutamos estas dos invocaciones print, veremos el siguiente resultado:

Outputshark shark 

Acceder a .species devuelve el mismo valor que acceder al segundo elemento de sammy usando [1]​​​.

Usar namedtuple desde el módulo collections hace que su programa sea más legible al tiempo que mantiene las propiedades importantes de una tupla (que son inmutables y están ordenadas).

Además, la función de fábrica namedtuple añade varios métodos adicionales para las instancias de Fish.

Utilice ._asdict() para convertir una instancia en un diccionario:

print(sammy._asdict()) 

Si ejecutamos print, verá un resultado como el siguiente:

Output{'name': 'Sammy', 'species': 'shark', 'tank': 'tank-a'} 

Invocar .asdict() en sammy devuelve una asignación de diccionario de cada uno de los tres nombres de campo con sus correspondientes valores.

Las versiones de Python anteriores a 3.8 pueden mostrar esta línea de forma ligeramente diferente. Es posible, por ejemplo, que vea OrderedDict en vez del diccionario simple mostrado aquí.

Nota: En Python, los métodos con guiones bajos precedentes se consideran, normalmente, “privados”. Los métodos adicionales proporcionados por namedtuple (por ejemplo, _asdict(), ._make(), ._replace(), etc.), sin embargo, son públicos.

Recoger datos en un diccionario

A menudo es útil recopilar datos en los diccionarios Python. defaultdict del módulo collections puede ayudarnos a ensamblar información en diccionarios de forma rápida y concisa.

defaultdict nunca plantea un KeyError. Si no está presente una clave, defaultdict solo inserta y devuelve un valor de marcador de posición:

from collections import defaultdict  my_defaultdict = defaultdict(list)  print(my_defaultdict["missing"]) 

Si ejecutamos este código, veremos un resultado como el siguiente:

Output[] 

defaultdict inserta y devuelve un valor de marcador de posición en vez de lanzar un KeyError. En este caso, especificamos el valor de marcador de posición como una lista.

Los diccionarios regulares, por el contrario, lanzarán un KeyError sobre las claves que faltan:

my_regular_dict = {}  my_regular_dict["missing"] 

Si ejecutamos este código, veremos un resultado como el siguiente:

OutputTraceback (most recent call last):   File "<stdin>", line 1, in <module> KeyError: 'missing' 

El diccionario regular my_regular_dict plantea un KeyError cuando intentamos acceder a una clave que no está presente.

defaultdict se comporta de forma diferente a un diccionario regular. En vez de plantear un KeyError sobre una clave que falta, defaultdict invoca el valor de marcador de posición sin argumentos para crear un nuevo objeto. En este caso list() para crear una lista vacía.

Continuando con nuestro ejemplo de acuario ficticio, digamos que tenemos una lista de tuplas de peces que representan el inventario de un acuario:

fish_inventory = [     ("Sammy", "shark", "tank-a"),     ("Jamie", "cuttlefish", "tank-b"),     ("Mary", "squid", "tank-a"), ] 

Existen tres peces en el acuario; sus nombres, especies y tanque se anotan en estas tres tuplas.

Nuestro objetivo es organizar nuestro inventario por tanque; queremos conocer la lista de peces presentes en cada tanque. En otras palabras, queremos un diccionario que asigne "tank-a" a ["Jamie", "Mary"] y "tank-b" a ["Jamie"].

Podemos usar defaultdict para agrupar los peces por tanque:

from collections import defaultdict  fish_inventory = [     ("Sammy", "shark", "tank-a"),     ("Jamie", "cuttlefish", "tank-b"),     ("Mary", "squid", "tank-a"), ] fish_names_by_tank = defaultdict(list) for name, species, tank in fish_inventory:     fish_names_by_tank[tank].append(name)  print(fish_names_by_tank) 

Cuando ejecutemos este código, veremos el siguiente resultado:

Outputdefaultdict(<class 'list'>, {'tank-a': ['Sammy', 'Mary'], 'tank-b': ['Jamie']}) 

fish_names_by_tank se declara como un defaultdict que va por defecto a insertar list() en vez de lanzar un KeyError. Ya que esto garantiza que cada clave en fish_names_by_tank apuntará a una lista, podemos invocar libremente .append() para añadir nombres a la lista de cada tanque.

defaultdict le ayuda aquí porque reduce la probabilidad de KeyErrors inesperados. Reducir los KeyErrors inesperados significa que su programa puede escribirse de forma más clara y con menos líneas. Más concretamente, el idioma defaultdict le permite evitar instanciar manualmente una lista vacía para cada tanque.

Sin defaultdict, el cuerpo de bucle for podría haber tenido un aspecto más similar a este:

More Verbose Example Without defaultdict

...  fish_names_by_tank = {} for name, species, tank in fish_inventory:     if tank not in fish_names_by_tank:       fish_names_by_tank[tank] = []     fish_names_by_tank[tank].append(name) 

Usar simplemente un diccionario regular (en vez de un defaultdict) significa que el cuerpo de bucle for siempre tiene que comprobar la existencia de tank dado en fish_names_by_tank. Solo tras haber verificado que tank ya está presente en fish_names_by_tank, o que ha sido inicializado con un [], podremos anexar el nombre del pez.

defaultdict puede ayudar a reducir el código de plantilla cuando se completan diccionarios, porque nunca plantea un KeyError.

Usar deque para añadir elementos de forma eficiente a cada lado de una colección

Las listas de Python son secuencias de elementos ordenadas mutables, o alterables. Python puede anexar a listas en tiempo constante (la longitud de la lista no tiene efecto sobre el tiempo en que se tarda en anexar), pero insertar al principio de una lista puede ser más lento (el tiempo que tarda aumenta a medida que la lista aumenta).

En términos de la notación Big O, anexar a una lista es una operación O(1) de tiempo constante. Insertar al principio de una lista, por el contrario, es más lento con el rendimiento de O(n).

Nota: Los ingenieros de software a menudo miden el rendimiento de los procedimientos usando algo llamado la notación “Big O”. Cuando el tamaño de una entrada no tiene efecto sobre el tiempo que se tarda en realizar un procedimiento, se dice que se ejecuta en tiempo constante u O(1) (“Big O de 1”) Como ha aprendido antes, Python puede anexar a listas con un rendimiento de tiempo constante, conocido como O(1).

A veces, el tamaño de una entrada afecta directamente a la cantidad de tiempo que se tarda en ejecutar un procedimiento. Insertar al principio de una lista Python, por ejemplo, es más lento cuantos más elementos haya en la lista. La notación Big O utiliza la letra n para representar el tamaño de la entrada. Esto significa que añadir elementos al principio de una lista Python se ejecuta en “tiempo lineal” u O(n) (“Big O de n”).

En general, los procedimientos O(1) son más rápidos que los procedimientos O(n).

Podemos insertar al principio de una lista Python:

favorite_fish_list = ["Sammy", "Jamie", "Mary"]  # O(n) performance favorite_fish_list.insert(0, "Alice")  print(favorite_fish_list) 

Si ejecutamos lo siguiente, veremos un resultado como el siguiente:

Output['Alice', 'Sammy', 'Jamie', 'Mary'] 

El método .insert(index, object) en la lista nos permite insertar "Alice" al principio de favorite_fish_list. Notablemente, sin embargo, insertar al principio de una lista tiene un rendimiento O(n). A medida que la longitud de favorite_fish_list aumenta, el tiempo para insertar un pez al principio de la lista aumentará proporcionalmente y tardará cada vez más.

deque (pronunciado “deck”) del módulo collections es un objeto tipo listado que nos permite insertar elementos al principio o al final de una secuencia con un rendimiento de tiempo constante O(1).

Insertar un elemento al principio de un deque:

from collections import deque  favorite_fish_deque = deque(["Sammy", "Jamie", "Mary"])  # O(1) performance favorite_fish_deque.appendleft("Alice")  print(favorite_fish_deque) 

Al ejecutar este código, veremos el siguiente resultado:

Outputdeque(['Alice', 'Sammy', 'Jamie', 'Mary']) 

Podemos instanciar un deque usando una colección de elementos preexistente, en este caso una lista de nombres de tres peces favoritos. Invocar el método appendleft de favorite_fish_deque nos permite insertar un elemento al principio de nuestra colección con un rendimiento O(1). El rendimiento O(1) significa que el tiempo que se requiere para añadir un elemento al principio de favorite_fish_deque no aumentará incluso si favorite_fish_deque tiene miles o millones de elementos.

Nota: Aunque deque añade entradas al principio de una secuencia de forma más eficiente que una lista, deque no realiza todas sus operaciones de forma más eficiente que una lista. Por ejemplo, acceder a un elemento aleatorio en un deque tiene un rendimiento O(n), pero acceder a un elemento aleatorio en una lista tiene un rendimiento O(1). Utilice deque cuando sea importante insertar o eliminar elementos de cualquier lado de su colección rápidamente. Podrá ver una comparativa completa del rendimiento de tiempo en la wiki de Python.

Conclusión

El módulo collections es una parte potente de la biblioteca estándar de Python que le permite trabajar con datos de forma concisa y eficiente. Este tutorial ha cubierto tres de las clases proporcionadas por el módulo collections incluyendo namedtuple, defaultdict y deque.

Desde aquí, puede usar la documentación del módulo collectionpara aprender más sobre otras clases y utilidades disponibles. Para obtener más información sobre Python en general, puede leer nuestra serie de tutoriales Cómo codificar en Python 3.