Ce message utilisera les nombres de Fibonacci comme outil pour construire pour expliquer l'utilité des générateurs Python .
Cet article présentera à la fois du code C ++ et Python.
Les nombres de Fibonacci sont définis comme la séquence: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....
Ou en général:
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2
Cela peut être transféré dans une fonction C ++ extrêmement facilement:
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);
}
Mais si vous souhaitez imprimer les six premiers nombres de Fibonacci, vous recalculerez beaucoup de valeurs avec la fonction ci-dessus.
Par exemple :, Fib(3) = Fib(2) + Fib(1)
mais Fib(2)
recalcule également Fib(1)
. Plus la valeur que vous souhaitez calculer est élevée, plus vous vous sentirez mal.
On peut donc être tenté de réécrire ce qui précède en gardant une trace de l'état dans 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;
}
Mais c'est très moche, et cela complique notre logique main
. Il vaudrait mieux ne pas avoir à se soucier de l'état de notre main
fonction.
Nous pourrions renvoyer un vector
de valeurs et utiliser un iterator
pour itérer sur cet ensemble de valeurs, mais cela nécessite beaucoup de mémoire en même temps pour un grand nombre de valeurs de retour.
Donc, revenons à notre ancienne approche, que se passe-t-il si nous voulons faire autre chose que d'imprimer les chiffres? Nous devions copier et coller tout le bloc de code main
et changer les instructions de sortie en tout ce que nous voulions faire. Et si vous copiez et collez du code, vous devriez être abattu. Vous ne voulez pas vous faire tirer dessus, n'est-ce pas?
Pour résoudre ces problèmes et éviter de se faire tirer dessus, nous pouvons réécrire ce bloc de code à l'aide d'une fonction de rappel. Chaque fois qu'un nouveau numéro de Fibonacci est rencontré, nous appelons la fonction de rappel.
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;
}
Il s'agit clairement d'une amélioration, votre logique main
n'est pas aussi encombrée et vous pouvez faire tout ce que vous voulez avec les numéros de Fibonacci, simplement définir de nouveaux rappels.
Mais ce n'est pas encore parfait. Et si vous vouliez obtenir seulement les deux premiers numéros de Fibonacci, puis faire quelque chose, puis en obtenir plus, puis faire autre chose?
Eh bien, nous pourrions continuer comme nous l'avons été, et nous pourrions recommencer à ajouter l'état main
, permettant à GetFibNumbers de démarrer à partir d'un point arbitraire. Mais cela va encore alourdir notre code, et il semble déjà trop gros pour une tâche simple comme l'impression de nombres de Fibonacci.
Nous pourrions mettre en œuvre un modèle de producteur et de consommateur via quelques fils. Mais cela complique encore plus le code.
Parlons plutôt des générateurs.
Python a une fonctionnalité de langage très agréable qui résout des problèmes comme ceux-ci appelés générateurs.
Un générateur vous permet d'exécuter une fonction, de vous arrêter à un point arbitraire, puis de reprendre là où vous vous étiez arrêté. Renvoyant à chaque fois une valeur.
Considérez le code suivant qui utilise un générateur:
def fib():
pp, p = 0, 1
while 1:
yield pp
pp, p = p, pp+p
g = fib()
for i in range(6):
g.next()
Ce qui nous donne les résultats:
0 1 1 2 3 5
L' yield
instruction est utilisée en conjonction avec des générateurs Python. Il enregistre l'état de la fonction et renvoie la valeur yeilded. La prochaine fois que vous appellerez la fonction next () sur le générateur, elle continuera là où le rendement s'est arrêté.
C'est de loin plus propre que le code de fonction de rappel. Nous avons un code plus propre, un code plus petit, et sans parler de code beaucoup plus fonctionnel (Python autorise des entiers arbitrairement grands).
La source
send
transmettre des données à un générateur. Une fois que vous avez fait cela, vous avez une «coroutine». Il est très simple d'implémenter des modèles comme le consommateur / producteur mentionné avec des coroutines car ils n'ont pas besoin deLock
s et ne peuvent donc pas bloquer. Il est difficile de décrire les coroutines sans dénigrer les threads, donc je dirai simplement que les coroutines sont une alternative très élégante au threading.