Diccionarios

Un diccionario es como una lista, pero más general. En una lista, los índices de posiciones tienen que ser enteros; en un diccionario, los índices pueden ser (casi) cualquier tipo.

Puedes pensar en un diccionario como una asociación entre un conjunto de índices (que son llamados claves) y un conjunto de valores. Cada clave apunta a un valor. La asociación de una clave y un valor es llamada par clave-valor o a veces elemento.

Como ejemplo, vamos a construir un diccionario que asocia palabras de Inglés a Español, así que todas las claves y los valores son cadenas.

La función dict crea un nuevo diccionario sin elementos. Debido a que dict es el nombre de una función interna, deberías evitar usarlo como un nombre de variable.

>>> eng2sp = dict()
>>> print(eng2sp)
{}

Las llaves, {}, representan un diccionario vacío. Para agregar elementos a un diccionario, puedes utilizar corchetes:

>>> eng2sp['one'] = 'uno'

Esta línea crea un elemento asociando a la clave 'one' el valor “uno”. Si imprimimos el diccionario de nuevo, vamos a ver un par clave-valor con dos puntos entre la clave y el valor:

>>> print(eng2sp)
{'one': 'uno'}

Este formato de salida es también un formato de entrada. Por ejemplo, puedes crear un nuevo diccionario con tres elementos. Pero si imprimes eng2sp, te vas a sorprender:

>>> eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}
>>> print(eng2sp)
{'one': 'uno', 'three': 'tres', 'two': 'dos'}

El orden de los pares clave-elemento no es el mismo. De hecho, si tu escribes este mismo ejemplo en tu computadora, podrías obtener un resultado diferente. En general, el orden de los elementos en un diccionario es impredecible.

Pero ese no es un problema porque los elementos de un diccionario nunca son indexados con índices enteros. En vez de eso, utilizas las claves para encontrar los valores correspondientes:

>>> print(eng2sp['two'])
'dos'

La clave 'two' siempre se asocia al valor “dos”, así que el orden de los elementos no importa.

Si la clave no está en el diccionario, obtendrás una excepción (exception):

>>> print(eng2sp['four'])
KeyError: 'four'

La función len funciona en diccionarios; ésta regresa el número de pares clave-valor:

>>> len(eng2sp)
3

El operador in funciona en diccionarios; éste te dice si algo aparece como una clave en el diccionario (aparecer como valor no es suficiente).

>>> 'one' in eng2sp
True
>>> 'uno' in eng2sp
False

Para ver si algo aparece como valor en un diccionario, puedes usar el método values, el cual retorna los valores como una lista, y después puedes usar el operador in:

>>> vals = list(eng2sp.values())
>>> 'uno' in vals
True

El operador in utiliza diferentes algoritmos para listas y diccionarios. Para listas, utiliza un algoritmo de búsqueda lineal. Conforme la lista se vuelve más grande, el tiempo de búsqueda se vuelve más largo en proporción al tamaño de la lista. Para diccionarios, Python utiliza un algoritmo llamado tabla hash (hash table, en inglés) que tiene una propiedad importante: el operador in toma la misma cantidad de tiempo sin importar cuántos elementos haya en el diccionario. No voy a explicar porqué las funciones hash son tan mágicas, pero puedes leer más al respecto en es.wikipedia.org/wiki/Tabla_hash.

Ejercicio 1: Descargar una copia del archivo www.py4e.com/code3/words.txt

Escribe un programa que lee las palabras en words.txt y las almacena como claves en un diccionario. No importa qué valores tenga. Luego puedes utilizar el operador in como una forma rápida de revisar si una cadena está en el diccionario.

Diccionario como un conjunto de contadores

Supongamos que recibes una cadena y quieres contar cuántas veces aparece cada letra. Hay varias formas en que puedes hacerlo:

  1. Puedes crear 26 variables, una por cada letra del alfabeto. Luego puedes recorrer la cadena, y para cada caracter, incrementar el contador correspondiente, probablemente utilizando varios condicionales.

  2. Puedes crear una lista con 26 elementos. Después podrías convertir cada caracter en un número (usando la función interna ord), usar el número como índice dentro de la lista, e incrementar el contador correspondiente.

  3. Puedes crear un diccionario con caracteres como claves y contadores como los valores correspondientes. La primera vez que encuentres un caracter, agregarías un elemento al diccionario. Después de eso incrementarías el valor del elemento existente.

Cada una de esas opciones hace la misma operación computacional, pero cada una de ellas implementa esa operación en forma diferente.

Una implementación es una forma de llevar a cabo una operación computacional; algunas implementaciones son mejores que otras. Por ejemplo, una ventaja de la implementación del diccionario es que no tenemos que saber con antelación qué letras aparecen en la cadena y solamente necesitamos espacio para las letras que sí aparecen.

Aquí está un ejemplo de como se vería ese código:

palabra = 'brontosaurio'
d = dict()
for c in palabra:
    if c not in d:
        d[c] = 1
    else:
        d[c] = d[c] + 1
print(d)

Realmente estamos calculando un histograma, el cual es un término estadístico para un conjunto de contadores (o frecuencias).

El bucle for recorre la cadena. Cada vez que entramos al bucle, si el caracter c no está en el diccionario, creamos un nuevo elemento con la clave c y el valor inicial 1 (debido a que hemos visto esta letra solo una vez). Si c ya está previamente en el diccionario incrementamos d[c].

Aquí está la salida del programa:

{'b': 1, 'r': 2, 'o': 3, 'n': 1, 't': 1, 's': 1, 'a': 1, 'u': 1, 'i': 1}

El histograma indica que las letras “a” y “b” aparecen solo una vez; “o” aparece dos, y así sucesivamente.

Los diccionarios tienen un método llamado get que toma una clave y un valor por defecto. Si la clave aparece en el diccionario, get regresa el valor correspondiente; si no, regresa el valor por defecto. Por ejemplo:

>>> cuentas = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
>>> print(cuentas.get('jan', 0))
100
>>> print(cuentas.get('tim', 0))
0

Podemos usar get para escribir nuestro bucle de histograma más conciso. Puesto que el método get automáticamente maneja el caso en que una clave no está en el diccionario, podemos reducir cuatro líneas a una y eliminar la sentencia if.

palabra = 'brontosaurio'
d = dict()
for c in palabra:
    d[c] = d.get(c,0) + 1
print(d)

El uso del método get para simplificar este bucle contador termina siendo un “idioma” muy utilizado en Python y vamos a utilizarlo muchas veces en el resto del libro. Así que deberías tomar un momento para comparar el bucle utilizando la sentencia if y el operador in con el bucle utilizando el método get. Ambos hacen exactamente lo mismo, pero uno es más breve.

Diccionarios y archivos

Uno de los usos más comunes de un diccionario es contar las ocurrencias de palabras en un archivo con algún texto escrito. Vamos comenzando con un archivo de palabras muy simple tomado del texto de Romeo y Julieta.

Para el primer conjunto de ejemplos, vamos a usar una versión más corta y más simplificada del texto sin signos de puntuación. Después trabajaremos con el texto de la escena con signos de puntuación incluidos.

But soft what light through yonder window breaks
It is the east and Juliet is the sun
Arise fair sun and kill the envious moon
Who is already sick and pale with grief

Vamos a escribir un programa de Python para leer a través de las líneas del archivo, dividiendo cada línea en una lista de palabras, y después iterando a través de cada una de las palabras en la línea y contando cada palabra utilizando un diccionario.

Verás que tenemos dos bucles for. El bucle externo está leyendo las líneas del archivo y el bucle interno está iterando a través de cada una de las palabras en esa línea en particular. Este es un ejemplo de un patrón llamado bucles anidados porque uno de los bucles es el bucle externo y el otro bucle es el bucle interno.

Como el bucle interno ejecuta todas sus iteraciones cada vez que el bucle externo hace una sola iteración, consideramos que el bucle interno itera “más rápido” y el bucle externo itera más lento.

La combinación de los dos bucles anidados asegura que contemos cada palabra en cada línea del archivo de entrada.

fname = input('Ingresa el nombre de archivo: ')
try:
    fhand = open(fname)
except:
    print('El archivo no se puede abrir:', fname)
    exit()

counts = dict()
for line in fhand:
    words = line.split()
    for word in words:
        if word not in counts:
            counts[word] = 1
        else:
            counts[word] += 1

print(counts)

# Código: https://es.py4e.com/code3/count1.py

En nuestra sentencia else, utilizamos la alternativa más compacta para incrementar una variable. counts[word] += 1 es equivalente a counts[word] = counts[word] + 1. Cualquiera de los dos métodos puede usarse para cambiar el valor de una variable en cualquier cantidad. Existen alternativas similares para -=, *=, y /=.

Cuando ejecutamos el programa, vemos una salida sin procesar que contiene todos los contadores sin ordenar. (el archivo romeo.txt está disponible en es.py4e.com/code3/romeo.txt)

python count1.py
Ingresa el nombre de archivo: romeo.txt
{'and': 3, 'envious': 1, 'already': 1, 'fair': 1,
'is': 3, 'through': 1, 'pale': 1, 'yonder': 1,
'what': 1, 'sun': 2, 'Who': 1, 'But': 1, 'moon': 1,
'window': 1, 'sick': 1, 'east': 1, 'breaks': 1,
'grief': 1, 'with': 1, 'light': 1, 'It': 1, 'Arise': 1,
'kill': 1, 'the': 3, 'soft': 1, 'Juliet': 1}

Es un poco inconveniente ver a través del diccionario para encontrar las palabras más comunes y sus contadores, así que necesitamos agregar un poco más de código para mostrar una salida que nos sirva más.

Bucles y diccionarios

Si utilizas un diccionario como una secuencia para una sentencia for, esta recorre las claves del diccionario. Este bucle imprime cada clave y su valor correspondiente:

contadores = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
for clave in contadores:
    print(clave, contadores[clave])

Aquí está lo que muestra de salida:

jan 100
chuck 1
annie 42

De nuevo, las claves no están en ningún orden en particular.

Podemos utilizar este patrón para implementar varios idiomas de bucles que hemos descrito previamente. Por ejemplo, si queremos encontrar todas las entradas en un diccionario con valor mayor a diez, podemos escribir el siguiente código:

contadores = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
for clave in contadores:
    if contadores[clave] > 10 :
        print(clave, contadores[clave])

El bucle for itera a través de las claves del diccionario, así que debemos utilizar el operador índice para obtener el valor correspondiente para cada clave. Aquí está la salida del programa:

jan 100
annie 42

Vemos solamente las entradas que tienen un valor mayor a 10.

Si quieres imprimir las claves en orden alfabético, primero haces una lista de las claves en el diccionario utilizando el método keys disponible en los objetos de diccionario, y después ordenar esa lista e iterar a través de la lista ordenada, buscando cada clave e imprimiendo pares clave-valor ordenados, tal como se muestra a continuación:

contadores = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
lst = list(contadores.keys())
print(lst)
lst.sort()
for clave in lst:
    print(clave, contadores[clave])

Así se muestra la salida:

['jan', 'chuck', 'annie']
annie 42
chuck 1
jan 100

Primero se ve la lista de claves sin ordenar como la obtuvimos del método keys. Después vemos los pares clave-valor en orden desde el bucle for.

Análisis avanzado de texto

En el ejemplo anterior utilizando el archivo romeo.txt, hicimos el archivo tan simple como fue posible removiendo los signos de puntuación a mano. El text real tiene muchos signos de puntuación, como se muestra abajo.

But, soft! what light through yonder window breaks?
It is the east, and Juliet is the sun.
Arise, fair sun, and kill the envious moon,
Who is already sick and pale with grief,

Puesto que la función split en Python busca espacios y trata las palabras como piezas separadas por esos espacios, trataríamos a las palabras “soft!” y “soft” como diferentes palabras y crearíamos una entrada independiente para cada palabra en el diccionario.

Además, como el archivo tiene letras mayúsculas, trataríamos “who” y “Who” como diferentes palabras con diferentes contadores.

Podemos resolver ambos problemas utilizando los métodos de cadenas lower, punctuation, y translate. El método translate es el más sutil de los métodos. Aquí esta la documentación para translate:

line.translate(str.maketrans(fromstr, tostr, deletestr))

Reemplaza los caracteres en fromstr con el caracter en la misma posición en tostr y elimina todos los caracteres que están en deletestr. Los parámetros fromstr y tostr pueden ser cadenas vacías y el parámetro deletestr es opcional.

No vamos a especificar el valor de tostr pero vamos a utilizar el parámetro deletestr para eliminar todos los signos de puntuación. Incluso vamos a dejar que Python nos diga la lista de caracteres que considera como “signos de puntuación”:

>>> import string
>>> string.punctuation
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'

Los parámetros utilizados por translate eran diferentes en Python 2.0.

Hacemos las siguientes modificaciones a nuestro programa:

import string

fname = input('Ingresa el nombre de archivo: ')
try:
    fhand = open(fname)
except:
    print('El archivo no se puede abrir:', fname)
    exit()

counts = dict()
for line in fhand:
    line = line.rstrip()
    line = line.translate(line.maketrans('', '', string.punctuation))
    line = line.lower()
    words = line.split()
    for word in words:
        if word not in counts:
            counts[word] = 1
        else:
            counts[word] += 1

print(counts)

# Código: https://es.py4e.com/code3/count2.py

Parte de aprender el “Arte de Python” o “Pensamiento Pythónico” es entender que Python muchas veces tiene funciones internas para muchos problemas de análisis de datos comunes. A través del tiempo, verás suficientes códigos de ejemplo y leerás lo suficiente en la documentación para saber dónde buscar si alguien escribió algo que haga tu trabajo más fácil.

Lo siguiente es una versión reducida de la salida:

Ingresa el nombre de archivo: romeo-full.txt
{'swearst': 1, 'all': 6, 'afeard': 1, 'leave': 2, 'these': 2,
'kinsmen': 2, 'what': 11, 'thinkst': 1, 'love': 24, 'cloak': 1,
a': 24, 'orchard': 2, 'light': 5, 'lovers': 2, 'romeo': 40,
'maiden': 1, 'whiteupturned': 1, 'juliet': 32, 'gentleman': 1,
'it': 22, 'leans': 1, 'canst': 1, 'having': 1, ...}

Interpretar los datos a través de esta salida es aún difícil, y podemos utilizar Python para darnos exactamente lo que estamos buscando, pero para que sea así, necesitamos aprender acerca de las tuplas en Python. Vamos a retomar este ejemplo una vez que aprendamos sobre tuplas.

Depuración

Conforme trabajes con conjuntos de datos más grandes puede ser complicado depurar imprimiendo y revisando los datos a mano. Aquí hay algunas sugerencias para depurar grandes conjuntos de datos:

Reducir la entrada

Si es posible, trata de reducir el tamaño del conjunto de datos. Por ejemplo, si el programa lee un archivo de texto, comienza solamente con las primeras 10 líneas, o con el ejemplo más pequeño que puedas encontrar. Puedes ya sea editar los archivos directamente, o (mejor) modificar el programa para que solamente lea las primeras n número de líneas.

Si hay un error, puedes reducir n al valor más pequeño que produce el error, y después incrementarlo gradualmente conforme vayas encontrando y corrigiendo errores.

Revisar extractos y tipos

En lugar de imprimir y revisar el conjunto de datos completo, considera imprimir extractos de los datos: por ejemplo, el número de elementos en un diccionario o el total de una lista de números.

Una causa común de errores en tiempo de ejecución es un valor que no es el tipo correcto. Para depurar este tipo de error, generalmente es suficiente con imprimir el tipo de un valor.

Escribe auto-verificaciones

Algunas veces puedes escribir código para revisar errores automáticamente. Por ejemplo, si estás calculando el promedio de una lista de números, podrías verificar que el resultado no sea más grande que el elemento más grande de la lista o que sea menor que el elemento más pequeño de la lista. Esto es llamado “prueba de sanidad” porque detecta resultados que son “completamente ilógicos”.

Otro tipo de prueba compara los resultados de dos diferentes cálculos para ver si son consistentes. Esto es conocido como “prueba de consistencia”.

Imprimir una salida ordenada
Dar un formato a los mensajes de depuración puede facilitar encontrar un error.

De nuevo, el tiempo que inviertas haciendo una buena estructura puede reducir el tiempo que inviertas en depurar.

Glosario

bucles anidados
Cuando hay uno o más bucles “dentro” de otro bucle. Los bucles internos terminan de ejecutar cada vez que el bucle externo ejecuta una vez.
búsqueda
Una operación de diccionario que toma una clave y encuentra su valor correspondiente.
clave
Un objeto que aparece en un diccionario como la primera parte de un par clave-valor.
diccionario
Una asociación de un conjunto de claves a sus valores correspondientes.
elemento
Otro nombre para un par clave-valor.
función hash
Una función utilizada por una tabla hash para calcular la localización de una clave.
histograma
Un set de contadores.
implementación
Una forma de llevar a cabo un cálculo.
par clave-valor
La representación de una asociación de una clave a un valor.
tabla hash
El algoritmo utilizado para implementar diccionarios en Python.
valor
Un objeto que aparece en un diccionario como la segunda parte de un par clave-valor. Esta definición es más específica que nuestro uso previo de la palabra “valor”.

Ejercicios

Ejercicio 2: Escribir un programa que clasifica cada mensaje de correo dependiendo del día de la semana en que se recibió. Para hacer esto busca las líneas que comienzan con “From”, después busca por la tercer palabra y mantén un contador para cada uno de los días de la semana. Al final del programa imprime los contenidos de tu diccionario (el orden no importa).

Línea de ejemplo:

From [email protected] Sat Jan  5 09:14:16 2008

Ejemplo de ejecución:

python dow.py
Ingresa un nombre de archivo: mbox-short.txt
{'Fri': 20, 'Thu': 6, 'Sat': 1}

Ejercicio 3: Escribe un programa para leer a través de un historial de correos, construye un histograma utilizando un diccionario para contar cuántos mensajes han llegado de cada dirección de correo electrónico, e imprime el diccionario.

Ingresa un nombre de archivo: mbox-short.txt
{'[email protected]': 1, '[email protected]': 3,
'[email protected]': 5, '[email protected]': 1,
'[email protected]': 2, '[email protected]': 3,
'[email protected]': 4, '[email protected]': 1,
'[email protected]': 4, '[email protected]': 2,
'[email protected]': 1}

Ejercicio 4: Agrega código al programa anterior para determinar quién tiene la mayoría de mensajes en el archivo. Después de que todos los datos hayan sido leídos y el diccionario haya sido creado, mira a través del diccionario utilizando un bucle máximo (ve Capítulo 5: Bucles máximos y mínimos) para encontrar quién tiene la mayoría de mensajes e imprimir cuántos mensajes tiene esa persona.

Ingresa un nombre de archivo: mbox-short.txt
[email protected] 5

Ingresa un nombre de archivo: mbox.txt
[email protected] 195

Ejercicio 5: Este programa almacena el nombre del dominio (en vez de la dirección) desde donde fue enviado el mensaje en vez de quién envió el mensaje (es decir, la dirección de correo electrónica completa). Al final del programa, imprime el contenido de tu diccionario.

python schoolcount.py
Ingresa un nombre de archivo: mbox-short.txt
{'media.berkeley.edu': 4, 'uct.ac.za': 6, 'umich.edu': 7,
'gmail.com': 1, 'caret.cam.ac.uk': 1, 'iupui.edu': 8}

Si encuentras un error en este libro, siéntete libre de enviarme una solución usando Github.