Este post usará os números de Fibonacci como uma ferramenta para explicar a utilidade dos geradores Python .
Esta postagem apresentará códigos C ++ e Python.
Os números de Fibonacci são definidos como a sequência: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....
Ou em geral:
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2
Isso pode ser transferido para uma função C ++ com extrema facilidade:
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);
}
Mas se você quiser imprimir os seis primeiros números de Fibonacci, recalculará muitos valores com a função acima.
Por exemplo :, Fib(3) = Fib(2) + Fib(1)
mas Fib(2)
também recalcula Fib(1)
. Quanto maior o valor que você deseja calcular, pior será.
Portanto, pode-se tentar reescrever o que foi dito acima, acompanhando o estado em 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;
}
Mas isso é muito feio e complica nossa lógica main
. Seria melhor não ter que se preocupar com o estado em nossamain
função.
Poderíamos retornar um vector
de valores e usar um iterator
para iterar sobre esse conjunto de valores, mas isso requer muita memória ao mesmo tempo para um grande número de valores de retorno.
Voltando à nossa antiga abordagem, o que acontece se quisermos fazer outra coisa além de imprimir os números? Teríamos que copiar e colar todo o bloco de código main
e alterar as instruções de saída para o que mais desejássemos fazer. E se você copiar e colar o código, deverá levar um tiro. Você não quer levar um tiro, não é?
Para resolver esses problemas e evitar ser atingido, podemos reescrever esse bloco de código usando uma função de retorno de chamada. Sempre que um novo número de Fibonacci é encontrado, chamaríamos a função de retorno de chamada.
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;
}
Isso é claramente uma melhoria, sua lógica main
não é tão confusa e você pode fazer o que quiser com os números de Fibonacci, basta definir novos retornos de chamada.
Mas isso ainda não é perfeito. E se você quisesse obter apenas os dois primeiros números de Fibonacci e, em seguida, fazer alguma coisa, obter mais um pouco e fazer outra coisa?
Bem, poderíamos continuar como estivemos, e poderíamos começar a adicionar novamente o estado main
, permitindo que GetFibNumbers iniciasse de um ponto arbitrário. Mas isso vai inchar ainda mais o nosso código, e já parece grande demais para uma tarefa simples como imprimir números de Fibonacci.
Poderíamos implementar um modelo de produtor e consumidor por meio de alguns threads. Mas isso complica ainda mais o código.
Em vez disso, vamos falar sobre geradores.
O Python possui um recurso de linguagem muito agradável que resolve problemas como esses chamados geradores.
Um gerador permite que você execute uma função, pare em um ponto arbitrário e continue novamente de onde parou. Sempre que retornar um valor.
Considere o seguinte código que usa um gerador:
def fib():
pp, p = 0, 1
while 1:
yield pp
pp, p = p, pp+p
g = fib()
for i in range(6):
g.next()
O que nos dá os resultados:
0 1 1 2 3 5
A yield
declaração é usada em conjunto com geradores Python. Ele salva o estado da função e retorna o valor gerado. Na próxima vez que você chamar a função next () no gerador, ela continuará de onde o rendimento parou.
Isso é muito mais limpo que o código da função de retorno de chamada. Temos código mais limpo, menor e sem mencionar muito mais código funcional (o Python permite números inteiros arbitrariamente grandes).
Fonte
send
dados para um gerador. Depois de fazer isso, você tem uma 'corotina'. É muito simples implementar padrões como o Consumidor / Produtor mencionado com corotinas, porque eles não precisam de se,Lock
portanto, não podem entrar em conflito. É difícil descrever corotinas sem contornar threads; portanto, direi apenas que as corotinas são uma alternativa muito elegante ao encadeamento.