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.

¿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…).
Lista frente a diccionarioA la izquierda, una lista con posiciones 0, 1 y 2. A la derecha, un diccionario cuyas claves son títulos de canciones y cuyos valores son duraciones en segundos.Lista: se busca por POSICIÓNDiccionario: se busca por CLAVEposiciónvalorclavevalor0"Luz de neón""Luz de neón"2151"Café a las 3""Café a las 3"1872"Pixel""Pixel"176canciones[1] → "Café a las 3"duraciones["Pixel"] → 176
En una lista llegas a cada dato por su número de posición; en un diccionario, por su clave.

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:

  • {} y dict() crean un diccionario vacío. Los dos dan lo mismo: type dice dict y len cuenta cero parejas.
  • duraciones es 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: valor delante del for. Aquí recorre duraciones.items() (enseguida veremos qué es) y, para cada título, guarda los minutos enteros: 215 // 60 es 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"] = 242 crea la pareja, porque esa clave no existía.
  • Asignar a una clave que ya existe la sustituye. duraciones["Café a las 3"] = 190 no 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.
  • len cuenta parejas, no claves y valores por separado: 3.
  • in pregunta si existe una clave. Fíjate en la segunda: 215 in duraciones es False aunque 215 esté dentro, porque 215 es un valor, no una clave. in sobre un diccionario solo mira las claves.
  • pop(clave) quita la pareja y te devuelve el valor, por si lo necesitas (aquí, 215). del tambié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:

  1. **Preguntar antes con in:** if "Pixel" in duraciones: print(duraciones["Pixel"]).
  2. **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:

Vueltacancionget(cancion, 0) devuelvereproducciones 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ónUsaPor qué
Solo quieres saber si existe la claveclave in dDevuelve True/False y no lee el valor
Quieres el valor, y si no existe te vale uno por defectod.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 existed[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() e items() no devuelven listas, sino vistas (dict_keys(…)). Una vista se puede recorrer con for y preguntar con in, pero no se puede indexar con [0]. Si necesitas una lista de verdad, conviértela con list(…).
  • items() da tuplas (clave, valor). Por eso en el for se escribe for cancion, veces in …: cada tupla se desempaqueta en dos variables.
  • Un for sin más (for cancion in reproducciones) recorre las claves. Es lo mismo que for cancion in reproducciones.keys().
  • Como values() es una colección de números, sum, max o min funcionan 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…RecorreEjemplo
Solo la clavefor k in dMostrar los títulos
Solo el valorfor v in d.values()Sumar duraciones
Las dos cosasfor 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:

Acceso a un diccionario anidadoEl diccionario canciones tiene como valores otros diccionarios. La expresión canciones["c02"]["titulo"] baja primero a la clave c02 y luego a la clave titulo, y obtiene el texto Café a las 3.canciones["c02"]["titulo"]→ "Café a las 3"canciones"c01": {…}"c02": {…}"c03": {…}1. la clave de fuera"titulo""Café a las 3""artista""Los Turnos""segundos"1872. la clave de dentro
Cada par de corchetes baja un nivel: primero eliges la canción y después el dato dentro de ella.
  • 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, TrueSíSon inmutables
("Pixel", "Bit Bang")SíTupla de strings: inmutable
["Pixel", "Bit Bang"]NoLista: mutable → TypeError: unhashable type: 'list'
{"pop"}NoConjunto: 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
  • add añade un elemento. Añadir uno que ya estaba no da error: simplemente no cambia nada.
  • update añade todos los elementos de una colección. Cuidado: generos.update("jazz") añadiría las letras j, a y z, porque un string es una colección de letras.
  • remove y discard quitan un elemento. La diferencia está en qué pasa si no está: discard no hace nada, remove lanza un KeyError:
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ónOperadorMétodoDevuelve los elementos que están…
Unióna | ba.union(b)en a, en b o en los dos
Interseccióna & ba.intersection(b)en los dos a la vez
Diferenciaa - ba.difference(b)en a pero no en b
Diferencia simétricaa ^ ba.symmetric_difference(b)en uno de los dos, pero no en ambos
Subconjuntoa <= ba.issubset(b)True si todo a está en b
Superconjuntoa >= ba.issuperset(b)True si todo b está en a
Las cuatro operaciones de conjuntosCuatro diagramas de Venn con dos círculos, A y B. La parte coloreada muestra el resultado de la unión, la intersección, la diferencia y la diferencia simétrica.ABUniónA | Balguno de los dosABIntersecciónA & Blos dosABDiferenciaA – Bsolo el primeroABDiferencia simétricaA ^ Buno, pero no los dos
La zona coloreada es el resultado de cada operación entre el conjunto A (izquierda) y el conjunto B (derecha).

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 - luis son 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 <= ana es True porque las dos favoritas están en ana, y favoritas < ana también, porque además ana tiene más canciones (es un subconjunto estricto). Un conjunto siempre está incluido en sí mismo (ana <= ana), pero nunca estrictamente (ana < ana es False).

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:

  • titulo tiene la clave (el código de la canción). No recorre nada: hace una consulta directa con get. 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_guarda busca por el valor: el código está dentro de las listas de los usuarios, no en las claves. Aquí no queda otra que recorrer usuarios.items() y preguntar con in en cada lista.
  • oyentes empieza desempaquetando la tupla datos en sus dos diccionarios, y solo busca a los usuarios si la canción existe. Con "c99", titulo devuelve 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:

  1. En la línea 2, el diccionario duraciones gana una fila nueva: la clave "Pixel" no existía, así que la asignación la añade.
  2. 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.
  3. En la línea 4, segundos vale 0. get no encontró "Café a las 3" y devolvió el valor por defecto, sin error. Fíjate en que duraciones no cambia: get solo lee.
  4. En playlists, cada valor se dibuja como una flecha a una lista. Cuando se ejecuta el append, la lista de "Estudiar" crece en su sitio: no hace falta volver a asignarla, porque el diccionario guarda una referencia a esa lista.
  5. Al final, generos sigue teniendo 2 elementos: añadir "pop", que ya estaba, no cambia nada.

Resumen

ConceptoLo esencial
DiccionarioParejas clave: valor; se busca por clave, no por posición
Crear{}, {"a": 1}, dict(parejas), comprehension {k: v for …}
d[k] = vAñ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 dPregunta por las claves, nunca por los valores
d.get(k, defecto)Lee sin error; devuelve el defecto (o None) si no está
Contadord[k] = d.get(k, 0) + 1
Recorrerfor k in d, d.values(), for k, v in d.items()
AnidadosUn corchete por nivel: d["c02"]["titulo"]
Claves válidasInmutables: números, strings, tuplas
ConjuntoElementos únicos y sin orden; vacío con set(), nunca {}
Modificar un conjuntoadd, update, remove (error si no está), discard
Álgebra| unión, & intersección, - diferencia, ^ simétrica, <= subconjunto

Tres ideas para llevarte:

  1. 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 in o get.
  2. **in y el for de un diccionario miran las claves.** Para los valores, .values(); para las parejas, .items().
  3. 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 →

Sigue aprendiendo

4 comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *