Estructuras de datos en Python: listas, tuplas, diccionarios y conjuntos
- Cuatro estructuras, cuatro propósitos distintos
- Listas: la estructura más versátil de Python
- Slicing: extraer sublistas con elegancia
- Tuplas: inmutabilidad como contrato
- Diccionarios: datos con nombre y acceso O(1)
- Conjuntos: unicidad y operaciones matemáticas
- Comprensiones: listas, dicts y sets en una línea
- Cómo elegir la estructura correcta
- Programa completo: gestor de contactos
- Errores clásicos con estructuras de datos
- Preguntas frecuentes
Python incluye cuatro estructuras de datos en su biblioteca estándar que cubren el noventa por ciento de los casos de uso: la lista, la tupla, el diccionario y el conjunto. No son intercambiables. Cada una resuelve un problema diferente y elegir la correcta hace el código más claro, más seguro y más eficiente. Esta lección cubre las cuatro en detalle.
📋 Listas: la estructura más versátil de Python
Una lista es una secuencia ordenada y mutable que puede contener elementos de cualquier tipo, incluso mezclados y repetidos. Es la estructura más usada en Python por su flexibilidad.
# Crear listas
vacia = []
numeros = [1, 2, 3, 4, 5]
mixta = [42, "hola", True, 3.14, None]
anidada = [[1, 2], [3, 4], [5, 6]] # lista de listas
# Acceder por índice (empieza en 0, negativo desde el final)
frutas = ["manzana", "pera", "naranja", "uva"]
frutas[0] # "manzana"
frutas[-1] # "uva"
frutas[-2] # "naranja"
# Modificar
frutas[1] = "kiwi" # ahora ["manzana","kiwi","naranja","uva"]
Métodos esenciales de lista
L = [3, 1, 4, 1, 5, 9]
L.append(2) # añade al final: [..., 2]
L.insert(0, 0) # inserta en posición: [0, 3, 1, ...]
L.extend([6, 5]) # concatena otra lista
L.remove(1) # elimina primera aparición de 1
ultimo = L.pop() # saca y devuelve el último elemento
tercero = L.pop(2) # saca el elemento en índice 2
L.sort() # ordena in-place (modifica L)
L.sort(reverse=True) # ordena descendente
L.reverse() # invierte in-place
L.index(4) # devuelve índice de la primera aparición de 4
L.count(1) # cuántas veces aparece 1
L.clear() # vacía la lista
# Funciones built-in sobre listas:
len(L) # longitud
sum(L) # suma (si son números)
min(L), max(L) # mínimo y máximo
sorted(L) # nueva lista ordenada (no modifica L)
reversed(L) # iterador inverso
✂️ Slicing: extraer sublistas con elegancia
El slicing (rebanado) permite extraer partes de una lista, string o tupla con la sintaxis [inicio:fin:paso]. El índice fin es siempre excluido.
L = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
L[2:5] # [2, 3, 4] — desde índice 2 hasta el 4
L[:3] # [0, 1, 2] — los primeros 3
L[7:] # [7, 8, 9] — desde el índice 7 hasta el final
L[-3:] # [7, 8, 9] — los últimos 3
L[::2] # [0, 2, 4, 6, 8] — cada 2 (pares)
L[::-1] # [9, 8, 7, ..., 0] — invertir
L[1:8:3] # [1, 4, 7] — inicio=1, fin=8, paso=3
# En strings:
"Python"[::-1] # "nohtyP"
"abcdefg"[2:5] # "cde"
# Crear una copia superficial:
copia = L[:]
copia = list(L) # equivalente
# Modificar con slicing:
L[1:4] = [10, 20] # reemplaza elementos 1, 2, 3 por 10 y 20
-1 es el último, -2 el penúltimo. L[-3:] es "los últimos tres", independientemente del tamaño de la lista.
📌 Tuplas: inmutabilidad como contrato
Una tupla es como una lista, pero inmutable: una vez creada no puedes cambiar, añadir ni eliminar sus elementos. Esa restricción comunica intención: "estos datos son fijos".
# Crear tuplas
punto = (10, 20)
rgb = (255, 128, 0)
tupla_un_elemento = (42,) # la coma es obligatoria
sin_parentesis = 1, 2, 3 # los paréntesis son opcionales
# Acceder (igual que lista, sin modificar)
punto[0] # 10
punto[-1] # 20
# Intentar modificar → TypeError
punto[0] = 5 # TypeError: 'tuple' object does not support item assignment
# Desempaquetado (muy habitual en Python):
x, y = punto
print(x, y) # 10 20
# Con * para el resto:
primero, *resto = (1, 2, 3, 4, 5)
print(primero) # 1
print(resto) # [2, 3, 4, 5]
# Intercambio sin variable temporal (solo Python):
a, b = 1, 2
a, b = b, a
print(a, b) # 2 1
# Las tuplas son hashables → usables como claves de dict
tablero = {}
tablero[(0, 0)] = "X"
tablero[(0, 1)] = "O"
🗂️ Diccionarios: datos con nombre y acceso O(1)
Un diccionario almacena pares clave: valor. Las claves deben ser únicas y hashables (strings, números, tuplas). El acceso por clave es O(1) — prácticamente instantáneo sin importar el tamaño del diccionario.
# Crear diccionarios
vacio = {}
persona = {"nombre": "Ana", "edad": 28, "ciudad": "Madrid"}
# dict() con keywords:
config = dict(host="localhost", puerto=5432, debug=True)
# Acceder
persona["nombre"] # "Ana"
persona.get("email") # None (sin KeyError)
persona.get("email", "N/A") # "N/A" como valor por defecto
# Modificar / añadir
persona["edad"] = 29 # modifica
persona["email"] = "ana@..." # añade nueva clave
# Eliminar
del persona["ciudad"]
persona.pop("email") # elimina y devuelve el valor
persona.pop("tel", None) # sin error si no existe
# Iterar
for clave in persona: # solo claves
print(clave)
for valor in persona.values(): # solo valores
print(valor)
for clave, valor in persona.items(): # pares
print(f"{clave}: {valor}")
# Comprobar existencia
"nombre" in persona # True
"email" in persona # False
# Métodos útiles
persona.keys() # dict_keys([...])
persona.values() # dict_values([...])
persona.items() # dict_items([(k,v), ...])
persona.update({"edad": 30, "pais": "España"}) # merge in-place
# Python 3.9+: merge con |
ampliado = persona | {"rol": "admin"} # nuevo dict
# setdefault: añade solo si la clave no existe
persona.setdefault("puntos", 0)
Patrones avanzados con diccionarios
# defaultdict: valor por defecto automático
from collections import defaultdict
contador = defaultdict(int) # int() = 0 por defecto
palabras = ["hola", "mundo", "hola", "python", "hola"]
for p in palabras:
contador[p] += 1
# defaultdict(int, {'hola': 3, 'mundo': 1, 'python': 1})
# Counter: contar ocurrencias directamente
from collections import Counter
c = Counter(palabras)
c.most_common(2) # [('hola', 3), ('mundo', 1)]
# Eliminar duplicados preservando orden (Python 3.7+):
lista_con_dups = [3, 1, 4, 1, 5, 9, 2, 6, 5]
sin_dups = list(dict.fromkeys(lista_con_dups))
# [3, 1, 4, 5, 9, 2, 6]
🔵 Conjuntos: unicidad y operaciones matemáticas
Un conjunto (set) es una colección sin orden y sin duplicados. La búsqueda en un set es O(1), igual que en un dict. Sus operaciones matemáticas (unión, intersección, diferencia) son especialmente útiles.
# Crear conjuntos
vacio = set() # no confundir con {} que es dict vacío
colores = {"rojo", "verde", "azul"}
desde_lista = set([1, 2, 2, 3, 3, 3]) # {1, 2, 3}
# Añadir / eliminar
colores.add("amarillo")
colores.discard("verde") # sin error si no existe
colores.remove("azul") # KeyError si no existe
# Pertenencia — muy rápida
"rojo" in colores # True
# Operaciones de conjunto
A = {1, 2, 3, 4, 5}
B = {3, 4, 5, 6, 7}
A | B # unión: {1,2,3,4,5,6,7}
A & B # intersección: {3,4,5}
A - B # diferencia A-B: {1,2}
B - A # diferencia B-A: {6,7}
A ^ B # diferencia simétrica: {1,2,6,7}
A.issubset(B) # ¿A ⊆ B?
A.issuperset(B) # ¿A ⊇ B?
A.isdisjoint(B) # ¿A ∩ B = ∅?
# Caso práctico: encontrar elementos comunes entre dos listas
lista1 = ["Ana", "Luis", "Marta", "Pedro"]
lista2 = ["Luis", "Carlos", "Ana", "Sara"]
comunes = set(lista1) & set(lista2) # {"Ana", "Luis"}
solo_en_1 = set(lista1) - set(lista2) # {"Marta", "Pedro"}
todos = set(lista1) | set(lista2)
# frozenset: versión inmutable (usable como clave de dict)
fs = frozenset({1, 2, 3})
⚡ Comprensiones: listas, dicts y sets en una línea
Las comprensiones son la forma idiomática de Python para crear colecciones a partir de otras, con lógica de transformación y filtrado integrada.
# List comprehension: [expresión for x in iterable if condición]
cuadrados = [x**2 for x in range(1, 11)]
pares = [x for x in range(20) if x % 2 == 0]
mayusculas = [s.upper() for s in ["hola", "mundo"]]
# Comprensión anidada — aplanar lista de listas:
matriz = [[1,2,3],[4,5,6],[7,8,9]]
plana = [x for fila in matriz for x in fila]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Dict comprehension: {clave: valor for x in iterable}
cuadrados_d = {x: x**2 for x in range(1, 6)}
# {1:1, 2:4, 3:9, 4:16, 5:25}
invertido = {v: k for k, v in {"a":1,"b":2,"c":3}.items()}
# {1:"a", 2:"b", 3:"c"}
# Set comprehension: {expresión for x in iterable}
iniciales = {nombre[0] for nombre in ["Ana","Arturo","Beatriz"]}
# {"A", "B"}
# Solo pares únicos de una lista con duplicados:
lista = [1, 2, 2, 3, 3, 3, 4]
unicos_pares = {x for x in lista if x % 2 == 0}
# {2, 4}
🧭 Cómo elegir la estructura correcta
Una regla rápida para tomar la decisión correcta:
# ¿Colección ordenada que cambia? → lista
tareas = ["comprar", "estudiar", "cocinar"]
# ¿Datos fijos que no deben cambiar? → tupla
coordenadas = (40.4168, -3.7038) # Madrid
pixel_color = (255, 128, 0)
# ¿Necesitas acceder por nombre/clave? → dict
usuario = {"id": 1, "nombre": "Ana", "rol": "admin"}
# ¿Necesitas unicidad o comparar grupos? → set
etiquetas_vistas = {"python", "django", "flask"}
permisos_usuario & permisos_requeridos # intersección
# Tabla de decisión rápida:
# ┌─────────────────────────────┬──────────┐
# │ ¿Necesitas...? │ Usar │
# ├─────────────────────────────┼──────────┤
# │ Orden + mutabilidad │ lista │
# │ Orden + inmutabilidad │ tupla │
# │ Clave → valor │ dict │
# │ Sin duplicados / conjuntos │ set │
# └─────────────────────────────┴──────────┘
🛠️ Programa completo: gestor de contactos
from collections import defaultdict
# Cada contacto es un dict; todos los contactos en una lista
contactos = []
def buscar_por_nombre(nombre):
"""Busca contactos cuyo nombre contenga el texto."""
nombre = nombre.lower()
return [c for c in contactos if nombre in c["nombre"].lower()]
def buscar_por_etiqueta(etiqueta):
"""Devuelve contactos que tienen una etiqueta concreta."""
return [c for c in contactos if etiqueta in c.get("etiquetas", set())]
def agregar_contacto(nombre, telefono, etiquetas=None):
"""Añade un nuevo contacto si el teléfono no está duplicado."""
telefonos = {c["telefono"] for c in contactos} # set para O(1)
if telefono in telefonos:
print(f" Ya existe un contacto con el teléfono {telefono}.")
return False
contactos.append({
"nombre": nombre,
"telefono": telefono,
"etiquetas": set(etiquetas) if etiquetas else set(),
})
return True
def resumen_etiquetas():
"""Cuenta cuántos contactos hay por etiqueta."""
conteo = defaultdict(int)
for c in contactos:
for tag in c.get("etiquetas", set()):
conteo[tag] += 1
return dict(sorted(conteo.items(), key=lambda x: -x[1]))
# Cargar datos de ejemplo
datos = [
("Ana García", "600111222", ["amigo", "trabajo"]),
("Luis López", "600333444", ["familia"]),
("Marta Ruiz", "600555666", ["amigo"]),
("Pedro Sanz", "600777888", ["trabajo"]),
("Sara Díez", "600999000", ["amigo", "familia"]),
]
for nombre, tel, tags in datos:
agregar_contacto(nombre, tel, tags)
# Menú interactivo
while True:
print("\n=== GESTOR DE CONTACTOS ===")
print("1. Ver todos 2. Buscar nombre 3. Buscar etiqueta")
print("4. Añadir 5. Resumen tags 6. Salir")
opcion = input("Opción: ").strip()
if opcion == "1":
for c in sorted(contactos, key=lambda x: x["nombre"]):
tags = ", ".join(sorted(c["etiquetas"])) or "sin etiquetas"
print(f" {c['nombre']:<20} {c['telefono']} [{tags}]")
elif opcion == "2":
texto = input(" Nombre a buscar: ").strip()
resultado = buscar_por_nombre(texto)
if resultado:
for c in resultado:
print(f" {c['nombre']} — {c['telefono']}")
else:
print(" Sin resultados.")
elif opcion == "3":
tag = input(" Etiqueta: ").strip()
resultado = buscar_por_etiqueta(tag)
print(f" {len(resultado)} contacto(s) con etiqueta '{tag}':")
for c in resultado:
print(f" {c['nombre']}")
elif opcion == "4":
nombre = input(" Nombre: ").strip()
tel = input(" Teléfono: ").strip()
tags = input(" Etiquetas (separadas por coma): ").strip()
lista_tags = [t.strip() for t in tags.split(",")] if tags else []
if agregar_contacto(nombre, tel, lista_tags):
print(" Contacto añadido.")
elif opcion == "5":
resumen = resumen_etiquetas()
for tag, n in resumen.items():
print(f" {tag:<15} {n} contacto(s)")
elif opcion == "6":
print("Hasta luego.")
break
🐛 Errores clásicos con estructuras de datos
1. Modificar una lista mientras la iteras
# ❌ Comportamiento indefinido
for x in lista:
if x < 0:
lista.remove(x) # modifica la lista durante la iteración
# ✅ Iterar sobre una copia o usar comprensión
lista = [x for x in lista if x >= 0]
2. Crear un set vacío con {}
vacio = {} # ← dict vacío, NO set
vacio = set() # ← set vacío ✅
type({}) #
type(set()) #
3. Usar dict["clave"] cuando puede no existir
datos = {"nombre": "Ana"}
datos["email"] # KeyError: 'email'
datos.get("email") # None ✅
datos.get("email", "N/A") # "N/A" ✅
4. Guardar el resultado de list.sort()
ordenada = lista.sort() # sort() devuelve None
print(ordenada) # None ← trampa
ordenada = sorted(lista) # sorted() devuelve lista nueva ✅
5. Confundir list() con []
a = [1, 2, 3]
b = a # b apunta al MISMO objeto
b.append(4)
print(a) # [1, 2, 3, 4] ← a también cambió
b = a[:] # copia superficial ✅
b = list(a) # copia superficial ✅
import copy
b = copy.deepcopy(a) # copia profunda (para listas anidadas)
✅ Resumen y próximos pasos
Listas para colecciones ordenadas que cambian. Tuplas para datos fijos que comunican inmutabilidad. Diccionarios para acceso por clave con rendimiento O(1). Conjuntos para unicidad y operaciones matemáticas. Slicing para extraer sublistas elegantemente. Comprensiones para transformar y filtrar en una línea.
La siguiente lección: manejo de errores con try/except — cómo escribir código que falla de forma controlada y recuperable.
❓ Preguntas frecuentes
❓ Preguntas frecuentes sobre Estructuras de datos en Python: listas, tuplas, diccionarios y conjuntos
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Estructuras de datos en Python: listas, tuplas, diccionarios y conjuntos? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!