web-dev-qa-db-fra.com

Système d'événements en Python

Quel système d’événement pour Python utilisez-vous? Je suis déjà au courant de pydispatcher , mais je me demandais quoi d'autre pourrait être trouvé ou utilisé couramment

Je ne suis pas intéressé par les gestionnaires d'événements qui font partie de grands cadres, je préfère utiliser une petite solution simple que je peux facilement étendre.

160
Josip

Voici les différents systèmes d’événements mentionnés dans les réponses:

Le style le plus élémentaire de système d’événement est la méthode du «sac de gestionnaires», qui est une simple implémentation du modèle Observer . Fondamentalement, les méthodes de gestionnaire (callables) sont stockées dans un tableau et sont chacune appelées lorsque l'événement se déclenche.

  • zope.event montre comment cela fonctionne (voir réponse de Lennart ). Remarque: cet exemple ne prend même pas en charge les arguments du gestionnaire.
  • La "liste appelable" de/ de LongPoke montre qu'un tel système d'événements peut être implémenté de manière très minimaliste en sous-classant list.
  • EventHook de spassig (modèle d'événement de Michael Foord) est une implémentation simple.
  • La classe d'événement Valued Leons de Josip est fondamentalement la même, mais utilise un set au lieu d'un list pour stocker le sac, et implémente __call__ qui sont des ajouts raisonnables.
  • PyNotify est similaire dans le concept et fournit également des concepts supplémentaires de variables et de conditions ('variable événement changé').
  • axel est fondamentalement un sac à main avec plus de fonctionnalités liées au threading, au traitement des erreurs, ...

L'inconvénient de ces systèmes d'événements est que vous ne pouvez enregistrer les gestionnaires que sur l'objet d'événement réel (ou sur la liste des gestionnaires) . Ainsi, au moment de l'enregistrement, l'événement doit déjà exister.

C'est pourquoi le deuxième style de systèmes d'événements existe: le modèle publish-subscribe . Ici, les gestionnaires ne s'inscrivent pas sur un objet d'événement (ni sur une liste de gestionnaires), mais sur un répartiteur central. De plus, les notifiants ne parlent qu’au répartiteur. Ce qu'il faut écouter ou publier est déterminé par 'signal', qui n'est rien d'autre qu'un nom (chaîne).

  • clignotant a quelques fonctionnalités intéressantes telles que la déconnexion automatique et le filtrage en fonction de l'expéditeur.
  • PyPubSub semble à première vue assez simple.
  • PyDispatcher semble mettre l’accent sur la flexibilité en ce qui concerne les publications plusieurs à plusieurs, etc.
  • louie est un PyDispatcher remanié "fournissant une infrastructure de plug-in comprenant un support spécifique Twisted et PyQt". Il semble avoir perdu la maintenance après janvier 2016.
  • Django.dispatch est un PyDispatcher réécrit "avec une interface plus limitée, mais des performances supérieures".
  • Les signaux et les slots de Qt sont disponibles à partir de PyQt ou PySide . Ils fonctionnent comme des rappels lorsqu'ils sont utilisés dans le même thread ou comme des événements (utilisant une boucle d'événement) entre deux threads différents. Les signaux et les emplacements ont la limitation de ne fonctionner que dans des objets de classes dérivés de QObject.

Remarque: threading.Event n'est pas un "système d'événements" au sens précédent. C'est un système de synchronisation de threads où un thread attend jusqu'à ce qu'un autre thread "signale" l'objet Event.

Note: pypydispatcher , python-dispatch et le 'système de hook' de pluggy ne sont pas encore inclus ci-dessus pourraient également être intéressants.

114
florisla

Je l'ai fait de cette façon:

class Event(list):
    """Event subscription.

    A list of callable objects. Calling an instance of this will cause a
    call to each item in the list in ascending order by index.

    Example Usage:
    >>> def f(x):
    ...     print 'f(%s)' % x
    >>> def g(x):
    ...     print 'g(%s)' % x
    >>> e = Event()
    >>> e()
    >>> e.append(f)
    >>> e(123)
    f(123)
    >>> e.remove(f)
    >>> e()
    >>> e += (f, g)
    >>> e(10)
    f(10)
    g(10)
    >>> del e[0]
    >>> e(2)
    g(2)

    """
    def __call__(self, *args, **kwargs):
        for f in self:
            f(*args, **kwargs)

    def __repr__(self):
        return "Event(%s)" % list.__repr__(self)

Cependant, comme pour tout ce que j'ai vu auparavant, il n'y a pas de pydoc généré automatiquement pour cela, ni de signatures, ce qui est vraiment nul.

Nous utilisons un EventHook comme suggéré par Michael Foord dans son Event Pattern :

Ajoutez simplement EventHooks à vos cours avec:

class MyBroadcaster()
    def __init__():
        self.onChange = EventHook()

theBroadcaster = MyBroadcaster()

# add a listener to the event
theBroadcaster.onChange += myFunction

# remove listener from the event
theBroadcaster.onChange -= myFunction

# fire event
theBroadcaster.onChange.fire()

Nous ajoutons la fonctionnalité permettant de supprimer tout écouteur d'un objet de la classe Michaels et aboutissons à ceci:

class EventHook(object):

    def __init__(self):
        self.__handlers = []

    def __iadd__(self, handler):
        self.__handlers.append(handler)
        return self

    def __isub__(self, handler):
        self.__handlers.remove(handler)
        return self

    def fire(self, *args, **keywargs):
        for handler in self.__handlers:
            handler(*args, **keywargs)

    def clearObjectHandlers(self, inObject):
        for theHandler in self.__handlers:
            if theHandler.im_self == inObject:
                self -= theHandler
61
spassig

J'utilise zope.event . Ce sont les os les plus nus que vous puissiez imaginer. : -) En fait, voici le code source complet:

subscribers = []

def notify(event):
    for subscriber in subscribers:
        subscriber(event)

Notez que vous ne pouvez pas envoyer de messages entre processus, par exemple. Ce n'est pas un système de messagerie, mais simplement un système d'événements, rien de plus, rien de moins.

16
Lennart Regebro

J'ai trouvé ce petit script sur Valued Lessons . Il semble avoir le bon rapport simplicité/puissance que je recherche. Peter Thatcher est l'auteur du code suivant (aucune licence n'est mentionnée).

class Event:
    def __init__(self):
        self.handlers = set()

    def handle(self, handler):
        self.handlers.add(handler)
        return self

    def unhandle(self, handler):
        try:
            self.handlers.remove(handler)
        except:
            raise ValueError("Handler is not handling this event, so cannot unhandle it.")
        return self

    def fire(self, *args, **kargs):
        for handler in self.handlers:
            handler(*args, **kargs)

    def getHandlerCount(self):
        return len(self.handlers)

    __iadd__ = handle
    __isub__ = unhandle
    __call__ = fire
    __len__  = getHandlerCount

class MockFileWatcher:
    def __init__(self):
        self.fileChanged = Event()

    def watchFiles(self):
        source_path = "foo"
        self.fileChanged(source_path)

def log_file_change(source_path):
    print "%r changed." % (source_path,)

def log_file_change2(source_path):
    print "%r changed!" % (source_path,)

watcher              = MockFileWatcher()
watcher.fileChanged += log_file_change2
watcher.fileChanged += log_file_change
watcher.fileChanged -= log_file_change2
watcher.watchFiles()
13
Josip

Vous pouvez regarder pymitter ( pypi ). Il s’agit d’une approche basée sur un fichier unique (~ 250 loc.)

Voici un exemple de base:

from pymitter import EventEmitter

ee = EventEmitter()

# decorator usage
@ee.on("myevent")
def handler1(arg):
   print "handler1 called with", arg

# callback usage
def handler2(arg):
    print "handler2 called with", arg
ee.on("myotherevent", handler2)

# emit
ee.emit("myevent", "foo")
# -> "handler1 called with foo"

ee.emit("myotherevent", "bar")
# -> "handler2 called with bar"
8
Dalailirium

J'ai créé une classe EventManager (code à la fin). La syntaxe est la suivante:

#Create an event with no listeners assigned to it
EventManager.addEvent( eventName = [] )

#Create an event with listeners assigned to it
EventManager.addEvent( eventName = [fun1, fun2,...] )

#Create any number event with listeners assigned to them
EventManager.addEvent( eventName1 = [e1fun1, e1fun2,...], eventName2 = [e2fun1, e2fun2,...], ... )

#Add or remove listener to an existing event
EventManager.eventName += extra_fun
EventManager.eventName -= removed_fun

#Delete an event
del EventManager.eventName

#Fire the event
EventManager.eventName()

Voici un exemple:

def hello(name):
    print "Hello {}".format(name)

def greetings(name):
    print "Greetings {}".format(name)

EventManager.addEvent( salute = [greetings] )
EventManager.salute += hello

print "\nInitial salute"
EventManager.salute('Oscar')

print "\nNow remove greetings"
EventManager.salute -= greetings
EventManager.salute('Oscar')

Sortie:

Salut initial 
Salutations Oscar 
Salut Oscar 

Maintenant, supprimez les salutations 
Salut Oscar

Code EventManger:

class EventManager:

    class Event:
        def __init__(self,functions):
            if type(functions) is not list:
                raise ValueError("functions parameter has to be a list")
            self.functions = functions

        def __iadd__(self,func):
            self.functions.append(func)
            return self

        def __isub__(self,func):
            self.functions.remove(func)
            return self

        def __call__(self,*args,**kvargs):
            for func in self.functions : func(*args,**kvargs)

    @classmethod
    def addEvent(cls,**kvargs):
        """
        addEvent( event1 = [f1,f2,...], event2 = [g1,g2,...], ... )
        creates events using **kvargs to create any number of events. Each event recieves a list of functions,
        where every function in the list recieves the same parameters.

        Example:

        def hello(): print "Hello ",
        def world(): print "World"

        EventManager.addEvent( salute = [hello] )
        EventManager.salute += world

        EventManager.salute()

        Output:
        Hello World
        """
        for key in kvargs.keys():
            if type(kvargs[key]) is not list:
                raise ValueError("value has to be a list")
            else:
                kvargs[key] = cls.Event(kvargs[key])

        cls.__dict__.update(kvargs)
6
Cristian Garcia

Voici un design minimal qui devrait bien fonctionner. Ce que vous devez faire est simplement d'hériter de Observer dans une classe et d'utiliser ensuite observe(event_name, callback_fn) pour écouter un événement spécifique. Le rappel correspondant est déclenché chaque fois que cet événement spécifique est déclenché dans le code (par exemple, Event('USB connected')).

class Observer():
    _observers = []
    def __init__(self):
        self._observers.append(self)
        self._observed_events = []
    def observe(self, event_name, callback_fn):
        self._observed_events.append({'event_name' : event_name, 'callback_fn' : callback_fn})


class Event():
    def __init__(self, event_name, *callback_args):
        for observer in Observer._observers:
            for observable in observer._observed_events:
                if observable['event_name'] == event_name:
                    observable['callback_fn'](*callback_args)

Exemple:

class Room(Observer):
    def __init__(self):
        print("Room is ready.")
        Observer.__init__(self) # DON'T FORGET THIS
    def someone_arrived(self, who):
        print(who + " has arrived!")

# Observe for specific event
room = Room()
room.observe('someone arrived',  room.someone_arrived)

# Fire some events
Event('someone left',    'John')
Event('someone arrived', 'Lenard') # will output "Lenard has arrived!"
Event('someone Farted',  'Lenard')
5
Pithikos

J'ai proposé une variante de l'approche minimaliste de Longpoke qui garantit également les signatures pour les appelants et les appelants:

class EventHook(object):
    '''
    A simple implementation of the Observer-Pattern.
    The user can specify an event signature upon inizializazion,
    defined by kwargs in the form of argumentname=class (e.g. id=int).
    The arguments' types are not checked in this implementation though.
    Callables with a fitting signature can be added with += or removed with -=.
    All listeners can be notified by calling the EventHook class with fitting
    arguments.

    >>> event = EventHook(id=int, data=dict)
    >>> event += lambda id, data: print("%d %s" % (id, data))
    >>> event(id=5, data={"foo": "bar"})
    5 {'foo': 'bar'}

    >>> event = EventHook(id=int)
    >>> event += lambda wrong_name: None
    Traceback (most recent call last):
        ...
    ValueError: Listener must have these arguments: (id=int)

    >>> event = EventHook(id=int)
    >>> event += lambda id: None
    >>> event(wrong_name=0)
    Traceback (most recent call last):
        ...
    ValueError: This EventHook must be called with these arguments: (id=int)
    '''
    def __init__(self, **signature):
        self._signature = signature
        self._argnames = set(signature.keys())
        self._handlers = []

    def _kwargs_str(self):
        return ", ".join(k+"="+v.__for k, v in self._signature.items())

    def __iadd__(self, handler):
        params = inspect.signature(handler).parameters
        valid = True
        argnames = set(n for n in params.keys())
        if argnames != self._argnames:
            valid = False
        for p in params.values():
            if p.kind == p.VAR_KEYWORD:
                valid = True
                break
            if p.kind not in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY):
                valid = False
                break
        if not valid:
            raise ValueError("Listener must have these arguments: (%s)"
                             % self._kwargs_str())
        self._handlers.append(handler)
        return self

    def __isub__(self, handler):
        self._handlers.remove(handler)
        return self

    def __call__(self, *args, **kwargs):
        if args or set(kwargs.keys()) != self._argnames:
            raise ValueError("This EventHook must be called with these " +
                             "keyword arguments: (%s)" % self._kwargs_str())
        for handler in self._handlers[:]:
            handler(**kwargs)

    def __repr__(self):
        return "EventHook(%s)" % self._kwargs_str()
5
Felk

Si je code avec pyQt, j'utilise le paradigme des prises/signaux QT, de même pour Django

Si je fais des E/S asynchrones, j'utilise un module de sélection natif

Si j'utilise un analyseur SAX python, j'utilise une API d'événement fournie par SAX. On dirait que je suis victime de l'API sous-jacente :-)

Peut-être devriez-vous vous demander ce que vous attendez du cadre/du module d'événement. Ma préférence personnelle est d’utiliser le paradigme Socket/Signal de QT. plus d'informations à ce sujet peuvent être trouvées ici

3
SashaN

Voici un autre module à prendre en compte. Cela semble un choix viable pour des applications plus exigeantes.

Py-notify est un paquet Python fournissant des outils pour la mise en œuvre Modèle de programmation d'observateur. Celles-ci les outils incluent les signaux, les conditions et variables.

Les signaux sont des listes de gestionnaires qui sont appelé lorsque le signal est émis . Les conditions sont fondamentalement booléennes variables couplées avec un signal que est émis lorsque l'état de condition changements. Ils peuvent être combinés avec opérateurs logiques standard (pas, et, , etc.) en conditions composées . Les variables, contrairement aux conditions, peuvent contenir n'importe quel objet en Python, pas seulement des booléens, mais ils ne peuvent pas être combinés.

2
Josip

Si vous souhaitez effectuer des tâches plus complexes telles que la fusion d'événements ou la réessai, vous pouvez utiliser le modèle Observable et une bibliothèque mature qui l'implémente. https://github.com/ReactiveX/RxPY . Les observables sont très courants en Javascript et en Java et très pratiques à utiliser pour certaines tâches asynchrones. 

from rx import Observable, Observer


def Push_five_strings(observer):
        observer.on_next("Alpha")
        observer.on_next("Beta")
        observer.on_next("Gamma")
        observer.on_next("Delta")
        observer.on_next("Epsilon")
        observer.on_completed()


class PrintObserver(Observer):

    def on_next(self, value):
        print("Received {0}".format(value))

    def on_completed(self):
        print("Done!")

    def on_error(self, error):
        print("Error Occurred: {0}".format(error))

source = Observable.create(Push_five_strings)

source.subscribe(PrintObserver())

SORTIE:

Received Alpha
Received Beta
Received Gamma
Received Delta
Received Epsilon
Done!
0
David Dehghan

Vous pouvez essayer buslane module. 

Cette bibliothèque facilite la mise en œuvre d'un système basé sur un message. Il prend en charge les commandes (gestionnaire unique) et les événements (0 ou plusieurs gestionnaires). Buslane utilise des annotations de type Python pour enregistrer correctement le gestionnaire.

Exemple simple:

from dataclasses import dataclass

from buslane.commands import Command, CommandHandler, CommandBus


@dataclass(frozen=True)
class RegisterUserCommand(Command):
    email: str
    password: str


class RegisterUserCommandHandler(CommandHandler[RegisterUserCommand]):

    def handle(self, command: RegisterUserCommand) -> None:
        assert command == RegisterUserCommand(
            email='[email protected]',
            password='secret',
        )


command_bus = CommandBus()
command_bus.register(handler=RegisterUserCommandHandler())
command_bus.execute(command=RegisterUserCommand(
    email='[email protected]',
    password='secret',
))

Pour installer buslane, utilisez simplement pip:

$ pip install buslane
0
Konrad Hałas

Si vous avez besoin d’un bus event qui fonctionne au-delà des limites du processus ou du réseau, vous pouvez essayer PyMQ . Il supporte actuellement pub/sub, les files de messages et le RPC synchrone. La version par défaut fonctionne sur un serveur Redis, vous avez donc besoin d'un serveur Redis en cours d'exécution. Il existe également un back-end en mémoire pour les tests. Vous pouvez également écrire votre propre backend.

import pymq

# common code
class MyEvent:
    pass

# subscribe code
@pymq.subscriber
def on_event(event: MyEvent):
    print('event received')

# publisher code
pymq.publish(MyEvent())

# you can also customize channels
pymq.subscribe(on_event, channel='my_channel')
pymq.publish(MyEvent(), channel='my_channel')

Pour initialiser le système:

from pymq.provider.redis import RedisConfig

# starts a new thread with a Redis event loop
pymq.init(RedisConfig())

# main application control loop

pymq.shutdown()

Disclaimer: Je suis l'auteur de cette bibliothèque

0
thrau

Il y a quelque temps, j'ai écrit une bibliothèque qui pourrait vous être utile. Il vous permet d’avoir des écouteurs locaux et globaux, de multiples façons de les enregistrer, une priorité d’exécution, etc.

from pyeventdispatcher import register

register("foo.bar", lambda event: print("second"))
register("foo.bar", lambda event: print("first "), -100)

dispatch(Event("foo.bar", {"id": 1}))
# first second

Regardez pyeventdispatcher

0
Daniel Ancuta