En esta página

Diccionarios y conjuntos

12 min lectura TextoCap. 2 — Funciones y datos

Diccionarios en Python

Los diccionarios son colecciones de pares clave-valor no ordenadas por posición (pero sí por inserción desde Python 3.7+). Son la estructura de datos más eficiente para buscar valores por clave, con una complejidad promedio de O(1):

# Formas de crear diccionarios
vacío = {}
vacío2 = dict()

# Literal
persona = {
    "nombre": "Elena Martínez",
    "edad": 32,
    "ciudad": "Buenos Aires",
    "activa": True
}

# Constructor dict() con keyword arguments
config = dict(host="localhost", puerto=8080, debug=False)

# Desde una lista de tuplas (pares clave-valor)
claves = ["x", "y", "z"]
valores = [10, 20, 30]
coordenadas = dict(zip(claves, valores))
print(coordenadas)  # {'x': 10, 'y': 20, 'z': 30}

# fromkeys() — crear con claves predefinidas y valor inicial
plantilla = dict.fromkeys(["nombre", "email", "telefono"], None)
print(plantilla)  # {'nombre': None, 'email': None, 'telefono': None}

Acceso a valores

producto = {
    "id": "P001",
    "nombre": "Laptop Pro",
    "precio": 1299.99,
    "categoría": "Electrónica",
    "stock": 15
}

# Acceso directo (lanza KeyError si no existe)
print(producto["nombre"])   # Laptop Pro
print(producto["precio"])   # 1299.99

# Acceso seguro con get()
print(producto.get("stock"))           # 15
print(producto.get("descuento"))       # None — no lanza error
print(producto.get("descuento", 0))    # 0 — valor por defecto personalizado

# Verificar existencia
print("precio" in producto)       # True
print("descuento" in producto)    # False
print("id" not in producto)       # False

# Acceso a diccionarios anidados
tienda = {
    "productos": {
        "P001": {"nombre": "Laptop", "precio": 1299.99},
        "P002": {"nombre": "Mouse", "precio": 29.99}
    },
    "ubicación": "Madrid"
}

# Forma segura con .get() encadenado
precio_laptop = tienda.get("productos", {}).get("P001", {}).get("precio", 0)
print(precio_laptop)  # 1299.99

Métodos esenciales de diccionario

keys(), values(), items()

empleado = {
    "nombre": "Roberto Silva",
    "departamento": "Ingeniería",
    "salario": 75000,
    "antigüedad": 5
}

# keys() — devuelve una vista de las claves
claves = empleado.keys()
print(list(claves))  # ['nombre', 'departamento', 'salario', 'antigüedad']

# values() — devuelve una vista de los valores
valores = empleado.values()
print(list(valores))  # ['Roberto Silva', 'Ingeniería', 75000, 5]

# items() — devuelve pares (clave, valor) como tuplas
for clave, valor in empleado.items():
    print(f"{clave:15}: {valor}")

# Las vistas son dinámicas — reflejan cambios al dict
empleado["bonus"] = 5000
print(list(claves))  # ['nombre', 'departamento', 'salario', 'antigüedad', 'bonus']

update(), pop(), popitem()

inventario = {"manzanas": 50, "peras": 30, "uvas": 20}

# update() — actualiza con otro diccionario o iterable de pares
inventario.update({"peras": 45, "sandías": 10})
print(inventario)  # {'manzanas': 50, 'peras': 45, 'uvas': 20, 'sandías': 10}

# Operador | (Python 3.9+) — fusión sin modificar los originales
a = {"x": 1, "y": 2}
b = {"y": 10, "z": 3}
c = a | b  # Las claves de b sobrescriben las de a
print(c)   # {'x': 1, 'y': 10, 'z': 3}
print(a)   # {'x': 1, 'y': 2} — sin cambios

# Operador |= (Python 3.9+) — actualización in-place
a |= b
print(a)   # {'x': 1, 'y': 10, 'z': 3}

# pop() — elimina y devuelve el valor de la clave
stock_uvas = inventario.pop("uvas")
print(stock_uvas)  # 20
print(inventario)  # sin 'uvas'

# pop() con valor por defecto (no lanza error si no existe)
inexistente = inventario.pop("plátanos", "No encontrado")
print(inexistente)  # No encontrado

# popitem() — elimina y devuelve el último par (LIFO en Python 3.7+)
ultimo_par = inventario.popitem()
print(ultimo_par)  # ('sandías', 10)

# setdefault() — establece valor si la clave no existe
inventario.setdefault("kiwis", 0)
print(inventario["kiwis"])   # 0

# Si la clave ya existe, setdefault() NO la modifica
inventario.setdefault("manzanas", 999)
print(inventario["manzanas"])  # 50 — sin cambios

Patrones comunes con diccionarios

Contar ocurrencias

palabras = ["el", "cielo", "el", "mar", "el", "viento", "mar"]

# Forma manual
conteo: dict[str, int] = {}
for palabra in palabras:
    conteo[palabra] = conteo.get(palabra, 0) + 1
print(conteo)  # {'el': 3, 'cielo': 1, 'mar': 2, 'viento': 1}

# Con Counter del módulo collections (más eficiente)
from collections import Counter
conteo_counter = Counter(palabras)
print(conteo_counter)          # Counter({'el': 3, 'mar': 2, ...})
print(conteo_counter.most_common(2))  # [('el', 3), ('mar', 2)]

Agrupar elementos

from collections import defaultdict

empleados = [
    {"nombre": "Ana", "departamento": "Ing"},
    {"nombre": "Bruno", "departamento": "Ventas"},
    {"nombre": "Carmen", "departamento": "Ing"},
    {"nombre": "Diego", "departamento": "Ventas"},
    {"nombre": "Elena", "departamento": "Ing"},
]

# defaultdict evita verificar si la clave existe
por_depto: defaultdict[str, list[str]] = defaultdict(list)
for emp in empleados:
    por_depto[emp["departamento"]].append(emp["nombre"])

print(dict(por_depto))
# {'Ing': ['Ana', 'Carmen', 'Elena'], 'Ventas': ['Bruno', 'Diego']}

Conjuntos (sets)

Los conjuntos son colecciones no ordenadas de elementos únicos y hashables. Son perfectos para eliminar duplicados y hacer operaciones matemáticas de conjuntos:

# Crear sets
vacío = set()  # ¡No uses {} para crear un set vacío! (eso crea un dict)
numeros = {1, 2, 3, 4, 5}
letras = {"a", "b", "c", "d"}
con_duplicados = {1, 2, 2, 3, 3, 3}  # {1, 2, 3} — duplicados eliminados

# Crear desde otros iterables
desde_lista = set([1, 2, 2, 3, 4, 4])      # {1, 2, 3, 4}
desde_string = set("programación")          # letras únicas
sin_duplicados = set(["Ana", "Bruno", "Ana", "Carmen"])  # {'Ana', 'Bruno', 'Carmen'}

print(sin_duplicados)  # {'Ana', 'Bruno', 'Carmen'}

Métodos de conjunto

colores = {"rojo", "verde", "azul"}

# add() — agrega un elemento (in-place)
colores.add("amarillo")
print(colores)

# remove() — elimina (lanza KeyError si no existe)
colores.remove("verde")

# discard() — elimina sin error si no existe
colores.discard("morado")   # No lanza error
colores.discard("azul")     # Lo elimina

# pop() — elimina y devuelve un elemento aleatorio
elemento = colores.pop()
print(elemento)  # Algún elemento (orden no garantizado)

# clear() — vacía el set
temp = {1, 2, 3}
temp.clear()
print(temp)  # set()

# Pertenencia — O(1), mucho más rápido que en listas
frutas = {"manzana", "pera", "uva", "naranja"}
print("pera" in frutas)       # True
print("sandía" in frutas)     # False
print("sandía" not in frutas) # True

Operaciones matemáticas de conjuntos

python_devs = {"Ana", "Bruno", "Carmen", "Diego"}
js_devs = {"Bruno", "Elena", "Diego", "Felipe"}
rust_devs = {"Carmen", "Gabriel"}

# Unión (|) — todos los elementos de ambos conjuntos
todos = python_devs | js_devs
print(todos)  # {'Ana', 'Bruno', 'Carmen', 'Diego', 'Elena', 'Felipe'}

# Intersección (&) — elementos en ambos conjuntos
ambos = python_devs & js_devs
print(ambos)  # {'Bruno', 'Diego'}

# Diferencia (-) — en el primero pero no en el segundo
solo_python = python_devs - js_devs
print(solo_python)  # {'Ana', 'Carmen'}

# Diferencia simétrica (^) — en uno o en otro, pero no en ambos
exclusivos = python_devs ^ js_devs
print(exclusivos)  # {'Ana', 'Carmen', 'Elena', 'Felipe'}

# Subconjunto y superconjunto
a = {1, 2, 3}
b = {1, 2, 3, 4, 5}
print(a.issubset(b))     # True — a ⊆ b
print(b.issuperset(a))   # True — b ⊇ a
print(a <= b)            # True — equivalente a issubset()
print(b >= a)            # True — equivalente a issuperset()
print(a.isdisjoint(rust_devs))  # True — no tienen elementos en común... 
# (depende de los nombres)

# Métodos alternativos (equivalentes a los operadores)
print(python_devs.union(js_devs))
print(python_devs.intersection(js_devs))
print(python_devs.difference(js_devs))
print(python_devs.symmetric_difference(js_devs))

frozenset — conjuntos inmutables

# frozenset es la versión inmutable de set
# Se puede usar como clave de diccionario o elemento de otro set
permisos_lectura = frozenset({"leer", "descargar"})
permisos_escritura = frozenset({"leer", "escribir", "editar"})

# Hashable — puede ser clave de dict
mapa_permisos = {
    permisos_lectura: "Solo lectura",
    permisos_escritura: "Lectura y escritura"
}

print(mapa_permisos[permisos_lectura])  # Solo lectura

¿Cuándo usar cada estructura?

Estructura Cuándo usarla
list Colección ordenada, puede cambiar, permite duplicados
tuple Colección ordenada, no cambia (inmutable), permite duplicados
dict Acceso rápido por clave, pares clave-valor, sin duplicados en claves
set Elementos únicos, operaciones matemáticas, pruebas de pertenencia rápida
frozenset Set inmutable que necesita ser hashable

Resumen

Los diccionarios son esenciales para búsquedas O(1) por clave y para representar datos estructurados. Los conjuntos son ideales para trabajar con colecciones únicas y hacer operaciones matemáticas de teoría de conjuntos. En la próxima lección aprenderemos las comprensiones y generadores, que transformarán la forma en que construyes estas estructuras de datos.

Los diccionarios mantienen el orden de inserción desde Python 3.7
A partir de Python 3.7, los diccionarios son ordenados por inserción (CPython 3.6+ en la implementación). Puedes confiar en que las iteraciones respetan el orden en que se insertaron las claves.
Usa get() para evitar KeyError
En lugar de acceder directamente con dict[clave] (que lanza KeyError si no existe), usa dict.get(clave, valor_por_defecto). El valor por defecto es None si no se especifica.
Las claves de un diccionario deben ser hashables
Solo los objetos inmutables (str, int, float, bool, tuplas de inmutables) pueden ser claves de un diccionario. Las listas, sets y otros diccionarios no son válidos como claves porque son mutables.