Contenedores asociativos en Python — teoría con una playlist, diccionarios dentro de diccionarios y conjuntos que se cruzan (y el contador que explotó en la primera canción)
Quería saber qué canciones escuchaba más mientras estudiaba. Tenía el historial en una lista y pensé: «un diccionario donde la clave sea la canción y el valor, las veces que la he escuchado; en cada vuelta le sumo 1 y listo». Tres líneas. Lo ejecuté con un historial de tres canciones:
historial = ["Luz de neón", "Café a las 3", "Luz de neón"]
reproducciones = {} # canción -> veces que la he escuchado
for cancion in historial:
reproducciones[cancion] += 1 # sumar 1 a la cuenta de esa canción
print(reproducciones)
Traceback (most recent call last):
File "gancho.py", line 5, in <module>
reproducciones[cancion] += 1 # sumar 1 a la cuenta de esa canción
~~~~~~~~~~~~~~^^^^^^^^^
KeyError: 'Luz de neón'
«¿KeyError? Si lo que quiero es precisamente añadirla.» El problema estaba en el += 1. Esa línea significa reproducciones[cancion] = reproducciones[cancion] + 1, y para hacer la suma Python primero tiene que leer cuánto vale reproducciones["Luz de neón"]. En la primera vuelta el diccionario está vacío: esa clave no existe, no hay nada que leer y Python se para con un KeyError. Escribir en una clave nueva está permitido; leer una clave que no existe, no.
Hoy entendemos qué son los contenedores asociativos: los diccionarios, que guardan parejas clave → valor, y los conjuntos, que guardan elementos sin repetir y se combinan como en matemáticas. Veremos cómo crearlos, cómo leer sin miedo al KeyError (con in y con get), cómo recorrer claves, valores y parejas, cómo meter diccionarios dentro de diccionarios y cómo calcular uniones, intersecciones y diferencias. El ejemplo que nos acompaña es una app de música: canciones, duraciones, playlists y los gustos de varios usuarios. Si vienes de recursividad, aquí cambiamos de tercio: menos llamadas y más estructuras de datos. Y cuando acabes, la práctica de contenedores asociativos te espera con tres programas completos.
Tabla de Contenidos
¿Qué es un contenedor asociativo?
Hasta ahora has guardado colecciones en listas y tuplas. En ellas cada elemento tiene una posición (0, 1, 2…) y llegas a él por su número: canciones[1]. Eso funciona muy bien cuando el orden es lo importante, pero piensa en esta pregunta: «¿cuánto dura Pixel?». Con una lista tendrías que recorrerla buscando el título y, al encontrarlo, mirar su duración en otra lista o en una tupla. Con un diccionario la pregunta se escribe tal cual: duraciones["Pixel"].
Un contenedor asociativo guarda los datos asociados a una clave en lugar de a una posición. Python trae dos:
- **Diccionario (
dict): parejas clave → valor**. Buscas por la clave y obtienes el valor. Como una agenda: buscas por el nombre y obtienes el teléfono. - **Conjunto (
set): solo claves, sin valores y sin repetidos**. Sirve para preguntar «¿está o no está?» y para combinar colecciones (lo que tienen en común, lo que solo tiene una…).
Las búsquedas por clave son muy rápidas, da igual que el diccionario tenga diez parejas o un millón. Python no recorre el diccionario para encontrar la clave: calcula a partir de ella dónde está guardada. Por eso las claves tienen que ser inmutables (números, strings, tuplas…), como veremos más abajo.
Crear un diccionario
Un diccionario se escribe entre llaves {}, con parejas clave: valor separadas por comas. Hay varias formas de construirlo:
vacio = {} # diccionario vacío, con llaves
tambien_vacio = dict() # o con el constructor dict()
print(type(vacio), len(vacio), len(tambien_vacio))
# Lo normal: parejas clave: valor separadas por comas
duraciones = {"Luz de neón": 215, "Café a las 3": 187, "Domingo lento": 242}
print(duraciones)
# Desde una lista de parejas (clave, valor)
parejas = [("pop", 12), ("rock", 7), ("indie", 9)]
por_genero = dict(parejas)
print(por_genero)
# Con dictionary comprehension: los minutos enteros de cada canción
minutos = {titulo: segundos // 60 for titulo, segundos in duraciones.items()}
print(minutos)
# Con parámetros por nombre (solo si las claves son palabras sencillas)
volumen = dict(ana=70, luis=45)
print(volumen)
<class 'dict'> 0 0
{'Luz de neón': 215, 'Café a las 3': 187, 'Domingo lento': 242}
{'pop': 12, 'rock': 7, 'indie': 9}
{'Luz de neón': 3, 'Café a las 3': 3, 'Domingo lento': 4}
{'ana': 70, 'luis': 45}
Vamos línea a línea:
{}ydict()crean un diccionario vacío. Los dos dan lo mismo:typedicedictylencuenta cero parejas.duracioneses la forma más habitual: escribes cada pareja con dos puntos entre la clave y el valor. Aquí la clave es el título (un string) y el valor, los segundos (un entero).dict(parejas)convierte una lista de tuplas de dos elementos en un diccionario: el primer elemento de cada tupla pasa a ser la clave y el segundo, el valor. Muy útil cuando los datos te llegan en forma de lista.- La dictionary comprehension funciona como las de listas, pero con
clave: valordelante delfor. Aquí recorreduraciones.items()(enseguida veremos qué es) y, para cada título, guarda los minutos enteros:215 // 60es 3. dict(ana=70, luis=45)usa parámetros por nombre: cada nombre se convierte en una clave de tipo string. Solo vale cuando las claves son palabras sencillas sin espacios ni acentos raros;dict(Luz de neón=215)no se puede escribir.
¿y si repites una clave al crear el diccionario, como {"pop": 1, "pop": 2}? No da error: se queda con el último valor. Un diccionario nunca tiene dos claves iguales.
Añadir, cambiar, leer y borrar
La gran ventaja del diccionario es que la misma sintaxis, diccionario[clave], sirve para casi todo:
duraciones = {"Luz de neón": 215, "Café a las 3": 187}
duraciones["Domingo lento"] = 242 # clave nueva: se AÑADE la pareja
duraciones["Café a las 3"] = 190 # clave que ya existe: se SUSTITUYE el valor
print(duraciones)
print(duraciones["Luz de neón"]) # leer el valor asociado a una clave
print(len(duraciones)) # cuántas parejas clave: valor hay
print("Pixel" in duraciones) # ¿existe la CLAVE "Pixel"?
print(215 in duraciones) # in mira las claves, nunca los valores
quitada = duraciones.pop("Luz de neón") # quita la pareja y DEVUELVE el valor
print(quitada, duraciones)
del duraciones["Café a las 3"] # quita la pareja sin devolver nada
print(duraciones)
duraciones.clear() # vacía el diccionario entero
print(duraciones)
{'Luz de neón': 215, 'Café a las 3': 190, 'Domingo lento': 242}
215
3
False
False
215 {'Café a las 3': 190, 'Domingo lento': 242}
{'Domingo lento': 242}
{}
Detengámonos en cada operación, porque aquí está la mitad del tema:
- Asignar a una clave nueva la añade.
duraciones["Domingo lento"] = 242crea la pareja, porque esa clave no existía. - Asignar a una clave que ya existe la sustituye.
duraciones["Café a las 3"] = 190no añade una segunda pareja: cambia el 187 por 190. Por eso el diccionario sigue teniendo tres parejas. - Leer es poner la clave entre corchetes sin asignar:
duraciones["Luz de neón"]devuelve 215. lencuenta parejas, no claves y valores por separado: 3.inpregunta si existe una clave. Fíjate en la segunda:215 in duracionesesFalseaunque 215 esté dentro, porque 215 es un valor, no una clave.insobre un diccionario solo mira las claves.pop(clave)quita la pareja y te devuelve el valor, por si lo necesitas (aquí, 215).deltambién la quita, pero no devuelve nada.clear()vacía el diccionario entero.
Desde Python 3.7, los diccionarios recuerdan el orden en que añadiste las claves. Por eso "Domingo lento" sale al final: fue la última en entrar. Cambiar el valor de una clave que ya existe no la mueve de sitio.
El KeyError: leer una clave que no existe
Esto es exactamente lo que le pasaba a mi contador:
duraciones = {"Luz de neón": 215}
print(duraciones["Pixel"])
Traceback (most recent call last):
File "error_key.py", line 2, in <module>
print(duraciones["Pixel"])
~~~~~~~~~~^^^^^^^^^
KeyError: 'Pixel'
El KeyError siempre te dice qué clave buscaste (aquí 'Pixel'). Cuando te salga, la pregunta no es «¿qué he roto?», sino «¿por qué esperaba que esa clave existiera?». Hay dos formas de evitarlo:
- **Preguntar antes con
in:**if "Pixel" in duraciones: print(duraciones["Pixel"]). - **Usar
get**, que es justo lo que vemos ahora.
Confundir escribir con leer. d["nueva"] = 1 siempre funciona (crea la clave). d["nueva"] += 1, print(d["nueva"]) o d["nueva"].append(…) necesitan leer primero, y fallan si la clave no existe.
get: leer sin miedo
get(clave, por_defecto) devuelve el valor de la clave si existe y, si no existe, devuelve el valor por defecto sin lanzar ningún error. Si no le das valor por defecto, devuelve None.
duraciones = {"Luz de neón": 215, "Café a las 3": 187}
print(duraciones.get("Luz de neón")) # si la clave existe: igual que [ ]
print(duraciones.get("Pixel")) # si no existe: None, y sin error
print(duraciones.get("Pixel", 0)) # o el valor por defecto que le digas
# El contador que fallaba al principio, arreglado con get
historial = ["Luz de neón", "Café a las 3", "Luz de neón", "Pixel", "Luz de neón"]
reproducciones = {}
for cancion in historial:
# get devuelve la cuenta que lleva la canción, o 0 si es la primera vez
reproducciones[cancion] = reproducciones.get(cancion, 0) + 1
print(reproducciones)
215
None
0
{'Luz de neón': 3, 'Café a las 3': 1, 'Pixel': 1}
La clave del arreglo está en esta línea:
reproducciones[cancion] = reproducciones.get(cancion, 0) + 1
Léela de derecha a izquierda: «lo que llevaba esta canción (o 0 si es la primera vez), más uno, se guarda como su nueva cuenta». Así queda la traza con el historial del programa:
| Vuelta | cancion | get(cancion, 0) devuelve | reproducciones después |
|---|---|---|---|
| 1 | "Luz de neón" | 0 (no existía) | {'Luz de neón': 1} |
| 2 | "Café a las 3" | 0 (no existía) | {'Luz de neón': 1, 'Café a las 3': 1} |
| 3 | "Luz de neón" | 1 | {'Luz de neón': 2, 'Café a las 3': 1} |
| 4 | "Pixel" | 0 (no existía) | {…, 'Pixel': 1} |
| 5 | "Luz de neón" | 2 | {'Luz de neón': 3, 'Café a las 3': 1, 'Pixel': 1} |
Este patrón, d[k] = d.get(k, 0) + 1, es el contador de Python. Lo vas a escribir tantas veces que acabarás haciéndolo sin pensar. Con listas como valor, el equivalente es d[k] = d.get(k, []) + [elemento].
¿in o get?
| Situación | Usa | Por qué |
|---|---|---|
| Solo quieres saber si existe la clave | clave in d | Devuelve True/False y no lee el valor |
| Quieres el valor, y si no existe te vale uno por defecto | d.get(clave, defecto) | Una sola línea, sin if |
| Si la clave no existe, hay que hacer algo distinto (avisar, crear varias cosas…) | if clave in d: … else: … | El if deja clara cada rama |
| Estás seguro de que la clave existe | d[clave] | Si no existe, el KeyError te avisa de un fallo real |
Recorrer un diccionario: keys, values e items
Un diccionario tiene tres «vistas» que puedes recorrer: sus claves, sus valores y sus parejas.
reproducciones = {"Luz de neón": 3, "Café a las 3": 1, "Pixel": 1, "Domingo lento": 4}
print(reproducciones.keys()) # una "vista" con las claves
print(list(reproducciones.keys())) # convertida en lista
print(list(reproducciones.values())) # todos los valores
print(list(reproducciones.items())) # todas las parejas, como tuplas
for cancion in reproducciones: # sin decir nada, recorre las CLAVES
print(cancion, end=" | ")
print() # salto de línea al terminar
for cancion, veces in reproducciones.items(): # clave y valor a la vez
print(f"{cancion}: {veces}")
print(sum(reproducciones.values())) # total de reproducciones
print(sorted(reproducciones)) # las claves, en orden alfabético
dict_keys(['Luz de neón', 'Café a las 3', 'Pixel', 'Domingo lento'])
['Luz de neón', 'Café a las 3', 'Pixel', 'Domingo lento']
[3, 1, 1, 4]
[('Luz de neón', 3), ('Café a las 3', 1), ('Pixel', 1), ('Domingo lento', 4)]
Luz de neón | Café a las 3 | Pixel | Domingo lento |
Luz de neón: 3
Café a las 3: 1
Pixel: 1
Domingo lento: 4
9
['Café a las 3', 'Domingo lento', 'Luz de neón', 'Pixel']
Lo que conviene tener claro:
keys(),values()eitems()no devuelven listas, sino vistas (dict_keys(…)). Una vista se puede recorrer confory preguntar conin, pero no se puede indexar con[0]. Si necesitas una lista de verdad, conviértela conlist(…).items()da tuplas(clave, valor). Por eso en elforse escribefor cancion, veces in …: cada tupla se desempaqueta en dos variables.- Un
forsin más (for cancion in reproducciones) recorre las claves. Es lo mismo quefor cancion in reproducciones.keys(). - Como
values()es una colección de números,sum,maxominfuncionan directamente con ella: 3 + 1 + 1 + 4 = 9 reproducciones. sorted(diccionario)devuelve una lista con las claves ordenadas. El diccionario original no cambia.
¿Cuál de las tres eliges? Depende de qué necesites dentro del bucle:
| Dentro del bucle necesito… | Recorre | Ejemplo |
|---|---|---|
| Solo la clave | for k in d | Mostrar los títulos |
| Solo el valor | for v in d.values() | Sumar duraciones |
| Las dos cosas | for k, v in d.items() | Imprimir «título: veces» |
Escribir for cancion, veces in reproducciones: (sin .items()). El for recorre las claves, que son strings, e intenta desempaquetar cada string en dos variables: ValueError: too many values to unpack.
Añadir o borrar claves mientras recorres el mismo diccionario. Python lo detecta y lanza RuntimeError: dictionary changed size during iteration. Si necesitas borrar, recorre una copia de las claves: for k in list(d):. Cambiar el valor de una clave que ya existe sí está permitido.
Diccionarios anidados
El valor de un diccionario puede ser cualquier cosa: un número, una lista… u otro diccionario. Así se representan datos con estructura, como el catálogo de una app de música:
canciones = {
"c01": {"titulo": "Luz de neón", "artista": "Marea Alta", "segundos": 215},
"c02": {"titulo": "Café a las 3", "artista": "Los Turnos", "segundos": 187},
"c03": {"titulo": "Domingo lento", "artista": "Noa Vela", "segundos": 242},
}
playlists = {
"Estudiar": ["c03", "c02"], # el valor es una LISTA de códigos
"Gym": ["c01"],
}
print(canciones["c02"]) # un diccionario entero
print(canciones["c02"]["titulo"]) # y, dentro de él, una de sus claves
print(playlists["Estudiar"][0]) # el primer código de la playlist
# Recorrer una playlist mostrando el título y el artista de cada canción
for codigo in playlists["Estudiar"]:
info = canciones[codigo] # paso 1: el diccionario de la canción
print("-", info["titulo"], "de", info["artista"]) # paso 2: sus datos
# get encadenado: si la canción no existe, un diccionario vacío por defecto
print(canciones.get("c09", {}).get("titulo", "desconocida"))
{'titulo': 'Café a las 3', 'artista': 'Los Turnos', 'segundos': 187}
Café a las 3
c03
- Domingo lento de Noa Vela
- Café a las 3 de Los Turnos
desconocida
La clave para no perderse es leer los corchetes de izquierda a derecha, un nivel cada vez:
canciones["c02"]devuelve un diccionario entero: el de la canción c02.canciones["c02"]["titulo"]aplica el segundo corchete sobre ese diccionario, y obtiene"Café a las 3".- En
playlists, el valor es una lista, así que el segundo corchete es una posición:playlists["Estudiar"][0]es el primer código,"c03". - El bucle combina las dos estructuras: recorre los códigos de la playlist y, con cada uno, busca su información en
canciones. Guardar la canción intermedia en una variable (info) hace que la línea siguiente se lea mucho mejor. - La última línea encadena dos
get. Si"c09"no existe, el primero devuelve{}(un diccionario vacío) y el segundo, al no encontrar"titulo"en ese diccionario vacío, devuelve"desconocida". Sin errores en ningún nivel.
Cuando te pierdas en una expresión con varios corchetes, pártela: cancion = canciones["c02"] y luego cancion["titulo"]. Imprime el valor intermedio y verás en qué nivel estás.
¿Qué puede ser una clave?
Las claves tienen que ser inmutables (técnicamente, hashables): números, strings, booleanos y tuplas de cosas inmutables. Las listas, los diccionarios y los conjuntos no pueden ser claves, porque pueden cambiar después de guardarlos y Python perdería la pista de dónde los guardó. Los valores, en cambio, pueden ser lo que quieras.
| Clave | ¿Vale? | Por qué |
|---|---|---|
"Pixel", 42, 3.5, True | Sí | Son inmutables |
("Pixel", "Bit Bang") | Sí | Tupla de strings: inmutable |
["Pixel", "Bit Bang"] | No | Lista: mutable → TypeError: unhashable type: 'list' |
{"pop"} | No | Conjunto: mutable |
Si necesitas una clave «compuesta» (por ejemplo, título y artista, porque hay dos canciones que se llaman igual), usa una tupla: favoritas[("Pixel", "Bit Bang")] = 5.
Conjuntos: colecciones sin repetidos
Un conjunto (set) es como un diccionario con solo claves: guarda elementos únicos y sin orden. Se escribe entre llaves, pero sin los dos puntos:
generos = {"pop", "indie", "pop", "rock", "indie"} # los repetidos desaparecen
print(len(generos))
print(sorted(generos)) # ordenado, para verlo siempre igual
vacio = set() # conjunto vacío: set(), nunca {}
print(type(vacio), type({}))
letras = set("pixel pop") # desde cualquier colección: un string...
print(sorted(letras))
artistas = set(["Noa Vela", "Marea Alta", "Noa Vela"]) # ...o una lista
print(sorted(artistas))
3 ['indie', 'pop', 'rock'] <class 'set'> <class 'dict'> [' ', 'e', 'i', 'l', 'o', 'p', 'x'] ['Marea Alta', 'Noa Vela']
Fíjate en cuatro detalles:
- Aunque escribimos
"pop"y"indie"dos veces, el conjunto solo tiene 3 elementos. Los repetidos desaparecen solos: por eso los conjuntos son la forma más rápida de quitar duplicados de una lista (set(lista)). - Los conjuntos no tienen orden. Si los imprimes tal cual, el orden puede cambiar de una ejecución a otra. Por eso en este artículo los mostramos con
sorted, que devuelve una lista ordenada y siempre sale igual. - **
{}es un diccionario vacío, no un conjunto.** Las llaves vacías se las quedó el diccionario porque llegó antes al lenguaje. Para un conjunto vacío,set(). set(…)construye un conjunto a partir de cualquier colección: de un string salen sus letras sin repetir (incluido el espacio), y de una lista, sus elementos sin repetir.
Como los elementos de un conjunto se guardan igual que las claves de un diccionario, también tienen que ser inmutables:
generos = {"pop", ["rock", "indie"]}
Traceback (most recent call last):
File "error_lista.py", line 1, in <module>
generos = {"pop", ["rock", "indie"]}
^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'list'
Añadir y quitar elementos
generos = {"pop", "rock"}
generos.add("indie") # añade UN elemento
generos.add("pop") # ya estaba: no pasa nada
generos.update(["jazz", "funk"]) # añade VARIOS, desde una colección
print(sorted(generos))
generos.remove("jazz") # lo quita (y da error si no está)
generos.discard("reggae") # lo quita si está; si no, no hace nada
print(sorted(generos))
print("rock" in generos, "jazz" in generos)
['funk', 'indie', 'jazz', 'pop', 'rock'] ['funk', 'indie', 'pop', 'rock'] True False
addañade un elemento. Añadir uno que ya estaba no da error: simplemente no cambia nada.updateañade todos los elementos de una colección. Cuidado:generos.update("jazz")añadiría las letrasj,ayz, porque un string es una colección de letras.removeydiscardquitan un elemento. La diferencia está en qué pasa si no está:discardno hace nada,removelanza unKeyError:
generos = {"pop", "rock"}
generos.remove("reggae")
Traceback (most recent call last):
File "error_remove.py", line 2, in <module>
generos.remove("reggae")
KeyError: 'reggae'
Usa remove cuando el elemento tendría que estar (si no está, prefieres enterarte del fallo) y discard cuando te da igual que esté o no.
Álgebra de conjuntos
Lo que hace especiales a los conjuntos es que se combinan como en matemáticas. Cada operación tiene un operador y un método equivalente:
| Operación | Operador | Método | Devuelve los elementos que están… |
|---|---|---|---|
| Unión | a | b | a.union(b) | en a, en b o en los dos |
| Intersección | a & b | a.intersection(b) | en los dos a la vez |
| Diferencia | a - b | a.difference(b) | en a pero no en b |
| Diferencia simétrica | a ^ b | a.symmetric_difference(b) | en uno de los dos, pero no en ambos |
| Subconjunto | a <= b | a.issubset(b) | True si todo a está en b |
| Superconjunto | a >= b | a.issuperset(b) | True si todo b está en a |
Probémoslo con las canciones guardadas de Ana y de Luis:
ana = {"Luz de neón", "Café a las 3", "Pixel", "Domingo lento"}
luis = {"Pixel", "Kilómetro 0", "Café a las 3", "Volver a casa"}
print(sorted(ana | luis)) # unión: lo que le gusta a alguno de los dos
print(sorted(ana & luis)) # intersección: lo que les gusta a los dos
print(sorted(ana - luis)) # diferencia: lo que solo le gusta a Ana
print(sorted(luis - ana)) # al revés: lo que solo le gusta a Luis
print(sorted(ana ^ luis)) # diferencia simétrica: lo que NO comparten
favoritas = {"Pixel", "Café a las 3"}
print(favoritas <= ana, favoritas < ana) # ¿subconjunto? ¿subconjunto estricto?
print(ana <= ana, ana < ana) # incluido en sí mismo, pero no estrictamente
print(favoritas <= luis) # Pixel y Café a las 3 también son de Luis
['Café a las 3', 'Domingo lento', 'Kilómetro 0', 'Luz de neón', 'Pixel', 'Volver a casa'] ['Café a las 3', 'Pixel'] ['Domingo lento', 'Luz de neón'] ['Kilómetro 0', 'Volver a casa'] ['Domingo lento', 'Kilómetro 0', 'Luz de neón', 'Volver a casa'] True True True False True
Entre líneas:
- Unión: 6 canciones distintas entre los dos. Pixel y Café a las 3 las tienen ambos, pero en la unión aparecen una sola vez.
- Intersección: las 2 que comparten.
- Diferencia:
ana - luisson las 2 que solo tiene Ana;luis - ana, las 2 que solo tiene Luis. Ojo: el orden importa, a diferencia de la unión y la intersección. - Diferencia simétrica: las 4 que no comparten, que son justo
(ana - luis) | (luis - ana). Fíjate en que 6 de la unión = 2 en común + 4 no compartidas. - Inclusión:
favoritas <= anaesTrueporque las dos favoritas están enana, yfavoritas < anatambién, porque ademásanatiene más canciones (es un subconjunto estricto). Un conjunto siempre está incluido en sí mismo (ana <= ana), pero nunca estrictamente (ana < anaesFalse).
Intentar sumar conjuntos con +. Los conjuntos no tienen + (eso es de listas y strings): la unión se escribe con |.
Tratamiento de datos: buscar por clave o recorrer
En la asignatura aparece mucho este tipo de problema: los datos vienen en varias estructuras (aquí, una tupla con dos diccionarios) y hay que cruzarlos. La decisión importante en cada función es: ¿tengo la clave, o tengo que buscar entre los valores?
datos = (
{ # canciones: código -> (título, artista, segundos)
"c01": ("Luz de neón", "Marea Alta", 215),
"c02": ("Café a las 3", "Los Turnos", 187),
"c03": ("Domingo lento", "Noa Vela", 242),
},
{ # usuarios: nombre -> lista de códigos que ha guardado
"ana": ["c01", "c03"],
"luis": ["c02"],
"sara": ["c03", "c02"],
},
)
def titulo(canciones, codigo):
"""Título de la canción, o el propio código si no existe."""
# Tengo la clave: consulta DIRECTA, sin recorrer nada
return canciones.get(codigo, (codigo,))[0]
def quien_la_guarda(usuarios, codigo):
"""Nombres de los usuarios que tienen ese código guardado."""
# Aquí busco por el VALOR (dentro de las listas): no hay más remedio que recorrer
nombres = []
for nombre, guardadas in usuarios.items():
if codigo in guardadas:
nombres.append(nombre)
return nombres
def oyentes(datos, codigo):
canciones, usuarios = datos # desempaquetar la tupla de dos diccionarios
nombre = titulo(canciones, codigo)
lista = []
if nombre != codigo: # solo buscamos si la canción existe
lista = quien_la_guarda(usuarios, codigo)
return nombre, lista
print(oyentes(datos, "c03"))
print(oyentes(datos, "c02"))
print(oyentes(datos, "c99"))
('Domingo lento', ['ana', 'sara'])
('Café a las 3', ['luis', 'sara'])
('c99', [])
Fíjate en cómo cambia la forma de trabajar en cada función:
titulotiene la clave (el código de la canción). No recorre nada: hace una consulta directa conget. Si el código no existe, devuelve la tupla(codigo,)por defecto, y el[0]saca de ella el propio código. Así la función siempre devuelve un string.quien_la_guardabusca por el valor: el código está dentro de las listas de los usuarios, no en las claves. Aquí no queda otra que recorrerusuarios.items()y preguntar coninen cada lista.oyentesempieza desempaquetando la tupladatosen sus dos diccionarios, y solo busca a los usuarios si la canción existe. Con"c99",titulodevuelve el propio código y la lista se queda vacía.
Esa es la regla de oro del tema. Buscar por clave es instantáneo; buscar por valor obliga a recorrer. Cuando diseñes tus datos, elige como clave aquello por lo que vas a buscar más a menudo.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta el código línea a línea y dibuja cada variable con flechas hacia los objetos a los que apunta. Con los diccionarios es especialmente útil, porque dibuja cada pareja clave → valor como una fila. Elige Python 3 y pega este programa:
duraciones = {"Luz de neón": 215}
duraciones["Pixel"] = 198
duraciones["Pixel"] = 200
segundos = duraciones.get("Café a las 3", 0)
playlists = {"Gym": ["Pixel"], "Estudiar": []}
playlists["Estudiar"].append("Luz de neón")
generos = {"pop", "indie"}
generos.add("pop")
print(duraciones, segundos)
print(playlists, len(generos))
Salida esperada:
{'Luz de neón': 215, 'Pixel': 200} 0
{'Gym': ['Pixel'], 'Estudiar': ['Luz de neón']} 2
Qué observar, paso a paso:
- En la línea 2, el diccionario
duracionesgana una fila nueva: la clave"Pixel"no existía, así que la asignación la añade. - En la línea 3 no aparece otra fila: la clave ya existía y Python Tutor solo cambia el 198 por 200. Un diccionario nunca tiene dos claves iguales.
- En la línea 4,
segundosvale 0.getno encontró"Café a las 3"y devolvió el valor por defecto, sin error. Fíjate en queduracionesno cambia:getsolo lee. - En
playlists, cada valor se dibuja como una flecha a una lista. Cuando se ejecuta elappend, la lista de"Estudiar"crece en su sitio: no hace falta volver a asignarla, porque el diccionario guarda una referencia a esa lista. - Al final,
generossigue teniendo 2 elementos: añadir"pop", que ya estaba, no cambia nada.
Resumen
| Concepto | Lo esencial |
|---|---|
| Diccionario | Parejas clave: valor; se busca por clave, no por posición |
| Crear | {}, {"a": 1}, dict(parejas), comprehension {k: v for …} |
d[k] = v | Añade si la clave es nueva; sustituye si ya existía |
d[k] (leer) | Devuelve el valor; KeyError si la clave no existe |
k in d | Pregunta por las claves, nunca por los valores |
d.get(k, defecto) | Lee sin error; devuelve el defecto (o None) si no está |
| Contador | d[k] = d.get(k, 0) + 1 |
| Recorrer | for k in d, d.values(), for k, v in d.items() |
| Anidados | Un corchete por nivel: d["c02"]["titulo"] |
| Claves válidas | Inmutables: números, strings, tuplas |
| Conjunto | Elementos únicos y sin orden; vacío con set(), nunca {} |
| Modificar un conjunto | add, update, remove (error si no está), discard |
| Álgebra | | unión, & intersección, - diferencia, ^ simétrica, <= subconjunto |
Tres ideas para llevarte:
- Escribir en una clave nueva siempre funciona; leer una clave que no existe, nunca. Antes de leer, pregúntate si la clave puede faltar y, si puede, usa
inoget. - **
iny elforde un diccionario miran las claves.** Para los valores,.values(); para las parejas,.items(). - Los conjuntos responden «¿qué tienen en común?» y «¿qué tiene uno que no tenga el otro?» en una sola operación. Y como no guardan repetidos, son también la forma más rápida de quitar duplicados.
En la práctica de contenedores asociativos aplicarás todo esto a un contador de reproducciones, a una comparación de gustos musicales con conjuntos y a una biblioteca con usuarios, playlists y canciones anidadas. Después, los ejercicios te pondrán a prueba. Y en el siguiente tema, los strings a fondo: recorrerlos, cortarlos y transformarlos.
Siguiente paso: prácticaContenedores asociativos en Python — práctica con un contador de reproducciones, gustos que se cruzan y una biblioteca de playlists (y el bucle que se quejó de que el diccionario cambiaba)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

4 comentarios