Tutorial
Cómo usar el módulo collections en Python 3
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:
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:
...
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 collection
para 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.