:PROPERTIES:
:ID:       67410dad-d959-4029-b281-9bf1c9e69ede
:mtime:    20220729213822
:ctime:    20220529123824
:END:
#+title: Generator

* Introduction
Un /generator/ est :
 * Une fonction retournant un /iterable/,
 * Un /iterable/ est un /object/ implémentant les méthodes ~__iter__~ et ~__next__~ et génère une erreur ~StopIteration~ lorsqu'il n'y a plus de valeur à retourner,
 * Utilise le /statement/ ~yield~ au lieu de ~return~.

Le statement ~return~ termine complètement une fonction alors que ~yield~ la suspend (sauvegarde de son contexte afin
de continuer là où elle en était lors de son prochain appel).

Différences entre une /function/ et un /generator/ :
 * Présence d'au moins un ~yield~ dans un ~generator~,
 * Le /generator/ retourne un /iterator/ (les méthodes ~__iter__~ et ~__next__~ sont implémentées automatiquement),
 * Sauvegarde du contexte d'un /generator/,
 * L'erreur ~StopIteration~ est générée automatiquement lors qu'un /generator/ termine.

Les /generator/ sont appropriés quand il est nécessaire de produire un flux de données infini.

#+BEGIN_SRC python :results output
def simple_generator():
    for num in range(1, 4):
        print(num)
        yield num

gen = simple_generator()
next(gen)
next(gen)
next(gen)
try:
    next(gen)
except StopIteration as __:
    print('StopIteration raised')

for num in simple_generator():
    pass
#+END_SRC
#+RESULTS:
: 1
: 2
: 3
: StopIteration raised
: 1
: 2
: 3

Un /generator/ peut être créé soit en déclarant une fonction (cf. exemple précédent) ou directement via une /generator
expression/ :

#+BEGIN_SRC python :results output
list_numbers = [10, 18, 13, 23]
list_square = [x**2 for x in list_numbers]

generator = (x**2 for x in list_numbers)

print(list_square)
print(generator)
#+END_SRC
#+RESULTS:
: [100, 324, 169, 529]
: <generator object <genexpr> at 0x7fae7b90bb30>

* Différences avec /list/
La principale différence entre les /list comprehension/ et les /generator expression/ consiste dans le fait que
l'ensemble de la /list/ est créée par la /list compréhension/ alors que seul un /generator/ est produit par la
/generator expression/. Cela apporte quelques avantages :
 * Chaque résultat du /generator/ n'est produit que suite à l'appel de ~__next__~ (/lazy execution/),
 * L'espace mémoire nécessaire s'en retrouve réduit,

Il peut s'avérer plus efficace d'utiliser un /generator/ lorsque l'ensemble des résultats ne sera pas utilisé à
l'inverse pour les /list comprehension/.

* Enchainement de /generator/
Il est possible d'enchainer plusieurs /generator/ :
#+BEGIN_SRC python :results output
def fibonacci_numbers(numbers):
    x, y = 0, 1
    for _ in range(numbers):
        x, y = y, x + y
        yield x

def square(numbers):
    for num in numbers:
        yield num**2

print(f'{sum(square(fibonacci_numbers(30))) = }')
#+END_SRC

#+RESULTS:
: sum(square(fibonacci_numbers(30))) = 1120149658760

* Renvoyer une valeur à un /generator/
#+BEGIN_SRC python :results output
def dummy_generator():
    i = 0
    for i in range(10):
        print(f'from generator {yield i}')

generator = dummy_generator()
received = generator.send(None)
print(f'from here {received}')
try:
    while True:
        received = generator.send(received + 100)
        print(f'from here {received}')
except StopIteration:
    pass
#+END_SRC

#+RESULTS:
: from here 0
: from generator 100
: from here 1
: from generator 101
: from here 2
: from generator 102
: from here 3
: from generator 103
: from here 4
: from generator 104
: from here 5
: from generator 105
: from here 6
: from generator 106
: from here 7
: from generator 107
: from here 8
: from generator 108
: from here 9
: from generator 109

* Références
 * [[https://blog.devgenius.io/what-is-generator-in-python-and-how-does-it-work-e6e0588785c3][What Is Generator in Python and How Does It Work ? - Medium]]
