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

  • 108