Sistema di eventi in Python


197

Quale sistema di eventi per Python usi? Sono già a conoscenza di pydispatcher , ma mi chiedevo cos'altro si può trovare o è comunemente usato?

Non mi interessano i gestori di eventi che fanno parte di grandi framework, preferirei utilizzare una piccola soluzione bare-bones che posso facilmente estendere.

Risposte:


182

Pacchetti PyPI

A partire da giugno 2020, questi sono i pacchetti relativi agli eventi disponibili su PyPI, ordinati per data di rilascio più recente.

C'è più

Ci sono molte librerie tra cui scegliere, usando una terminologia molto diversa (eventi, segnali, gestori, invio di metodi, hook, ...).

Sto cercando di tenere una panoramica dei pacchetti sopra, oltre alle tecniche menzionate nelle risposte qui.

Innanzitutto, un po 'di terminologia ...

Modello di osservatore

Lo stile di base del sistema di eventi è la "borsa dei metodi di gestione", che è una semplice implementazione del modello Observer .

Fondamentalmente, i metodi del gestore (callable) sono memorizzati in un array e vengono chiamati ciascuno quando l'evento "si attiva".

Publish-Subscribe

Lo svantaggio dei sistemi di eventi Observer è che è possibile registrare solo i gestori sull'oggetto Event (o sull'elenco dei gestori) effettivo. Quindi al momento della registrazione l'evento deve già esistere.

Ecco perché esiste il secondo stile di sistemi di eventi: il modello di pubblicazione-sottoscrizione . Qui, i gestori non si registrano su un oggetto evento (o elenco di gestori), ma su un dispatcher centrale. Anche i notificanti parlano solo al dispatcher. Cosa ascoltare o cosa pubblicare è determinato da 'signal', che non è altro che un nome (stringa).

Modello mediatore

Potrebbe anche essere interessante: il modello del mediatore .

ganci

Un sistema "hook" viene generalmente utilizzato nel contesto dei plug-in dell'applicazione. L'applicazione contiene punti di integrazione fissi (hook) e ciascun plug-in può connettersi a tale hook ed eseguire determinate azioni.

Altri "eventi"

Nota: threading.Event non è un "sistema di eventi" nel senso precedente. È un sistema di sincronizzazione dei thread in cui un thread attende fino a quando un altro thread "segnala" l'oggetto Event.

Le librerie di messaggistica di rete usano spesso anche il termine "eventi"; a volte questi sono simili nel concetto; a volte no. Ovviamente possono attraversare i confini di thread, processi e computer. Vedi ad esempio pyzmq , pymq , Twisted , Tornado , gevent , eventlet .

Riferimenti deboli

In Python, tenere un riferimento a un metodo o oggetto garantisce che non venga eliminato dal Garbage Collector. Questo può essere desiderabile, ma può anche portare a perdite di memoria: i gestori collegati non vengono mai ripuliti.

Alcuni sistemi di eventi utilizzano riferimenti deboli anziché normali per risolvere questo problema.

Alcune parole sulle varie biblioteche

Sistemi di eventi in stile osservatore:

  • zope.event mostra le ossa nude di come funziona (vedi la risposta di Lennart ). Nota: questo esempio non supporta nemmeno gli argomenti del gestore.
  • L'implementazione dell '"elenco richiamabile" di LongPoke mostra che un tale sistema di eventi può essere implementato in modo molto minimalista tramite la sottoclasse list.
  • La variazione di Felk EventHook assicura anche le firme di calle e chiamanti.
  • EventHook di Spassig (Event Pattern di Michael Foord) è un'implementazione semplice.
  • La classe Event di Lezioni valutate di Josip è sostanzialmente la stessa, ma usa un al setposto di a listper conservare la borsa e strumenti __call__che sono entrambi aggiunte ragionevoli.
  • PyNotify è simile nel concetto e fornisce anche concetti aggiuntivi di variabili e condizioni ('evento variabile modificata'). La home page non è funzionale.
  • axel è fondamentalmente un bag-of-handlers con più funzioni relative a threading, gestione degli errori, ...
  • python-dispatch richiede che le classi di origine pari derivino pydispatch.Dispatcher.
  • buslane è basato su classi, supporta gestori singoli o multipli e facilita suggerimenti di tipo estesi.
  • L' osservatore / evento di Pithikos è un design leggero.

Pubblica / iscrivi le librerie:

  • il lampeggiante ha alcune funzioni eleganti come la disconnessione automatica e il filtro basati sul mittente.
  • PyPubSub è un pacchetto stabile e promette "funzionalità avanzate che facilitano il debug e il mantenimento di argomenti e messaggi".
  • pymitter è una porta Python di Node.js EventEmitter2 e offre spazi dei nomi, caratteri jolly e TTL.
  • PyDispatcher sembra enfatizzare la flessibilità per quanto riguarda la pubblicazione da molti a molti, ecc. Supporta riferimenti deboli.
  • louie è un PyDispatcher rielaborato e dovrebbe funzionare "in un'ampia varietà di contesti".
  • pypydispatcher è basato su (hai indovinato ...) PyDispatcher e funziona anche in PyPy.
  • django.dispatch è un PyDispatcher riscritto "con un'interfaccia più limitata, ma prestazioni più elevate".
  • pyeventdispatcher è basato sul dispatcher di eventi del framework Symfony di PHP.
  • dispatcher è stato estratto da django.dispatch ma sta diventando piuttosto vecchio.
  • EventManger di Cristian Garcia è un'implementazione davvero breve.

Altri:

  • pluggy contiene un sistema di hook che viene utilizzato dai pytestplugin.
  • RxPy3 implementa il modello osservabile e consente di unire eventi, riprovare ecc.
  • I segnali e gli slot di Qt sono disponibili da PyQt o PySide2 . Funzionano come callback quando vengono utilizzati nello stesso thread o come eventi (utilizzando un loop di eventi) tra due thread diversi. Segnali e slot hanno la limitazione che funzionano solo in oggetti di classi da cui derivano QObject.

2
C'è anche louie, che si basa su PyDispatcher: pypi.python.org/pypi/Louie/1.1
the979kid

@ the979kid louie sembra essere mal gestito, la pagina pypi si collega a 404s su GitHub: 11craft.github.io/louie ; github.com/gldnspud/louie . Dovrebbe essere github.com/11craft/louie .
Florisla,

1
Gli ascoltatori di eventi deboli sono un bisogno comune. Altrimenti l'utilizzo nel mondo reale diventa arduo. Una nota che le soluzioni supportano che potrebbero essere utili.
kxr,

Pypubsub 4 è molti-a-molti e ha potenti strumenti di debug per i messaggi e diversi modi per limitare i payload dei messaggi in modo da sapere prima quando hai inviato dati non validi o dati mancanti. PyPubSub 4 supporta Python 3 (e PyPubSub 3.x supporta Python 2).
Oliver

Di recente ho pubblicato una libreria chiamata pymq github.com/thrau/pymq che potrebbe essere adatta a questo elenco.
gio

100

L'ho fatto in questo modo:

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)

Tuttavia, come con qualsiasi altra cosa che ho visto, non esiste un pydoc generato automaticamente per questo e nessuna firma, il che fa davvero schifo.


3
Trovo questo stile piuttosto intrigante. Sono dolcemente ossa nude. Mi piace il fatto che consenta di manipolare eventi e i loro abbonati come operazioni autonome. Vedrò come funziona in un vero progetto.
Rudy Lattae,

2
Bellissimo stile minimalista! super!
akaRem

2
Non posso votare abbastanza, questo è davvero semplice e facile.

2
grande favore, qualcuno potrebbe spiegarlo come se avessi 10 anni? Questa classe viene ereditata dalla classe principale? Non vedo un init così super () non verrebbe usato. Non sta facendo clic per me per qualche motivo.
omgimdrunk,

1
@omgimdrunk Un semplice gestore di eventi attiva una o più funzioni richiamabili ogni volta che viene generato un evento. Una classe per "gestirla" per te richiederebbe almeno i seguenti metodi: add & fire. All'interno di quella classe dovresti mantenere un elenco di gestori da eseguire. Mettiamolo nella variabile di istanza _bag_of_handlersche è un elenco. Il metodo di aggiunta della classe sarebbe semplicemente self._bag_of_handlers.append(some_callable). Il metodo di fuoco della classe passava attraverso `_bag_of_handlers` passando gli arg e i kwarg forniti ai gestori ed eseguendo ciascuno in sequenza.
Gabe Spradlin,

69

Usiamo un EventHook come suggerito da Michael Foord nel suo modello di evento :

Aggiungi EventHooks alle tue lezioni con:

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()

Aggiungiamo la funzionalità per rimuovere tutti i listener da un oggetto alla classe Michaels e alla fine siamo arrivati ​​a questo:

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

Uno svantaggio dell'utilizzo di questo è che è necessario prima aggiungere un evento prima di registrarsi come abbonato. Se solo gli editori aggiungono i loro eventi (non un must, solo una buona pratica), allora devi inizializzare gli editori prima degli abbonati, il che è un problema in grandi progetti
Jonathan

6
l'ultimo metodo viene corretto perché i gestori self .__ vengono modificati durante le iterazioni. Correzione: `self .__ handlers = [h for h in self .__ handlers if h.im_self! = Obj]`
Simon Bergot

1
@Simon ha ragione, ma introduce un bug perché possiamo avere funzioni non associate nei gestori self .__. Correzione:self.__handlers = [h for h in self._handlers if getattr(h, 'im_self', False) != obj]
Eric Marcos,

20

Uso zope.event . Sono le ossa più nude che puoi immaginare. :-) In effetti, ecco il codice sorgente completo:

subscribers = []

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

Si noti che non è possibile inviare messaggi tra processi, ad esempio. Non è un sistema di messaggistica, solo un sistema di eventi, niente di più, niente di meno.


17
pypi.python.org/pypi/zope.event ... per salvare un po 'di banda al povero Google ;-)
Boldewyn,

Mi piacerebbe comunque essere in grado di inviare messaggi. Userei il sistema di eventi nell'applicazione costruita su Tkinter. Non sto usando il suo sistema di eventi perché non supporta i messaggi.
Josip,

Puoi inviare quello che vuoi con zope.event. Ma il mio punto è che non è un sistema di messaggistica adeguato, poiché non è possibile inviare eventi / messaggi ad altri processi o altri computer. Probabilmente dovresti essere un ma più specifico con le tue esigenze.
Lennart Regebro,

15

Ho trovato questo piccolo copione su Lezioni valorizzate . Sembra avere il giusto rapporto semplicità / potenza che sto cercando. Peter Thatcher è l'autore del seguente codice (non viene menzionata alcuna licenza).

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()

1
L'uso di un set () anziché di un elenco è utile per evitare che i gestori vengano registrati due volte. Una conseguenza è che i gestori non vengono chiamati nell'ordine in cui sono stati registrati. Non necessariamente una cosa negativa ...
Florisla,

1
@florisla potrebbe cambiare per OrderedSet, se desiderato.
Robino,

9

Ecco un design minimale che dovrebbe funzionare bene. Quello che devi fare è semplicemente ereditare Observerin una classe e successivamente utilizzarlo observe(event_name, callback_fn)per ascoltare un evento specifico. Ogni volta che quell'evento specifico viene generato in qualsiasi punto del codice (ad es. Event('USB connected')), Verrà generato il callback corrispondente.

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)

Esempio:

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')

Mi piace il tuo design, è minimalista e facile da capire. e sarebbe leggero non dover importare alcuni moduli.
Atreyagaurav,

8

Ho creato una EventManagerclasse (codice alla fine). La sintassi è la seguente:

#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()

Ecco un esempio:

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')

Produzione:

Saluto iniziale
Saluti Oscar
Ciao Oscar

Ora rimuovi i saluti
Ciao Oscar

Codice 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)

8

Potresti dare un'occhiata a pymitter ( pypi ). È un piccolo approccio a file singolo (~ 250 loc) "che fornisce spazi dei nomi, caratteri jolly e TTL".

Ecco un esempio di 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"

6

Ho fatto una variazione dell'approccio minimalista di Longpoke che assicura anche le firme sia per i callees che per i chiamanti:

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.__name__ 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()

3

Se faccio codice in pyQt, utilizzo il paradigma di socket / segnali QT, lo stesso vale per django

Se eseguo operazioni di I / O asincrono, utilizzare il modulo di selezione nativo

Se utilizzo un parser Python SAX, utilizzo l'API dell'evento fornita da SAX. Quindi sembra che io sia vittima dell'API sottostante :-)

Forse dovresti chiederti cosa ti aspetti dal framework / modulo dell'evento. La mia preferenza personale è usare il paradigma Socket / Signal di QT. maggiori informazioni al riguardo sono disponibili qui


2

Ecco un altro modulo da considerare. Sembra una scelta praticabile per applicazioni più impegnative.

Py-notification è un pacchetto Python che fornisce strumenti per l'implementazione del modello di programmazione Observer. Questi strumenti includono segnali, condizioni e variabili.

I segnali sono elenchi di gestori che vengono chiamati quando viene emesso il segnale. Le condizioni sono sostanzialmente variabili booleane accoppiate a un segnale che viene emesso quando cambia lo stato della condizione. Possono essere combinati usando operatori logici standard (no, e, ecc.) In condizioni composte. Le variabili, a differenza delle condizioni, possono contenere qualsiasi oggetto Python, non solo booleani, ma non possono essere combinati.


1
La home page è fuori servizio per questa, forse non è più supportata?
David Parks,

1

Se vuoi fare cose più complicate come unire eventi o riprovare puoi usare il modello Osservabile e una libreria matura che lo implementa. https://github.com/ReactiveX/RxPY . Gli osservabili sono molto comuni in Javascript e Java e molto convenienti da usare per alcune attività asincrone.

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())

USCITA :

Received Alpha
Received Beta
Received Gamma
Received Delta
Received Epsilon
Done!

1

Se hai bisogno di un eventbus che funzioni oltre i confini del processo o della rete, puoi provare PyMQ . Attualmente supporta pub / sub, code di messaggi e RPC sincrono. La versione predefinita funziona su un backend Redis, quindi è necessario un server Redis in esecuzione. C'è anche un backend in memoria per i test. Puoi anche scrivere il tuo 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')

Per inizializzare il sistema:

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: sono l'autore di questa biblioteca


0

Puoi provare il buslanemodulo.

Questa libreria semplifica l'implementazione del sistema basato sui messaggi. Supporta i comandi (gestore singolo) e l'approccio eventi (0 o gestori multipli). Buslane utilizza le annotazioni di tipo Python per registrare correttamente il gestore.

Esempio semplice:

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='john@lennon.com',
            password='secret',
        )


command_bus = CommandBus()
command_bus.register(handler=RegisterUserCommandHandler())
command_bus.execute(command=RegisterUserCommand(
    email='john@lennon.com',
    password='secret',
))

Per installare buslane, basta usare pip:

$ pip install buslane

0

Qualche tempo fa ho scritto una libreria che potrebbe esserti utile. Ti consente di avere ascoltatori locali e globali, diversi modi di registrarli, priorità di esecuzione e così via.

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

Dai un'occhiata a pyeventdispatcher

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.