El módulo 3 terminó señalando una grieta: el catálogo de Papyrus vive repartido en tres listas paralelas (catalogo, precios, stocks) que Ana debe mantener sincronizadas a mano. Antes de resolver esa grieta con diccionarios (lección 04-03), necesitas dominar de verdad la estructura que has estado usando desde el módulo 1 casi sin mirarla: la lista. Hasta ahora has recorrido listas con for, enumerate() y zip(), y has usado append() de pasada; en esta lección las abrimos por dentro: indexación, slicing, todos sus métodos importantes y —lo más delicado— qué significa que sean mutables.
Contenido
- Crear listas
- Indexación positiva y negativa
- Slicing a fondo
- Métodos de listas: añadir, quitar y buscar
- Ordenar:
sort()vssorted()yreverse() - Mutabilidad: alias vs copia
- Listas anidadas
- Pertenencia:
inynot in
Crear listas
Una lista es una secuencia ordenada y mutable de elementos, que pueden ser de cualquier tipo (incluso mezclados, aunque no suele ser buena idea).
# Las tres listas paralelas de Papyrus, tal y como quedaron en el módulo 3
catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
stocks = [4, 6, 8]
vacia = [] # lista vacía, lista para ir llenándola
tambien_vacia = list() # equivalente
desde_range = list(range(5)) # [0, 1, 2, 3, 4]: list() convierte iterables
letras = list("Ana") # ['A', 'n', 'a']: una cadena también es iterable[]crea una lista literal;list(iterable)construye una lista a partir de cualquier cosa recorrible.- Recuerda del módulo 2 que también existen las comprensiones:
[p * 1.04 for p in precios]crea una lista nueva calculada. Aquí nos centramos en manipular listas ya existentes.
Indexación positiva y negativa
Cada elemento tiene una posición (índice) que empieza en 0. Los índices negativos cuentan desde el final: -1 es el último elemento.
catalogo = ["La Odisea", "Hamlet", "El Quijote"]
print(catalogo[0]) # La Odisea (primero)
print(catalogo[2]) # El Quijote (tercero)
print(catalogo[-1]) # El Quijote (último, sin saber cuántos hay)
print(catalogo[-2]) # Hamlet (penúltimo)| Elemento | Índice positivo | Índice negativo |
|---|---|---|
| "La Odisea" | 0 | -3 |
| "Hamlet" | 1 | -2 |
| "El Quijote" | 2 | -1 |
Acceder a un índice que no existe (catalogo[10]) provoca un IndexError que detiene el programa. En el módulo 7 aprenderás a capturar ese error; de momento, comprueba antes con len().
Slicing a fondo
El slicing (rebanado) extrae una sublista nueva con la sintaxis lista[inicio:fin:paso]. Regla de oro: el inicio se incluye, el fin no.
titulos = ["La Odisea", "Hamlet", "El Quijote", "Fausto"]
print(titulos[0:2]) # ['La Odisea', 'Hamlet'] índices 0 y 1
print(titulos[1:]) # ['Hamlet', 'El Quijote', 'Fausto'] desde 1 hasta el final
print(titulos[:2]) # ['La Odisea', 'Hamlet'] desde el principio hasta 1
print(titulos[:]) # copia completa (¡lo veremos en mutabilidad!)
print(titulos[::2]) # ['La Odisea', 'El Quijote'] de dos en dos
print(titulos[::-1]) # lista invertida, sin tocar la original
print(titulos[-2:]) # ['El Quijote', 'Fausto'] los dos últimosPuntos que conviene interiorizar:
- Omitir
iniciosignifica "desde el principio"; omitirfin, "hasta el final". - Un slice nunca da error por pasarse:
titulos[1:100]devuelve lo que haya, sinIndexError. pasonegativo recorre hacia atrás;[::-1]es el idiom clásico para invertir.- El resultado es siempre una lista nueva: modificarla no afecta a la original.
Métodos de listas: añadir, quitar y buscar
Los métodos se llaman con la sintaxis lista.metodo(...). La mayoría modifican la lista en el sitio y devuelven None — fuente clásica de despistes.
| Método | Qué hace | Devuelve |
|---|---|---|
append(x) |
Añade x al final |
None |
insert(i, x) |
Inserta x en la posición i, desplazando el resto |
None |
extend(otra) |
Añade todos los elementos de otro iterable al final | None |
remove(x) |
Elimina la primera aparición de x (error si no está) |
None |
pop(i) |
Elimina y devuelve el elemento en i (por defecto, el último) |
el elemento |
index(x) |
Posición de la primera aparición de x (error si no está) |
int |
count(x) |
Cuántas veces aparece x |
int |
Veámoslos en acción con la llegada de "Fausto" a Papyrus (de momento con stock 0: está agotado en la distribuidora):
catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
stocks = [4, 6, 8]
# Alta de "Fausto": hay que tocar LAS TRES listas, y en el mismo orden
catalogo.append("Fausto")
precios.append(21.00)
stocks.append(0)
print(catalogo.index("Hamlet")) # 1 → sirve para localizar su precio: precios[1]
print(catalogo.count("Fausto")) # 1
# append vs extend: la diferencia importa
novedades = ["Ilíada", "Macbeth"]
copia = catalogo[:]
copia.append(novedades) # ['La Odisea', ..., 'Fausto', ['Ilíada', 'Macbeth']] ¡lista dentro de lista!
copia = catalogo[:]
copia.extend(novedades) # ['La Odisea', ..., 'Fausto', 'Ilíada', 'Macbeth'] elementos sueltosFíjate en el coste de mantenimiento: dar de alta un libro exige tres append() coordinados, y un remove() olvidado en una de las listas desincroniza precios y stocks para siempre. Este dolor es deliberado: es el gancho que resolveremos en 04-03.
La cola de pedidos
Las listas también sirven como cola: los pedidos entran por el final (append) y se atienden por el principio (pop(0)):
cola_pedidos = []
cola_pedidos.append("Luis: El Quijote")
cola_pedidos.append("Marta: Hamlet")
cola_pedidos.append("Luis: Fausto")
while cola_pedidos: # truthiness: lista vacía es False
pedido = cola_pedidos.pop(0) # saca y devuelve el primero
print(f"Atendiendo -> {pedido}")Funciona, pero pop(0) obliga a Python a desplazar todos los elementos restantes una posición. Para colas largas existe una estructura pensada exactamente para esto, collections.deque, que verás en 04-06.
Ordenar: sort() vs sorted() y reverse()
Esta distinción resume la filosofía de los métodos de lista:
lista.sort() |
sorted(lista) |
|
|---|---|---|
| ¿Qué modifica? | La lista original, en el sitio | Nada: crea una lista nueva |
| ¿Qué devuelve? | None |
La lista ordenada |
| ¿Sobre qué funciona? | Solo listas | Cualquier iterable (tuplas, cadenas...) |
| Uso típico | Cuando ya no necesitas el orden original | Cuando quieres conservar el original |
precios = [12.50, 9.95, 15.90, 21.00]
baratos_primero = sorted(precios) # [9.95, 12.5, 15.9, 21.0]; precios intacto
caros_primero = sorted(precios, reverse=True)
precios.sort() # ahora sí: precios queda ordenado
resultado = precios.sort() # ¡cuidado! resultado es None
catalogo = ["La Odisea", "Hamlet", "El Quijote", "Fausto"]
catalogo.reverse() # invierte en el sitio (devuelve None)
por_longitud = sorted(catalogo, key=len) # el key= de lambdas (03-03) también vive aquíOjo con las listas paralelas: si Ana hace catalogo.sort(), los títulos se reordenan pero precios y stocks no, y el desastre está servido. Con zip() (03-02) se puede ordenar todo junto, pero es otro malabar más que los diccionarios harán innecesario.
Mutabilidad: alias vs copia
Aquí está la consecuencia más importante (y traicionera) de que las listas sean mutables. Asignar una lista a otra variable no la copia: crea un alias, un segundo nombre para la misma lista.
stocks = [4, 6, 8]
inventario = stocks # ALIAS: las dos variables apuntan a LA MISMA lista
inventario.append(0)
print(stocks) # [4, 6, 8, 0] ← ¡stocks también "cambió"!
print(stocks is inventario) # True: son el mismo objetograph LR
stocks --> L["[4, 6, 8, 0]"]
inventario --> L
Para tener una lista independiente hay que copiar explícitamente:
copia1 = stocks.copy() # método copy()
copia2 = stocks[:] # slice completo: idiom equivalente
copia3 = list(stocks) # constructor: también copia
copia1.append(99)
print(stocks) # sin cambios: la copia es otra listaEsto explica también la trampa de los argumentos mutables por defecto que viste en 03-02: si una función recibe una lista y la modifica, el que llama nota el cambio, porque ambos comparten el mismo objeto.
Un matiz para dejar apuntado: copy() y [:] hacen una copia superficial (shallow). Si la lista contiene otras listas dentro, se copia la lista exterior pero las interiores siguen compartidas. Para copias profundas existe copy.deepcopy(); con saber que existe es suficiente por ahora.
Listas anidadas
Una lista puede contener listas. Es una forma primitiva de agrupar la ficha de cada libro:
fichas = [
["La Odisea", 12.50, 4],
["Hamlet", 9.95, 6],
["El Quijote", 15.90, 8],
]
print(fichas[1]) # ['Hamlet', 9.95, 6] → la ficha completa
print(fichas[1][1]) # 9.95 → primer índice: fila; segundo: columna
fichas[2][2] -= 1 # Luis compra un Quijote: el stock baja a 7
for titulo, precio, stock in fichas: # desempaquetado en el for, como con zip()
print(f"{titulo:<12} {precio:>6.2f} EUR ({stock} uds.)")Ya es un avance: cada libro viaja junto y un solo append(["Fausto", 21.00, 0]) da de alta todo. Pero ficha[1] y ficha[2] son índices mudos: nada dice que 1 sea el precio. La siguiente lección (tuplas) y sobre todo la 04-03 (diccionarios) pulirán esta idea.
Pertenencia: in y not in
Los operadores in / not in comprueban si un valor está en la lista, sin bucles a la vista:
catalogo = ["La Odisea", "Hamlet", "El Quijote", "Fausto"]
if "Fausto" in catalogo:
print("Lo tenemos fichado (aunque quizá sin stock).")
if "Drácula" not in catalogo:
print("Habría que pedirlo a la distribuidora.")Es una comparación exacta: "fausto" in catalogo da False por las mayúsculas. Por eso buscar_libro() de papyrus_utils.py compara en minúsculas; en 04-05 iremos aún más lejos normalizando texto.
Errores Comunes y Consejos
- Guardar el resultado de
sort(),append()oreverse():lista = lista.sort()dejalistavaliendoNone. Estos métodos modifican en el sitio y devuelvenNone; no reasignes. - Confundir alias con copia:
b = ano copia. Si luego mutasbya"cambia solo", ese es el síntoma. Usaa.copy()oa[:]. remove()eindex()con valores inexistentes lanzanValueError. Protege conif x in lista:antes de llamar (la gestión elegante de errores llega en el módulo 7).- Modificar una lista mientras la recorres con
forproduce saltos de elementos. Recorre una copia (for x in lista[:]) o construye una lista nueva con una comprensión. append(otra_lista)cuando queríasextend(otra_lista): el primero mete la lista entera como un único elemento anidado.- Consejo: cuando dudes entre mutar o crear, pregúntate si alguien más usa esa lista. Si la recibiste como argumento, crear una nueva (
sorted, slicing, comprensión) es casi siempre más seguro.
Ejercicios
- Alta y baja sincronizadas. Partiendo de las tres listas paralelas de Papyrus, escribe una función
alta_libro(catalogo, precios, stocks, titulo, precio, stock)que añada el libro a las tres listas, y unabaja_libro(catalogo, precios, stocks, titulo)que lo elimine de las tres usandoindex()ypop(). Prueba a dar de alta "Fausto" (21.00, 0) y de baja "Hamlet". - Rebanadas del catálogo. Con
catalogo = ["La Odisea", "Hamlet", "El Quijote", "Fausto"], obtén usando solo slicing: (a) los dos primeros títulos, (b) los dos últimos, (c) el catálogo invertido, (d) una copia independiente que puedas ordenar alfabéticamente sin tocar el original. - Cola con prioridad de socio. Simula la cola de pedidos como lista de cadenas
"nombre: título". Los pedidos de Luis (socio) deben colarse: escríbelos coninsert(0, ...)en vez deappend(). Atiende la cola conwhileypop(0)mostrando el orden final.
Soluciones
# Ejercicio 1
def alta_libro(catalogo, precios, stocks, titulo, precio, stock):
"""Añade un libro a las tres listas paralelas (mutándolas)."""
catalogo.append(titulo)
precios.append(precio)
stocks.append(stock)
def baja_libro(catalogo, precios, stocks, titulo):
"""Elimina un libro de las tres listas; devuelve su precio y stock."""
if titulo not in catalogo:
return None # centinela, como en 03-02
i = catalogo.index(titulo) # misma fila en las tres listas
catalogo.pop(i)
return (catalogo, precios.pop(i), stocks.pop(i))[1:] # (precio, stock)
catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
stocks = [4, 6, 8]
alta_libro(catalogo, precios, stocks, "Fausto", 21.00, 0)
print(baja_libro(catalogo, precios, stocks, "Hamlet")) # (9.95, 6)
print(catalogo) # ['La Odisea', 'El Quijote', 'Fausto']Fíjate en que las funciones mutan las listas recibidas: quien llama ve los cambios porque comparte los objetos. Y en que cada operación toca tres estructuras — el argumento definitivo para 04-03.
# Ejercicio 2
catalogo = ["La Odisea", "Hamlet", "El Quijote", "Fausto"]
print(catalogo[:2]) # (a) ['La Odisea', 'Hamlet']
print(catalogo[-2:]) # (b) ['El Quijote', 'Fausto']
print(catalogo[::-1]) # (c) invertido
copia = catalogo[:] # (d) copia independiente
copia.sort()
print(copia) # ordenada
print(catalogo) # el original, intacto# Ejercicio 3
cola = []
def encolar(cola, cliente, titulo, socio=False):
pedido = f"{cliente}: {titulo}"
if socio:
cola.insert(0, pedido) # los socios se cuelan al principio
else:
cola.append(pedido)
encolar(cola, "Marta", "Hamlet")
encolar(cola, "Luis", "El Quijote", socio=True)
encolar(cola, "Pau", "La Odisea")
while cola:
print("Atendiendo ->", cola.pop(0))
# Luis: El Quijote / Marta: Hamlet / Pau: La OdiseaConsejo: insert(0, ...) y pop(0) desplazan toda la lista; para Papyrus es irrelevante, pero recuérdalo cuando veas deque en 04-06.
Conclusión
Las listas son la navaja suiza de Python: sabes crearlas, indexarlas (también desde el final), rebanarlas con [inicio:fin:paso], y manejar sus métodos distinguiendo los que mutan en el sitio (append, sort, remove...) de las funciones que crean listas nuevas (sorted, slicing). Sobre todo, ahora entiendes la mutabilidad: b = a crea un alias, no una copia, y ese detalle explica comportamientos que hasta hoy parecían magia negra. También has visto sus límites en Papyrus: las listas paralelas exigen disciplina de sincronización y las listas anidadas usan índices mudos. El siguiente paso es su hermana inmutable, la tupla: la estructura perfecta para registros que no deben cambiar — y la que por fin salda la promesa del retorno múltiple de funciones que dejamos pendiente en 03-02.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
