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
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.