La culebrilla que se muerde la cola
-
Recursividad en Python
🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍
¡Hola!
/JavierLuna
@javierlunamolina
Soy Javi, Luna para los amigos
/in/jlunamolina/
Soy Javi, Luna para los amigos
- Ahora trabajo como SDE en Amazon Web Services, en Berlín
- ¡Amo el open source!
- pycaprio
- datastorm
- El resto de mis movidas raras
- Soy muy inquieto y me gusta meterme en múchos ámbitos de software distintos
Mi 😡 con la recursividad
Una historia triste pero real
+
🐍🐍🐍🐍🐍🐍🐍🐍🐍
=
❤️
"Joe es que me resulta muy liosa"
"para entender la recursividad primero hay que entender la recursividad"
Recursividad starter pack
Factorial!
¿Y qué es la recursividad?
📖 "Es un método de resolución de problemas en el que la solución depende de soluciones a instancias más pequeñas del mismo problema" 📖
Buscar un archivo "ejemplo.py"
📂 /jluna
📄 EnLaPlaya.png.exe
📂 /Downloads
📂 /home
📄 illo.txt
📂 /jluna
📂 /Downloads
🐍 ejemplo.py
Búsqueda binaria: ¿Está el 4?
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4]
[3, 4]
Nop, pero 4 < 5
Nop, pero 4 > 2
Oh yeah!!
Tipos de recursividad
- Simple: Una sóla referencia a sí misma
- Compuesta: Varias referencias a sí misma
- Indirecta: Llama a otra función que vuelve a llamar a la función original
- Anónima: Variante de la simple/compuesta pero con funciones anónimas
def factorial(n):
if n in {0, 1}:
return 1
return n * factorial(n-1) <---
def fib(n):
if n in {0, 1}:
return n
return fib(n-1) + fib(n-2) <-- 2x
###
def a():
b() <---
def b():
a() <---
###
def f(callable):
callable() <---
Si os fijáis...
- Las soluciones "más naturales" a estos problemas son recursivas. ¿Por qué?
- Son las estructuras de datos las que sugieren un algoritmo recursivo o no
¿Y qué tipo de recursividad "me deja Python usar"?
...
def f9996():
return f9995()
def f9997():
return f9996()
def f9998():
return f9997()
def f9999():
return f9998()
f9999()
😭
File "recursive.py", line 18004, in f9001
return f9000()
RecursionError: maximum recursion depth exceeded
Enter...
"The Stack"
f_a(1,2)
f_b()
f_c(42)
f_d("hello!")
Pequeño recordatorio
push(element)
pop() -> element
El Call Stack
__main__()
def fun_a():
return "Hello!"
def fun_b():
return fun_a()
fun_b() <---
__main__() -> fun_b()
El Call Stack
__main__
def fun_a():
return "Hello!"
def fun_b(): <---
return fun_a() <---
fun_b()
__main__()
└──fun_b() -> fun_a()
fun_b()
El Call Stack
__main__
def fun_a(): <---
return "Hello!" <---
def fun_b():
return fun_a()
fun_b()
__main__()
└──fun_b()
└── fun_a() -> "Hello!"
fun_b()
fun_a()
El Call Stack
__main__
def fun_a():
return "Hello!"
def fun_b():
return fun_a() <--- "Hello!"
fun_b()
__main__()
└──fun_b() -> "Hello!"
fun_b()
El Call Stack
__main__
def fun_a():
return "Hello!"
def fun_b():
return fun_a()
fun_b() <--- "Hello!"
__main__() -> "Hello!"
Los stack frames
def f_a(arg1, arg2):
local_var = 3
return f_b(3, 4)
Loaded variables:
arg1, arg2, local_var, f_b
Code itself
Bookeeping:
nombre archivo, número de fila, ref a frames anteriores..
function(3, 4)
import inspect
import inspect
stack = inspect.stack()
current_frame = stack[0]
loaded_vars = current_frame.frame.f_locals
print(loaded_vars)
RecursionError, un StackOverflow "soft"
f_a(1,2)
f_b()
f_c(42)
f_d("hello!")
El stack tiene límites
Límite "logico"
Límite "logico"
Límite "físico"
El valor del límite lógico depende del sistema, pero suele estar capado a 1000
f_a(1,2)
f_b()
f_c(42)
f_d("hello!")
El stack tiene límites
El valor del límite lógico depende del sistema, pero suele estar capado a 1000
import sys
sys.getrecursionlimit() -> 1000
sys.setrecursionlimit(100_000_000)
f_a(1,2)
f_b()
f_c(42)
f_d("hello!")
RecursionError!
Límite "logico"
Límite "logico"
Límite "físico"
f_d("hello!")
Optimizando tail calls 🐍
(o...no *llora lenta pero recursivamente*)
¿Qué es una tail call?
Una llamada a una subrutina está "en la cola" si está situada en el último lugar de un procedimiento
def f():
return a()
def f2(n):
if n == 1:
return a()
else:
return b()
def f():
result = a()
print(result)
return result
def f2(n):
return a() + 1
✅
❌
¿Y qué tienen de especial?
- Los stack frames tienen que estar en el stack mientras sean necesarios.
- Si las llamadas a una función se hacen en la cola (en última posición), se puede preveer que el stack frame no se necesita más...
- ¿Se podría borrar el stack frame?
f_a(1,2)
f_b()
f_c(42)
f_d("hello!")
Tail Call Optimization
- Borrar frames no... ¡pero reutilizarlos sí!
- Así nos ahorramos el overhead de crear un stack frame nuevo por cada llamada
a()
└─b() -> c()
a()
b()
a()
└─ c()
a()
c()
a()
a() "wtf pero si yo había llamado a b()"
¿Y qué tiene de malo?
😀
"Pos que los tracebacks de las excepciones quedan un poco confusos"
Tracebacks
Traceback (most recent call last):
File "illooo.py", line 10, in <module>
c()
File "illooo.py", line 8, in c
b()
File "illooo.py", line 5, in b
a()
File "illooo.py", line 2, in a
raise Exception("Obscure exception")
Exception: Obscure exception
Traceback (most recent call last):
File "illooo.py", line 5, in <module>
a()
File "illooo.py", line 2, in a
raise Exception("Obscure exception")
Exception: Obscure exception
Con TCO
Sin TCO
Y... ¿ahora qué?
Donde esté un buen bucle...
def fib(n):
if n in {0, 1}:
return n
return fib(n-1) + fib(n-2)
def fib(n):
if n in {0, 1}:
return n
for _ in range(n):
a, b = b, a + b
return a
- En lenguajes imperativos las llamadas a subrutinas causan mucho "overhead"
- Para cualquier implementación recursiva hay una implementación equivalente en iterativa
- Si la solución iterativa es fácil de entender, en Python es preferible usarla
- Si no... 🤷♂️🤷♀️
Dosis de realidad
Python es lento
Python es lento
- Python no tiene ningún tipo de JIT
- Tradeoff flexibilidad/claridad vs performance
- Lo que es lento es hacer llamadas a funciones, no la recursividad en sí
- Python permite interoperar con código escrito en C
Aunque casi siempre...
Es "lo suficientemente rápido"
🧑🔬👩🔬
🐍
Seamos pragmátic@s
¿Alguna pregunta?
🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍🐍
¡Muchas gracias por venir!
🐍
Copy of La culebrilla que se muerde la cola
By Javier Luna Molina
Copy of La culebrilla que se muerde la cola
- 180