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.
Supongamos que recibes una cadena y quieres contar cuántas veces aparece cada letra. Hay varias formas en que puedes hacerlo:
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.
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.
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.
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.
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
.
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.
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:
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.
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.
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”.
De nuevo, el tiempo que inviertas haciendo una buena estructura puede reducir el tiempo que inviertas en depurar.
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.