Fonctions de rappel en ligne en Python

Vous écrivez du code qui utilise des fonctions de rappel, mais vous vous inquiétez de la prolifération de petites fonctions et de l’étourdissant flux de contrôle. Vous aimeriez trouver un moyen de faire en sorte que le code ressemble davantage à une séquence normale d’étapes procédurales.

Les fonctions de rappel peuvent être intégrées dans une fonction à l’aide de générateurs et de coroutines. Pour illustrer cela, supposons que vous ayez une fonction qui exécute un travail et invoque un appel callback comme suit:

def apply_async(func, args, *, callback):
    # Calculer le résultat
    resultat = func(*args)

    # Invoquer le rappel avec le résultat
    callback(resultat)

Jetez maintenant un coup d’oeil au code de support suivant, qui implique une classe Async et un décorateur inlined_async:

from queue import Queue
from fonctiontools import wraps

class Async:
    def __init__(self, fonction, args):
        self.fonction = fonction
        self.args = args

def inlined_async(fonction):
    @wraps(fonction)
    def wrapper(*args):
        f = fonction(*args)
        file_resultat = Queue()
        file_resultat.put(None)
        while True:
            result = file_resultat.get()
            try:
                a = f.send(result)
                apply_async(a.fonction, a.args, callback=file_resultat.put)
            except StopIteration:
                break
    return wrapper

Ces deux fragments de code vous permettront d’aligner les étapes de callback à l’aide d’instructions yield. Par exemple:

def ajouter(x, y):
    return x + y

@inlined_async
def test():
    r = yield Async(ajouter, (2, 3))
    print(r)
    r = yield Async(ajouter, ('hello', 'world'))
    print(r)
    for n in range(10):
        r = yield Async(ajouter, (n, n))
        print(r)
    print('Au revoir')

Si vous appelez test(), vous obtiendrez une sortie comme celle-ci:

5
helloworld
0
2
4
6
8
10
12
14
16
18
Au revoir

Hormis le décorateur spécial et l’utilisation de yield, vous remarquerez qu’aucune fonction de rappel n’apparaît nulle part (sauf dans les coulisses).

Cet article va vraiment tester vos connaissances sur les fonctions de rappel, les générateurs et le contrôle du débit.

Tout d’abord, dans le code impliquant des rappels, le point essentiel est que le calcul actuel sera suspendu et reprendra à un moment ultérieur (par exemple, de façon asynchrone). Lorsque le calcul reprend, le rappel sera exécuté pour continuer le traitement.

La fonction apply_async() illustre les parties essentielles de l’exécution du callback, bien qu’en réalité cela puisse être beaucoup plus compliqué (impliquant des threads, processus, gestionnaires d’événements, etc.).

L’idée qu’un calcul va se suspendre et reprendre s’applique naturellement au modèle d’exécution d’une fonction de générateur. Spécifiquement, l’opération yield fait qu’une fonction de générateur émet une valeur et se suspend.

Les appels sous-jacents à la méthode __next__() ou send() d’un générateur le feront redémarrer.

Dans cette logique, le coeur de cet article se trouve dans la fonction décorateur inline_async(). L’idée clé est que le décorateur va faire passer la fonction de générateur à travers tous ses instructions yield, une à la fois.

Pour ce faire, une file de résultats est créée et initialement renseignée avec la valeur None. Une boucle est alors lancée dans laquelle un résultat est extrait de la file d’attente et envoyé dans le générateur.

Ceci passe au yield suivant, à partir duquel une instance d’Async est reçue. La boucle examine alors la fonction et les arguments, et lance le calcul asynchrone apply_async().

Cependant, la partie la plus rusée de ce calcul est qu’au lieu d’utiliser une fonction de rappel normale, le rappel est mis à la méthode put().

Pour l’instant, on ne sait pas exactement ce qui se passe. La boucle principale retourne immédiatement en haut et exécute simplement une opération get() sur la file d’attente. Si des données sont présentes, ce doit être le résultat placé là par le put() callback.

S’il n’y a rien, l’opération se bloque, en attendant qu’un résultat arrive à un moment ultérieur. Comment cela peut se produire dépend de l’implémentation précise de la fonction apply_async().

Si vous doutez que quelque chose d’aussi fou puisse fonctionner, vous pouvez l’essayer avec la bibliothèque multiprocessing et faire exécuter des opérations asynchrones dans des processus séparés:

if __name__ == '__main__':
    import multiprocessing
    pool = multiprocessing.Pool()
    apply_async = pool.apply_async

   # Exécutez la fonction de test
    test()

En effet, vous constaterez qu’il fonctionne, mais il faudra peut-être plus de café pour démêler le flux de contrôle.

Cacher le flux de contrôle délicat derrière les fonctions de générateur se trouve ailleurs dans la bibliothèque standard et dans les paquets tiers.

Par exemple, le décorateur @contextmanager dans le module contextlib exécute une astuce similaire qui colle l’entrée et la sortie d’un gestionnaire de contexte ensemble à travers une déclaration yield.

Le populaire package Twisted comporte des rappels en ligne qui sont également similaires.

LAISSER UN COMMENTAIRE

Please enter your comment!
Please enter your name here