La puissance des générateurs

Raphaël Gomès

  • Développeur chez

  • Travaille avec Python depuis 2013

  • A mis du temps à comprendre
    les générateurs

Les fondamentaux

(en Python)

Les itérables

  • list
  • str
  • tuple
  • dict
  • ...et d'autres !

Utiliser un itérable

for item in iterable:
    do_something(item)

Utiliser un itérable

iterator = iter(iterable)
while True:
    try:
        item = next(iterator)
    except StopIteration:
        break
    do_something(item)

Les générateurs

Dis papa, ça veut dire quoi
"yield" ?

La fonction

Renvoie un itérateur de générateur

L'itérateur

Objet correspondant au flux des données

()

<generator>

Une fonction de validation

def validate(form):
    if form.password_1 != form.password_2:
        yield "password-match-error"
    if len(form.password_1) < 8:
        yield "password-too-short"
    if form.password_1 == "password123":
        raise StopIteration("please-no")

Utiliser cette fonction

errors = []
validator = validate(form=form)

for message in validator:
    errors.append(error)
errors = (
    message 
    for message in validate(form)
)
errors = (
    message 
    for message in validate(form)
)
errors = tuple(
    message 
    for message in validate(form)
)
errors = [
    message 
    for message in validate(form)
]

Générateur

Tuple

Liste

Petites surprises

"Papa, mon générateur est cassé"

Fool me once...

one_line_gen = (x for x in range(3))

for x in one_line_gen:
    print(x)

for x in one_line_gen:
    print(x)  # jamais exécuté !

Debugger quantique ?

#yolo

Comme un disque rayé

def iters(numbers, values):
    iterators = []
    for value in values:
        iterators.append(n
            for n in numbers
            if n == value)
    return iterators
iters([1, 2, 3], [1, 4, 3])
[[1], [], [3]]  # Attendu
[[3], [3], [3]]  # Reçu

Comme un disque rayé

def iters2(numbers, values):
    def build_gen(value):
        return (n
            for n in numbers
            if n == value)
    return [build_gen(value)
    for value in values]

Le pourquoi

Performance

  • range vs xrange
  • Pas de classes englobantes
  • Pas de résultats intermédiaires

Faisabilité

  • Suites infinies
  • Restrictions machine (threads, sockets)
  • Réduire difficulté cognitive

Apporter une abstraction

def follow(infile):
    # On se place à la fin du fichier
    infile.seek(0, os.SEEK_END)
    while True:
         line = infile.readline()
         if not line:
             # On attend un peu
             time.sleep(0.1)    
             continue
         yield line

for line in follow(logfile):
    print(line)
def wrapper(logfile):
    ...
    for line in follow(logfile):
        yield line

def better_wrapper(logfile):
    yield from follow(infile)

yield from

_i = iter(EXPR)
try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value
else:
    while 1:
        try:
            _s = yield _y
        except GeneratorExit as _e:
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e:
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:
            try:
                if _s is None:
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:
                _r = _e.value
                break
RESULT = _r

yield from

RESULT = yield from EXPR

vs

.send()

l'inversion du flux

Comportement de .send()

  • Envoie une valeur à un générateur
  • Doit partir d'un arrêt (d'un "yield")

TypeError: can't send non-None value to a just-started generator

def grep(pattern):
    while True:
        line = yield
        if pattern in line:
            print(line)

g = grep("Python")
next(g)

g.send("Coucou le meetup Python !")
g.send("Rien à voir ici")
g.send("Python c'est cool, non ?")
def coroutine(func):
    def start(*args,**kwargs):
        cr = func(*args,**kwargs)
        next(cr)
        return cr
    return start

Un petit décorateur bien pratique

@coroutine
def grep(pattern):
    ...

g = grep("Python")

g.send("Coucou le meetup Python !")
g.send("Rien à voir ici")
g.send("Python c'est cool, non ?")

"yield" comme entrée/sortie

@coroutine
def func():
    output = None
    while True:
        input_ = yield output
        output = compute(input_)

f = func()
print(f.send("something"))
print(f.send("else"))

Chaîner les coroutines

"C'est une révolution" - Steve Jobs, peut-être

Un peu comme un "pipe"

@coroutine
def grep(pattern, target):
    while True:
        line = yield     
        if pattern in line:
            target.send(line)
@coroutine
def logger(color):
    while True:
         line = yield
         echo(line, color)
g = grep('Python', logger('red'))
g.send("Python") 
g.send("PHP !")

Javascript

(the good parts)

Fonction génératrice

function* gen() { 
  yield 1;
  yield 2;
  yield 3;
}

const g = gen()

Équivalent "yield from"

function* func1() {
  yield 42;
}

function* func2() {
  yield* func1();
}

redux-saga

  • Middleware Redux
  • Gestion des effets de bord
  • Basée sur les générateurs

C'est qui le patron ?

A

B

C

D

E

Transaction

function* fetchUserSaga(action) {
   try {
      const user = yield call(
          Api.fetchUser,
          action.userId)
      yield put(userFetchSucceeded(user))
   } catch (e) {
      yield put(userFetchFailed(e.message))
   }
}

function* mySaga() {
  yield take("USER_FETCH_REQUESTED", 
             fetchUserSaga);
}

Les effets

  • take, takeLatest, takeEvery
  • put
  • call, fork, spawn, join
  • cancel
  • all
  • ...

En gros

(très)

  • Les générateurs sont puissants
  • Les générateurs sont cool
  • C'est pas seulement en Python !

Merci !

Made with Slides.com