- `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
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
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
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`)
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
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']
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})
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`
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]}
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`
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
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.