Pensar en recursivo sin perderse
Una función recursiva no se entiende siguiendo cien llamadas. Se diseña con un contrato, un caso base y una reducción que siempre acerca el problema a ese caso.
Para este tema conviene tener claro:Funciones, parámetros y alcance
La recursión parece un truco: una función se llama a sí misma y, de algún modo, termina. Pero no hay magia, y tampoco hace falta seguir cien llamadas en la cabeza para escribirla. Toda solución recursiva se arma con tres piezas: qué promete resolver, cuándo deja de llamar y cómo achica el problema. Si las tres están bien, la función anda.
Empezá por el contrato
Antes de escribir código, decí qué promete la función. Para total(values): devuelve la
suma de todos los números de values. Nada más.
El paso clave es confiar en ese contrato para una entrada más chica. Si la lista no
está vacía, la suma es el primer elemento más la suma del resto, y “la suma del resto” es
exactamente lo que promete total:
Pensar en recursivo es tratar esa llamada más chica como una caja negra que ya funciona. No se expande mentalmente: se usa.
El caso base
La caja negra necesita un punto donde ya conozcamos la respuesta sin preguntarle a nadie:
def total(values):
if not values:
return 0
return values[0] + total(values[1:])Antes de seguir, predecí
Sin caso base, las llamadas no terminan. Y si el caso base existe pero no cubre todas las rutas, el problema sigue ahí: cada ejecución posible tiene que llegar a una respuesta sin crear otra llamada.
Una medida que siempre baja
Tener caso base no alcanza: cada llamada tiene que acercarse a él. En total, la longitud
de la lista baja de a en cada llamada, así que después de pasos llega a
cero.
Esa es la forma práctica de demostrar que una recursión termina: elegí una medida entera no negativa y mostrá que baja estrictamente en cada llamada. Puede ser la longitud de una lista, la altura de un árbol o la distancia entre dos índices. Si la medida queda igual o a veces sube, la recursión merece sospecha.
Más a fondo · formalPor qué total es correcta: inducción sobre la longitud
Queremos probar : para toda lista de longitud , total devuelve la suma de sus
elementos.
Caso base, . La lista está vacía, la función entra en el if y devuelve ,
que es la suma de una lista vacía. vale.
Paso inductivo. Supongamos y tomemos una lista de longitud . No está
vacía, así que la función devuelve values[0] + total(values[1:]). El resto tiene longitud
, y por hipótesis inductiva total(values[1:]) es su suma. Entonces el resultado es el
primer elemento más la suma del resto, que es la suma de toda la lista. vale.
Terminación. La longitud es un entero no negativo que baja en exactamente 1 por llamada, así que no puede haber más de llamadas.
Fijate que la demostración no sigue ninguna ejecución concreta: usa el contrato para la entrada más chica, igual que cuando diseñamos la función.
La ida y la vuelta
Para escribir una función recursiva no hace falta seguir sus llamadas. Para depurarla, sí conviene haberlas visto alguna vez. Cada llamada tiene una fase de ida, donde achica el problema, y otra de vuelta, donde combina resultados.
Antes de seguir, predecí
Escena 1 — La ida achica, la vuelta suma
paso a paso
Cargando la escena…
Separar las dos fases ayuda cuando el trabajo tiene que hacerse antes o después de la llamada recursiva. En los recorridos de árboles, esa decisión es la diferencia entre preorden y postorden.
Cuando los datos ya son recursivos
La recursión se vuelve natural cuando los datos también lo son. Un árbol es un nodo con subárboles; una carpeta contiene archivos y otras carpetas; una expresión tiene subexpresiones. El código puede copiar esa forma:
def tree_sum(node):
return node["value"] + sum(tree_sum(child) for child in node["children"])Acá ni siquiera hace falta un if para el caso base: una hoja no tiene hijos, sum de
nada da 0, y la recursión se corta sola.
Los tres errores posibles
Casi todo bug en una función recursiva es una de las tres piezas rota, y cada una deja un síntoma distinto:
| Pieza rota | Síntoma | Cómo encontrarlo |
|---|---|---|
| Caso base incompleto | RecursionError sólo con algunas entradas | Probá la entrada más chica y las que se pasan de largo: negativos, vacíos, None |
| Reducción que no progresa | RecursionError con cualquier entrada | Verificá que la medida baje en cada llamada |
| Combinación incorrecta | Termina, pero el resultado está mal | Probá el caso que necesita exactamente una llamada |
Un orden de pruebas que encuentra la pieza rota rápido: el caso base, después el ejemplo más chico que exige una llamada, y recién entonces uno con varias.
Cierre
Autoevaluación
¿Lo entendiste?
Práctica