web-dev-qa-db-fra.com

Partagé python

J'essaie de reproduire le concept observable "partagé" des extensions réactives avec des générateurs Python.

Disons que j'ai une API qui me donne un flux infini que je peux utiliser comme ceci:

def my_generator():
    for elem in the_infinite_stream():
        yield elem

Je pourrais utiliser ce générateur plusieurs fois comme ceci:

stream1 = my_generator()
stream2 = my_generator()

Et the_infinite_stream() sera appelé deux fois (une fois pour chaque générateur).

Supposons maintenant que the_infinite_stream() est une opération coûteuse. Existe-t-il un moyen de "partager" le générateur entre plusieurs clients? Il semble que tee ferait cela, mais je dois savoir à l'avance combien de générateurs indépendants je veux.

L'idée est que dans d'autres langages (Java, Swift) en utilisant les flux réactifs (RxJava, RxSwift) "partagés", je peux facilement dupliquer le flux côté client. Je me demande comment faire ça en Python.

Remarque: j'utilise asyncio

19
JonasVautherin

Si vous avez un seul générateur, vous pouvez utiliser une file d'attente par "abonné" et acheminer les événements vers chaque abonné car le générateur principal produit des résultats.

Cela a l'avantage de permettre aux abonnés de se déplacer à leur propre rythme, et il peut être supprimé dans le code existant avec très peu de changements à la source d'origine.

Par exemple:

def my_gen():
  ...

m1 = Muxer(my_gen)
m2 = Muxer(my_gen)

consumer1(m1).start()
consumer2(m2).start()

Lorsque les éléments sont extraits du générateur principal, ils sont insérés dans des files d'attente pour chaque écouteur. Les auditeurs peuvent s'abonner à tout moment en construisant un nouveau Muxer ():

import queue
from threading import Lock
from collections import namedtuple

class Muxer():
    Entry = namedtuple('Entry', 'genref listeners, lock')

    already = {}
    top_lock = Lock()

    def __init__(self, func, restart=False):
        self.restart = restart
        self.func = func
        self.queue = queue.Queue()

        with self.top_lock:
            if func not in self.already:
                self.already[func] = self.Entry([func()], [], Lock())
            ent = self.already[func]

        self.genref = ent.genref
        self.lock = ent.lock
        self.listeners = ent.listeners

        self.listeners.append(self)

    def __iter__(self):
        return self

    def __next__(self):
        try:
            e = self.queue.get_nowait()
        except queue.Empty:
            with self.lock:
                try:
                    e = self.queue.get_nowait()
                except queue.Empty:
                    try:
                        e = next(self.genref[0])
                        for other in self.listeners:
                            if not other is self:
                                other.queue.put(e)
                    except StopIteration:
                        if self.restart:
                            self.genref[0] = self.func()
                        raise
        return e

Code source d'origine, y compris la suite de tests:

https://Gist.github.com/earonesty/cafa4626a2def6766acf5098331157b

Les tests unitaires exécutent de nombreux threads traitant simultanément les mêmes événements générés dans l'ordre. Le code préserve l'ordre, avec une serrure acquise lors de l'accès du générateur unique.

Avertissements: la version ici utilise un singleton pour l'accès d'accès, sinon il serait possible d'échapper accidentellement à son contrôle sur les générateurs contenus. Il permet également aux générateurs contenus d'être "redémarrables", ce qui était une fonctionnalité utile pour moi à l'époque. Il n'y a pas de fonction "close ()", simplement parce que je n'en avais pas besoin. Il s'agit d'un cas d'utilisation approprié pour __del__ cependant, puisque la dernière référence à un auditeur est le bon moment pour nettoyer.

0
Erik Aronesty