Esta publicación utilizará los números de Fibonacci como una herramienta para explicar la utilidad de los generadores de Python .
Esta publicación contará con código C ++ y Python.
Los números de Fibonacci se definen como la secuencia: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
O en general:
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2
Esto se puede transferir a una función C ++ extremadamente fácil:
size_t Fib(size_t n)
{
//Fib(0) = 0
if(n == 0)
return 0;
//Fib(1) = 1
if(n == 1)
return 1;
//Fib(N) = Fib(N-2) + Fib(N-1)
return Fib(n-2) + Fib(n-1);
}
Pero si desea imprimir los primeros seis números de Fibonacci, volverá a calcular muchos de los valores con la función anterior.
Por ejemplo:, Fib(3) = Fib(2) + Fib(1)
pero Fib(2)
también recalcula Fib(1)
. Cuanto mayor sea el valor que desea calcular, peor será.
Por lo tanto, uno puede verse tentado a reescribir lo anterior haciendo un seguimiento del estado main
.
// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
int result = pp + p;
pp = p;
p = result;
return result;
}
int main(int argc, char *argv[])
{
size_t pp = 0;
size_t p = 1;
std::cout << "0 " << "1 ";
for(size_t i = 0; i <= 4; ++i)
{
size_t fibI = GetNextFib(pp, p);
std::cout << fibI << " ";
}
return 0;
}
Pero esto es muy feo y complica nuestra lógica main
. Sería mejor no tener que preocuparse por el estado en nuestra main
función.
Podríamos devolver un vector
valor y usar un iterator
para iterar sobre ese conjunto de valores, pero esto requiere mucha memoria de una vez para una gran cantidad de valores devueltos.
Volviendo a nuestro antiguo enfoque, ¿qué sucede si quisiéramos hacer otra cosa además de imprimir los números? Tendríamos que copiar y pegar todo el bloque de código main
y cambiar las declaraciones de salida a cualquier otra cosa que quisiéramos hacer. Y si copia y pega el código, entonces debería recibir un disparo. No quieres que te disparen, ¿verdad?
Para resolver estos problemas y evitar que te disparen, podemos reescribir este bloque de código usando una función de devolución de llamada. Cada vez que se encuentra un nuevo número de Fibonacci, llamamos a la función de devolución de llamada.
void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
if(max-- == 0) return;
FoundNewFibCallback(0);
if(max-- == 0) return;
FoundNewFibCallback(1);
size_t pp = 0;
size_t p = 1;
for(;;)
{
if(max-- == 0) return;
int result = pp + p;
pp = p;
p = result;
FoundNewFibCallback(result);
}
}
void foundNewFib(size_t fibI)
{
std::cout << fibI << " ";
}
int main(int argc, char *argv[])
{
GetFibNumbers(6, foundNewFib);
return 0;
}
Esto es claramente una mejora, su lógica main
no está tan abarrotada y puede hacer lo que quiera con los números de Fibonacci, simplemente defina nuevas devoluciones de llamadas.
Pero esto aún no es perfecto. ¿Qué sucede si solo desea obtener los dos primeros números de Fibonacci y luego hacer algo, luego obtener algo más y luego hacer otra cosa?
Bueno, podríamos continuar como lo hemos sido, y podríamos comenzar a agregar estado nuevamente main
, permitiendo que GetFibNumbers comience desde un punto arbitrario. Pero esto aumentará aún más nuestro código, y ya parece demasiado grande para una tarea simple como imprimir números de Fibonacci.
Podríamos implementar un modelo de productor y consumidor a través de un par de hilos. Pero esto complica aún más el código.
En cambio hablemos de generadores.
Python tiene una característica de lenguaje muy agradable que resuelve problemas como estos llamados generadores.
Un generador le permite ejecutar una función, detenerse en un punto arbitrario y luego continuar nuevamente donde lo dejó. Cada vez que devuelve un valor.
Considere el siguiente código que usa un generador:
def fib():
pp, p = 0, 1
while 1:
yield pp
pp, p = p, pp+p
g = fib()
for i in range(6):
g.next()
Lo que nos da los resultados:
0 1 1 2 3 5
La yield
declaración se usa en conjunción con generadores Python. Guarda el estado de la función y devuelve el valor final. La próxima vez que llame a la función next () en el generador, continuará donde se quedó el rendimiento.
Esto es mucho más limpio que el código de función de devolución de llamada. Tenemos un código más limpio, un código más pequeño y sin mencionar mucho más código funcional (Python permite enteros arbitrariamente grandes).
Fuente
send
datos a un generador. Una vez que haces eso, tienes una 'corutina'. Es muy simple implementar patrones como el mencionado consumidor / productor con corutinas porque no tienen necesidad deLock
s y, por lo tanto, no pueden llegar a un punto muerto. Es difícil describir las corutinas sin atacar los hilos, por lo que solo diré que las corutinas son una alternativa muy elegante a los hilos.