Entendiendo El Módulo Collections de Python

Daniel Morales
Feb 16, 2021

Entendiendo El Módulo Collections de Python

Feb 16, 2021 14 minutes read

El módulo collections de Python tiene diferentes tipos de datos especializados que funcionando como contenedores y que pueden utilizarse para reemplazar los contenedores de propósito general de Python (`dict`, `tuple`, `list` y `set`). Estudiaremos las siguientes partes de este módulo:

- `ChainMap`
- `defaultdict`
- `deque`

Existe un submódulo de colecciones llamado abc o Abstract Base Classes. Estas no serán tratadas en este post. ¡Empecemos con el contenedor ChainMap!

ChainMap

Un ChainMap es una clase que proporciona la capacidad de enlazar múltiples mapeos juntos de tal manera que terminan siendo una sola unidad. Si miras la documentación, te darás cuenta de que acepta `**maps*`, lo que significa que un ChainMap aceptará cualquier número de mapeos o diccionarios y los convertirá en una única vista que podrás actualizar. Veamos un ejemplo para que puedas ver cómo funciona:

from collections import ChainMap
car_parts = {'hood': 500, 'engine': 5000, 'front_door': 750}
car_options = {'A/C': 1000, 'Turbo': 2500, 'rollbar': 300}
car_accessories = {'cover': 100, 'hood_ornament': 150, 'seat_cover': 99}
car_pricing = ChainMap(car_accessories, car_options, car_parts)

car_pricing
>> ChainMap({'cover': 100, 'hood_ornament': 150, 'seat_cover': 99}, {'A/C': 1000, 'Turbo': 2500, 'rollbar': 300}, {'hood': 500, 'engine': 5000, 'front_door': 750})
car_pricing['hood']
>> 500

Aquí importamos ChainMap de nuestro módulo de colecciones. A continuación creamos tres diccionarios. Luego creamos una instancia de nuestro ChainMap pasándole los tres diccionarios que acabamos de crear.

Por último, intentamos acceder a una de las claves de nuestro ChainMap. Cuando hacemos esto, el ChainMap recorrerá cada mapa para ver si esa clave existe y tiene un valor. Si lo tiene, entonces el ChainMap devolverá el primer valor que encuentre que coincida con esa clave.

Esto es especialmente útil si quiere establecer valores por defecto. Supongamos que queremos crear una aplicación que tiene algunos valores por defecto. La aplicación también conocerá las variables de entorno del sistema operativo. Si hay una variable de entorno que coincide con una de las claves que tenemos por defecto en nuestra aplicación, el entorno anulará nuestro valor por defecto. Además, vamos a suponer que podemos pasar argumentos a nuestra aplicación. 

Estos argumentos tienen prioridad sobre el entorno y los valores por defecto. Este es un lugar donde un ChainMap puede realmente ser útil. Veamos un ejemplo sencillo que está basado en uno de la documentación de Python:

Nota: no ejecutes este código desde Jupyter Notebook, sino desde tu IDE favorito y llamandolo desde una terminal. de esta mandera `python chain_map.py -u daniel`

import argparse
import os

from collections import ChainMap

def main():
    app_defaults = {'username':'admin', 'password':'admin'}

    parser = argparse.ArgumentParser()
    parser.add_argument('-u', '--username')
    parser.add_argument('-p', '--password')
    args = parser.parse_args()

    command_line_arguments = {key:value for key, value in vars(args).items() if value}

    chain = ChainMap(command_line_arguments, os.environ, app_defaults)
    print(chain['username'])

if __name__ == '__main__':
    main()
    os.environ['username'] = 'test'
    main()

➜  python python3 post.py -u daniel       
daniel
daniel

Vamos a desglosar esto un poco. Aquí importamos el módulo `argparse` de Python junto con el módulo `os`. También importamos `ChainMap`.

A continuación tenemos una función simple que tiene algunos valores predeterminados. He visto estos valores por defecto utilizados para algunos enrutadores populares. Luego configuramos nuestro analizador de argumentos y le decimos cómo manejar ciertas opciones de la línea de comandos. Notarás que argparse no proporciona una forma de obtener un objeto diccionario de sus argumentos, así que usamos un dict comprehension para extraer lo que necesitamos. 

La otra pieza interesante aquí es el uso de las vars incorporadas de Python. Si lo llamaras sin un argumento vars se comportaría como los locales incorporados de Python. Pero si le pasas un objeto, entonces vars es el equivalente a la propiedad `__dict__` de object. En otras palabras, vars(args) es igual a `args.__dict__`. 

Finalmente creamos nuestro ChainMap pasando nuestros argumentos de la línea de comandos (si hay alguno), luego las variables de entorno y finalmente los valores por defecto.

Al final del código, intentamos llamar a nuestra función, luego establecer una variable de entorno y llamarla de nuevo. Pruébalo y verás que imprime admin y luego prueba como se esperaba. Ahora vamos a intentar llamar al script con un argumento de línea de comandos:

python chain_map.py -u daniel

Cuando yo ejecuto esto en mi maquina, me retorna  el daniel dos veces. Esto se debe a que nuestro argumento de línea de comandos anula todo lo demás. No importa que establezcamos el entorno porque nuestro ChainMap mirará primero los argumentos de la línea de comandos antes que cualquier otra cosa. Si lo intentas sin el `-u daniel` correrán los argumentos reales, en mi caso `"admin" "test"`

Ahora que sabes cómo usar ChainMaps, ¡podemos pasar al Counter!

Contador (`Counter`)

El módulo de colecciones también nos proporciona una pequeña herramienta que permite realizar cómodos y rápidos recuentos. Esta herramienta se llama `Counter`. Puedes ejecutarla con la mayoría de los iterables. Vamos a probarlo con un string

from collections import Counter

Counter('superfluous')
>> 
Counter({'s': 2, 'u': 3, 'p': 1, 'e': 1, 'r': 1, 'f': 1, 'l': 1, 'o': 1})counter = Counter('superfluous')
counter['u']
>> 3

En este ejemplo, importamos `Counter` de `collections` y le pasamos una cadena. Esto devuelve un objeto Counter que es una subclase del diccionario de Python. Luego ejecutamos el mismo comando pero lo asignamos a la variable counter para que podamos acceder al diccionario más fácilmente. En este caso, hemos visto que la letra `"u"` aparece tres veces en la cadena de ejemplo.

El contador proporciona algunos métodos que pueden interesarle. Por ejemplo, puede llamar a elementos que obtendrá un iterador sobre los elementos que están en el diccionario, pero en un orden arbitrario. Esta función se puede considerar como un "codificador", ya que la salida en este caso es una versión codificada de la cadena.

list(counter.elements())
>> ['s', 's', 'u', 'u', 'u', 'p', 'e', 'r', 'f', 'l', 'o']
Otro método útil es most_common. Puede preguntarle al Contador cuáles son los elementos más comunes pasando un número que represente cuáles son los elementos `"n"` más recurrentes:

counter.most_common(2)
[('u', 3), ('s', 2)]

Aquí sólo preguntamos a nuestro Counter cuáles fueron los dos elementos más recurrentes. Como puedes ver, se produjo una lista de tuplas que nos dice que `"u"` ocurrió 3 veces y `"s"` ocurrió dos veces.

El otro método que quiero cubrir es el método de sustracción. El método `subtract` acepta un iterable o un mapeo y utiliza ese argumento para restar. Es un poco más fácil de explicar si ves algo de código:

counter_one = Counter('superfluous')
counter_one
>> Counter({'s': 2, 'u': 3, 'p': 1, 'e': 1, 'r': 1, 'f': 1, 'l': 1, 'o': 1})

counter_two = Counter('super')
counter_one.subtract(counter_two)
counter_one
>> Counter({'s': 1, 'u': 2, 'p': 0, 'e': 0, 'r': 0, 'f': 1, 'l': 1, 'o': 1})
Así que aquí recreamos nuestro primer contador y lo imprimimos para saber qué hay en él. Así creamos nuestro segundo objeto Contador. Finalmente restamos el segundo contador del primero. Si miras con atención en la salida al final, notará que el número de letras de cinco de los elementos ha sido disminuido en uno.

Como mencioné al principio de esta sección, puedes usar el Contador contra cualquier iterable o mapeo, por lo que no tienes que usar sólo cadenas. También puedes pasarle tuplas, diccionarios y listas.

Pruébalo por tu cuenta para ver cómo funciona con esos otros tipos de datos. ¡Ahora estamos listos para pasar al `defaultdict`!

`defaultdict`

El módulo de colecciones tiene una práctica herramienta llamada `defaultdict`. El `defaultdict` es una subclase del dict de Python que acepta un `default_factory` como argumento principal. El `default_factory` suele ser un tipo de datos de Python, como int o una lista, pero también puedes usar una función o un lambda. Empecemos por crear un diccionario regular de Python que cuenta el número de veces que se utiliza cada palabra en una frase:

sentence = "The red for jumped over the fence and ran to the zoo for food"
words = sentence.split(' ')
words
>> ['The',
 'red',
 'for',
 'jumped',
 'over',
 'the',
 'fence',
 'and',
 'ran',
 'to',
 'the',
 'zoo',
 'for',
 'food']

reg_dict = {}
for word in words:
    if word in reg_dict:
        reg_dict[word] += 1
    else:
        reg_dict[word] = 1

print(reg_dict)
>> {'The': 1, 'red': 1, 'for': 2, 'jumped': 1, 'over': 1, 'the': 2, 'fence': 1, 'and': 1, 'ran': 1, 'to': 1, 'zoo': 1, 'food': 1}

¡Ahora vamos a intentar hacer lo mismo con defaultdict!

from collections import defaultdict
sentence = "The red for jumped over the fence and ran to the zoo for food"
words = sentence.split(' ')

d = defaultdict(int)
for word in words:
    d[word] += 1

print(d)
>> defaultdict(<class 'int'>, {'The': 1, 'red': 1, 'for': 2, 'jumped': 1, 'over': 1, 'the': 2, 'fence': 1, 'and': 1, 'ran': 1, 'to': 1, 'zoo': 1, 'food': 1})

Notarás enseguida que el código es mucho más sencillo. El defaultdict asignará automáticamente cero como valor a cualquier clave que no tenga ya en él. Nosotros añadimos uno para que tenga más sentido y también se incrementará si la palabra aparece varias veces en la frase.

Ahora vamos a intentar utilizar un tipo de lista de Python como nuestro `default_factory`. Empezaremos con un diccionario regular, como antes.

my_list = [(1234, 100.23), (345, 10.45), (1234, 75.00), (345, 222.66), (678, 300.25), (1234, 35.67)]

reg_dict = {}
for acct_num, value in my_list:
    if acct_num in reg_dict:
        reg_dict[acct_num].append(value)
    else:
        reg_dict[acct_num] = [value]

Si ejecuta este código, debería obtener una salida similar a la siguiente:

print(reg_dict)
>> {1234: [100.23, 75.0, 35.67], 345: [10.45, 222.66], 678: [300.25]}
Ahora vamos a reimplementar este código usando defaultdict:

from collections import defaultdict

my_list = [(1234, 100.23), (345, 10.45), (1234, 75.00), (345, 222.66), (678, 300.25), (1234, 35.67)]

d = defaultdict(list)
for acct_num, value in my_list:
    d[acct_num].append(value)

Una vez más, esto elimina la lógica condicional if/else y hace que el código sea más fácil de seguir. Aquí está la salida del código anterior:

print(d)
>> defaultdict(<class 'list'>, {1234: [100.23, 75.0, 35.67], 345: [10.45, 222.66], 678: [300.25]})

¡Esto es algo muy bueno! ¡Vamos a probar a usar un `lambda` también como nuestra `default_factory`!

from collections import defaultdict
animal = defaultdict(lambda: "Monkey")
animal
>> defaultdict(<function __main__.<lambda>()>, {})
animal['Sam'] = 'Tiger'
print (animal['Nick'])
>> Monkey
animal
>> defaultdict(<function __main__.<lambda>()>, {'Sam': 'Tiger', 'Nick': 'Monkey'})

Aquí creamos un `defaultdict` que asignará 'Monkey' como valor por defecto a cualquier clave. La primera llave la ponemos como 'Tiger', y la siguiente no la ponemos. Si imprimes la segunda llave, verás que tiene asignado 'Monkey'. 

En caso de que no lo hayas notado aún, es básicamente imposible causar un KeyError siempre y cuando establezcas el `default_factory` a algo que tenga sentido. La documentación menciona que si usted establece el `default_factory` a `None`, entonces recibirá un KeyError.

Veamos cómo funciona eso:

from collections import defaultdict
x = defaultdict(None)
x['Mike']

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-30-d21c3702d01d> in <module>
      1 from collections import defaultdict
      2 x = defaultdict(None)
----> 3 x['Mike']

KeyError: 'Mike'

En este caso, acabamos de crear un `defaultdict` con un error. Ya no puede asignar un valor por defecto a nuestra llave, así que lanza un `KeyError` en su lugar. Por supuesto, ya que es una subclase de `dict`, podemos simplemente establecer la llave con algún valor y funcionará. Pero eso anula el propósito de `defaultdict`.

`deque`

Según la documentación de Python, los deques "son una generalización de las pilas y colas (stacks y queues)". Se pronuncia "deck", que es la abreviatura de "double-ended queue". Son un contenedor de reemplazo para la lista de Python. Los deques son seguros para los hilos (threads) y permiten añadir y sacar datos de la memoria de forma eficiente desde cualquier lado del deque. 

Una lista está optimizada para operaciones rápidas de longitud fija. Puede obtener todos los detalles detalles en la documentación de Python. Un deque acepta un argumento `maxlen` que establece los límites para el deque. En caso contrario, el deque crecerá hasta un tamaño arbitrario. Cuando un deque acotado está lleno, cualquier nuevo elemento que se añada provocará que el mismo número de elementos salga del otro extremo.

Como regla general, si necesitas añadir o sacar elementos rápidamente, utiliza un deque. Si necesitas un acceso aleatorio rápido usa una lista. Tomemos un momento para ver cómo se puede crear y utilizar un deque.

from collections import deque
import string

d = deque(string.ascii_lowercase)

for letter in d:
    print(letter)

>> a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z
Aquí importamos el deque de nuestro módulo de colecciones y también importamos el módulo de strings. Para crear una instancia de un deque, necesitamos pasarle un iterable. En este caso, le pasamos `string.ascii_lowercase`, que devuelve una lista de todas las letras minúsculas del alfabeto. Finalmente, hacemos un bucle sobre nuestro deque e imprimimos cada elemento. Ahora veamos algunos de los métodos que posee deque.

d.append('bye')
d
>> deque(['a',
       'b',
       'c',
       'd',
       'e',
       'f',
       'g',
       'h',
       'i',
       'j',
       'k',
       'l',
       'm',
       'n',
       'o',
       'p',
       'q',
       'r',
       's',
       't',
       'u',
       'v',
       'w',
       'x',
       'y',
       'z',
       'bye'])

d.appendleft('hello')
d
>> deque(['hello',
       'a',
       'b',
       'c',
       'd',
       'e',
       'f',
       'g',
       'h',
       'i',
       'j',
       'k',
       'l',
       'm',
       'n',
       'o',
       'p',
       'q',
       'r',
       's',
       't',
       'u',
       'v',
       'w',
       'x',
       'y',
       'z',
       'bye'])

d.rotate(1)
d
>> deque(['bye',
       'hello',
       'a',
       'b',
       'c',
       'd',
       'e',
       'f',
       'g',
       'h',
       'i',
       'j',
       'k',
       'l',
       'm',
       'n',
       'o',
       'p',
       'q',
       'r',
       's',
       't',
       'u',
       'v',
       'w',
       'x',
       'y',
       'z'])

Vamos a desglosar esto un poco. Primero añadimos una cadena al extremo derecho del deque. Luego añadimos otra cadena al lado izquierdo del deque. Por último, llamamos a `rotate` en nuestro deque y le pasamos un uno, lo que hace que rote una vez a la derecha. 

En otras palabras, hace que un elemento gire desde el extremo derecho y en la parte delantera. Puedes pasarle un número negativo para que el deque rote hacia la izquierda en su lugar. 

Terminemos esta sección con un ejemplo basado en algo de la documentación de Python

from collections import deque
def get_last(filename, n=5):
    """
    Returns the last n lines from the file
    """
    try:
        with open(filename) as f:
            return deque(f, n)
    except OSError:
        print("Error opening file: {}".format(filename))
        raise

Este código funciona de forma muy parecida a como lo hace el programa tail de Linux. Aquí pasamos un `filename` a nuestro script junto con el número `n` de líneas que queremos que nos retorne. 

El deque está limitado a cualquier número que pasemos como `n`. Esto significa que una vez que el deque está lleno, cuando se leen nuevas líneas y se añaden al deque, las líneas más viejas son sacadas del otro extremo y descartadas. 

También he envuelto la apertura del archivo con un simple manejador de excepciones porque es muy fácil pasar una ruta malformada. Esto atrapará archivos que no existen.

Conclusión

Hemos cubierto mucho terreno en este post. Has aprendido a utilizar un defaultdict y un Count. También aprendimos sobre una subclase de la lista de Python, el deque. Por último, hemos visto cómo utilizarlas para realizar varias actividades. Espero que hayas encontrado cada una de estas colecciones interesantes. Pueden ser de gran utilidad para ti en tu dia a dia
Join our private community in Discord

Keep up to date by participating in our global community of data scientists and AI enthusiasts. We discuss the latest developments in data science competitions, new techniques for solving complex challenges, AI and machine learning models, and much more!