Indice / Herramientas / 0.1
Depurar en VS Code: breakpoints y la consola
Un programa que no anda no se arregla mirandolo fijo. Se para en el medio, se abre y se mira que tiene adentro. Esta leccion es la herramienta que hace eso, y es la que mas tiempo te va a ahorrar en toda la materia.
Para que sirve
Cuando algo da mal, casi todo el mundo hace lo mismo: agrega un
print, vuelve a correr, mira, agrega otro print, vuelve a
correr. Funciona, y por eso cuesta abandonarlo. Pero tiene tres costos que se pagan
todos los dias:
- Cada pregunta cuesta una corrida entera. Con un archivo de 500 filas no se nota; con uno de dos millones, si.
- Solo ves lo que se te ocurrio imprimir. Lo que no sospechabas queda invisible, y el error casi siempre esta ahi.
- Los
printquedan. Se olvidan, se commitean, y despues alguien borra el que si servia.
El depurador da vuelta la relacion: en vez de vos preguntandole cosas al
programa de a una y por adelantado, el programa se queda quieto y
vos le preguntas lo que quieras, en el orden que quieras, con los datos de verdad
cargados en memoria. Lo vas a usar cada vez que un TypeError no diga
en que columna, que es casi siempre.
ELI5Explicado facil
Imaginate que se te pincho la bici y para revisarla tenes que andar. Vas
pedaleando y gritas «¿ahora anda?», «¿y
ahora?». Eso es llenar el codigo de print.
El depurador es bajarte y frenar la rueda con la mano. La bici queda quieta en la posicion exacta en la que fallaba, y podes tocar todo: girar el pedal despacio, mirar la cadena, apretar el freno. Cuando terminas, la soltas y sigue andando desde donde estaba.
Lo minimo que hay que saber
Son cinco teclas y un click. Con eso alcanza para el 90 % de los casos; el resto de la leccion es sacarle punta.
| Gesto | Que hace | Cuando |
|---|---|---|
| Click en la canaleta (o F9) | Pone o saca un breakpoint: un punto rojo a la izquierda del numero de linea | Donde queres que frene |
| F5 | Iniciar la depuracion, o continuar hasta el proximo breakpoint | Arrancar, y saltar de parada en parada |
| F10 | Step Over: ejecuta la linea entera y frena en la siguiente | El paso por defecto |
| F11 | Step Into: si la linea llama a una funcion tuya, entra adentro | Cuando sospechas de la funcion, no de la linea |
| Shift+F11 | Step Out: termina la funcion actual y vuelve a quien la llamo | Cuando entraste de mas |
| Shift+F5 | Detener | Al terminar |
F10 pregunta «¿que hace esta linea?» y te muestra el resultado. F11 pregunta «¿por que esta linea hace eso?» y te mete adentro. Si ya sabes que la funcion anda bien, F10. Si el resultado de la funcion es justamente lo que no entendes, F11.
El ejemplo minimo
Este es el programa que vamos a recorrer. Calcula el promedio de las tres notas del primer estudiante, sin pandas todavia: solo la biblioteca estandar.
import csv
def promedio(valores):
total = 0.0
for v in valores:
total = total + float(v)
return total / len(valores)
with open("estudiantes_notas.csv", encoding="utf-8") as f:
filas = list(csv.DictReader(f))
fila = filas[0]
notas = [fila["nota_1"], fila["nota_2"], fila["nota_3"]]
print(fila["estudiante"], promedio(notas))
Karina Acosta 7.666666666666667
Lo mismo, paso a paso
Nueve pasos: poner el breakpoint, arrancar, avanzar linea por linea, entrar en la funcion y preguntarle cosas a la consola con el programa detenido. Mira las tres zonas a la vez - el codigo, las variables y la consola -, porque aprender a mirarlas juntas es la habilidad.
- Click en la canaletaUn click en el espacio a la izquierda del numero 12 deja un punto rojo. Eso es un breakpoint: la orden de frenar justo antes de ejecutar esa linea. Todavia no corrio nada.
fila = filas[0] - F5 - IniciarEl programa arranca y se detiene solo al llegar al breakpoint. La flecha marca la linea 12: esta por ejecutarse, todavia no se ejecuto. Las lineas 1 a 10 ya corrieron, y por eso filas ya tiene las 40 filas del archivo.
filas = list(csv.DictReader(f)) - F10 - Step OverStep Over ejecuta la linea 12 entera y frena en la 13. Aparece fila en el panel: es un diccionario, y todos sus valores son texto, porque csv.DictReader no convierte tipos. Ahi ya hay una pista de lo que va a pasar despues.
fila = filas[0] - F10 - Step OverOtro Step Over y ya esta armada la lista notas. Se ve clarito que son tres cadenas, no tres numeros: llevan comillas.
notas = [fila["nota_1"], fila["nota_2"], fila["nota_3"]] - F11 - Step IntoAca esta la diferencia que importa. Step Over habria ejecutado promedio(notas) de un saque y mostrado el resultado. Step Into entra adentro de la funcion: la flecha salta a la linea 4 y el panel cambia, porque ahora muestra las variables de la funcion, no las del programa principal.
def promedio(valores): - F10 x2Dos pasos mas: total ya existe y vale 0.0, y el for tomo el primer valor. v es la cadena '9.7', con comillas. Que el panel muestre las comillas es la forma mas rapida de distinguir un numero de un texto que parece numero.
for v in valores: - Consola de depuracionCon la ejecucion detenida, la consola es un interprete parado adentro del programa: lo que se escribe ahi se evalua con las variables tal como estan en este instante. Aca se prueba la conversion antes de que el programa la haga.
float(v) - F10 x3Tres vueltas del for despues, total llego a 23.0. Fijate en el valor intermedio: 9.7 + 4.7 no dio 14.4 sino 14.399999999999999. No es un error del depurador, es como funcionan los flotantes, y verlo aca ahorra un susto mas adelante.
total = total + float(v) - F5 - ContinuarContinuar deja correr hasta el proximo breakpoint o hasta el final. Como no hay otro, el programa termina e imprime. El promedio salio bien porque esta fila no tiene ningun valor raro: el problema aparece con el archivo entero, y de eso se trata la animacion siguiente.
print(fila["estudiante"], promedio(notas))
Cuando el panel muestra '9.7' con comillas, es un texto. Cuando
muestra 9.7 sin comillas, es un numero. Esa diferencia de dos
caracteres es la causa de la mitad de los errores de esta materia, y en el panel se
ve de un vistazo. En un print no se ve: print("9.7") y
print(9.7) escriben lo mismo en la pantalla.
Sobre datos de verdad
El programa de arriba anda. Ahora el mismo calculo, con pandas, sobre las 40 filas del archivo:
import pandas as pd
notas = pd.read_csv("estudiantes_notas.csv")
columnas = ["nota_1", "nota_2", "nota_3"]
try:
notas["promedio"] = notas[columnas].mean(axis=1)
print(notas["promedio"].round(2).head())
except TypeError as error:
print("TypeError:", error)
print()
print("El mensaje no dice que columna, ni que fila, ni que valor.")
print("Dice que en algun lado se intento sumar un float con un str.")
TypeError: unsupported operand type(s) for +: 'float' and 'str'
El mensaje no dice que columna, ni que fila, ni que valor.
Dice que en algun lado se intento sumar un float con un str.
Este es el error tipico de la materia, y el mensaje es un ejemplo perfecto de lo
inutil que puede ser un mensaje correcto. Dice la verdad - alguien sumo un
float con un str - y no dice nada de lo que
necesitas: que columna, que fila, que valor. Con tres columnas y
40 filas hay 120 candidatos.
Encontrarlo con el depurador
- EjecutarEl programa corre y revienta. El mensaje es cierto pero inutil: no dice que columna, ni que fila, ni que valor. En algun lado, entre 40 filas y 3 columnas, hay un texto donde deberia haber un numero.
notas["promedio"] = notas[columnas].mean(axis=1) - Breakpoint en la 6 + F5Un breakpoint en la linea que falla y F5. El programa se detiene justo antes de romperse, con todo cargado. El panel dice que notas tiene 40 filas y 5 columnas, pero no dice de que tipo es cada una: para eso hay que preguntar.
notas = pd.read_csv("estudiantes_notas.csv") - Consola de depuracionPrimera pregunta, y casi siempre la que resuelve el caso: de que tipo quedo cada columna. nota_1 y nota_2 son float64. nota_3 quedo str. Esa sola linea explica el error entero: pandas intento sumar un float con un texto.
notas.dtypes - Consola de depuracionSegunda pregunta: que valor es el que no es un numero. Ordenar los valores distintos pone los raros primero, y ahi esta el culpable: un guion suelto. Como el guion no esta en la lista de ausentes que read_csv reconoce por defecto, la columna entera se leyo como texto.
sorted(notas["nota_3"].dropna().unique())[:4] - Consola de depuracionTercera pregunta: cuantas filas y cuales. Dos filas de cuarenta alcanzaron para cambiar el tipo de la columna entera y voltear el programa. Ahora el problema tiene nombre y apellido, y se puede ir a mirar el archivo.
notas.loc[notas["nota_3"] == "-", "legajo"].tolist() - Consola de depuracionAca esta lo que casi nadie usa: en la consola no solo se puede preguntar, tambien se puede hacer. Volvemos a leer el archivo declarando que el guion significa ausente, sin cerrar el depurador ni reiniciar nada.
notas = pd.read_csv("estudiantes_notas.csv", na_values=["-"]) - Consola de depuracionY se prueba ahi mismo la linea que fallaba. Anda. Recien ahora sabemos que el arreglo es el correcto, antes de tocar el archivo: lo probamos sobre los datos de verdad.
notas[columnas].mean(axis=1).round(2).head() - Arreglar y sacar el breakpointUltimo paso, el que se olvida: llevar el arreglo al archivo, sacar el breakpoint y correr normal. El guion ahora se lee como ausente, nota_3 es float64 y el promedio sale. Cinco preguntas en la consola, ningun print agregado y ninguno que borrar despues.
notas = pd.read_csv("estudiantes_notas.csv", na_values=["-"])
Cinco preguntas y el caso esta cerrado. Lo importante no es el resultado sino el orden, que es siempre el mismo y sirve para cualquier error de tipos:
notas.dtypes- ¿de que tipo quedo cada columna?sorted(col.dropna().unique())[:4]- ¿cual es el valor raro?(col == "-").sum()- ¿cuantas filas?df.loc[col == "-", "legajo"]- ¿cuales?- Probar el arreglo ahi mismo, antes de tocar el archivo.
Las tres primeras, escritas como programa, imprimen esto:
import pandas as pd
notas = pd.read_csv("estudiantes_notas.csv")
# Cada una de estas lineas es una expresion que se tipea en la consola con la
# ejecucion detenida. Aca van juntas para poder mostrar sus salidas.
print(notas.dtypes)
print()
print("primeros valores distintos de nota_3, ordenados:")
print(sorted(notas["nota_3"].dropna().unique())[:4])
print()
print("cuantas filas tienen el guion:", int((notas["nota_3"] == "-").sum()))
legajo str
estudiante str
nota_1 float64
nota_2 float64
nota_3 str
dtype: object
primeros valores distintos de nota_3, ordenados:
['-', '-1.0', '3.2', '3.4']
cuantas filas tienen el guion: 2
Y la cuarta le pone nombre y apellido al problema:
import pandas as pd
notas = pd.read_csv("estudiantes_notas.csv")
culpables = notas["nota_3"] == "-"
print(notas.loc[culpables].to_string(index=False))
print()
print("Esa misma condicion sirve como breakpoint condicional:")
print(' notas["nota_3"][i] == "-"')
print("y el depurador se detiene solo en esas filas.")
legajo estudiante nota_1 nota_2 nota_3
L2621 Zacarías Juárez 3.5 5.5 -
L2629 Zacarías Gómez 5.7 5.1 -
Esa misma condicion sirve como breakpoint condicional:
notas["nota_3"][i] == "-"
y el depurador se detiene solo en esas filas.
Un solo - en una columna alcanza para que read_csv la
lea entera como texto, porque el guion no esta en la lista de
tokens de ausente que pandas reconoce por defecto (NaN,
N/A y null si estan; el guion no). Es la trampa que mas
veces vas a encontrar en esta materia, y la vas a ver en detalle en la
leccion 5.4.
Con el arreglo puesto, el programa anda y ademas queda declarado en el codigo que el guion significa ausente:
import pandas as pd
notas = pd.read_csv("estudiantes_notas.csv", na_values=["-"])
columnas = ["nota_1", "nota_2", "nota_3"]
print("dtype de nota_3:", notas["nota_3"].dtype)
print("ausentes en nota_3:", int(notas["nota_3"].isna().sum()))
notas["promedio"] = notas[columnas].mean(axis=1)
print()
print(notas[["legajo", "estudiante", "promedio"]].head(3).round(2).to_string(index=False))
dtype de nota_3: float64
ausentes en nota_3: 5
legajo estudiante promedio
L2601 Karina Acosta 7.67
L2602 Santiago Ramos 4.67
L2603 Carla Luna 5.17
La consola de depuracion
Si de toda la leccion te queda una sola cosa, que sea esta. La Consola de depuracion (Ctrl+Shift+Y) no es un panel de mensajes: es un interprete de Python parado adentro de tu programa, en la linea exacta donde esta detenido y con todas sus variables vivas.
Eso cambia como se trabaja. Lo que en un cuaderno de Jupyter harias en una celda nueva, aca lo haces sobre los datos reales del programa, en el punto exacto del problema, sin volver a cargar nada:
| Se escribe | Que devuelve |
|---|---|
notas.dtypes | El tipo de cada columna, ahora |
notas.head(3) | Las primeras filas, formateadas |
notas["nota_3"].unique() | Los valores distintos, para cazar el raro |
len(filas) | Cualquier expresion de Python, no solo variables |
notas = pd.read_csv(...) | Tambien se puede hacer: probar el arreglo sin reiniciar |
ELI5Explicado facil
Es como poder pausar una pelicula y meterte adentro de la escena a revisarle los bolsillos a los personajes. La pelicula queda congelada, vos revisas todo lo que quieras, y cuando le das play sigue como si nada.
Tres cosas mas que rinden mucho
Breakpoint condicional
Frenar en cada una de las 40 vueltas de un bucle para encontrar la fila mala es insoportable. Click derecho en la canaleta → Add Conditional Breakpoint, y se escribe una expresion de Python: el programa frena solo cuando esa expresion da verdadero.
Con el caso de arriba, la condicion seria fila["nota_3"] == "-": de
40 vueltas, frena en 2. Es la diferencia entre usar el depurador y odiarlo.
Logpoint
Click derecho → Add Logpoint. Es un breakpoint que no frena:
escribe un mensaje en la consola y sigue. Sirve para lo mismo que un
print, con dos ventajas: no toca el archivo (asi que
no hay nada que borrar despues) y se pone o se saca sin volver a correr.
Watch
En el panel Watch se anotan expresiones que se recalculan solas en cada
paso. Poner ahi notas["nota_3"].dtype y avanzar es ver el tipo de la
columna cambiar en vivo, sin escribirlo de nuevo cada vez.
breakpoint()
Escribir breakpoint() en una linea cualquiera y correr el programa
normal, desde la terminal, abre el depurador de la biblioteca estandar
(pdb) en ese punto. Los comandos son n (siguiente),
s (entrar), c (continuar), p variable
(imprimir) y q (salir). Es la misma idea sin ventanas, y funciona en un
servidor por SSH donde no hay VS Code.
Donde se rompe
Causa: el depurador arranca el programa desde la carpeta que tenes
abierta en VS Code, no desde la carpeta del archivo .py. Si el CSV
esta al lado del script pero abriste una carpeta de mas arriba, el nombre pelado
no resuelve.
Arreglo: abri en VS Code la carpeta donde estan los datos, o agrega
"cwd" en la configuracion de launch.json. Es el error
numero uno de esta materia, porque todos los ejemplos abren los archivos por
nombre pelado.
Causa: el depurador esta usando otro interprete de Python que el que usas en la terminal - tipicamente el del sistema en vez del entorno virtual.
Arreglo: Ctrl+Shift+P → Python: Select Interpreter, y elegi el mismo que usas para correr. El interprete elegido se ve abajo a la derecha, en la barra de estado.
Causa: casi siempre, esa linea no se ejecuta. Esta adentro de un
if que da falso, de una funcion que nadie llama, o despues de un
return.
Arreglo: pone otro breakpoint en la primera linea del archivo. Si en ese si frena, el problema no es el depurador: es que tu idea de por donde pasa el programa esta equivocada - que ya es un hallazgo.
Causa: la funcion es de una biblioteca, no tuya. Por defecto el
depurador de Python trae justMyCode activado y saltea el codigo de
terceros.
Arreglo: casi siempre no querias entrar en pandas. Si de
verdad hace falta, se pone "justMyCode": false en
launch.json.
Causa: muestra un resumen, no los datos. Es a proposito: dibujar 500 000 filas colgaria el editor.
Arreglo: pedile lo que necesitas por la consola
(notas.head(10), notas.describe()). Y si mirar tablas
grandes es tu tarea de todos los dias, mira la leccion 0.2: hay un IDE con una
planilla de verdad para eso.
Aca hay que elegir
print o depurador
No es que uno este mal. Es que sirven para cosas distintas y conviene saber cual:
print- cuando ya sabes que queres mirar y lo queres ver en cada vuelta de un bucle largo, o cuando el programa corre donde no podes conectarte. Unprintdeja rastro escrito; el depurador no.- Depurador - cuando no sabes que mirar. Que es
la situacion normal cuando algo falla y no entendes por que. Cinco preguntas en la
consola valen veinte corridas con
print. - Logpoint - cuando queres el rastro escrito del
printpero sin ensuciar el archivo.
La regla practica: si podes nombrar la variable que queres ver,
print alcanza. Si tu pregunta empieza con «pero por
que…», es depurador.
Recetario
| Gesto o expresion | Que hace | Cuando |
|---|---|---|
| F9 | Pone o saca el breakpoint de la linea actual | Sin soltar el teclado |
| F5 | Inicia, o continua hasta el proximo breakpoint | Arrancar y saltar |
| Ctrl+F5 | Corre sin depurar | Cuando solo queres el resultado |
| F10 / F11 / Shift+F11 | Step Over / Into / Out | Avanzar con control |
| Ctrl+Shift+D | Abre el panel Run and Debug | Ver variables, pila y watch |
| Ctrl+Shift+Y | Abre la consola de depuracion | Preguntar con el programa detenido |
| Click derecho → Conditional Breakpoint | Frena solo si la expresion da verdadero | Buscar una fila entre miles |
| Click derecho → Logpoint | Escribe un mensaje y sigue, sin tocar el archivo | print sin dejar rastro |
| Panel Watch | Recalcula una expresion en cada paso | Seguir un valor que cambia |
"justMyCode": false | Deja entrar en el codigo de las bibliotecas | Rara vez; casi nunca es lo que queres |
"cwd" en launch.json | Fija desde que carpeta corre el programa | Cuando los read_csv no encuentran nada |
breakpoint() | Abre pdb en esa linea, sin IDE | Por SSH, o sin VS Code |