Archivio

Posts Tagged ‘events’

Python: events per programmatori di coccio

18 Gennaio 2019 Commenti chiusi

Ci sono mille modi per utilizzare la programmazione EVENT-DRIVEN e ci sono mille librerie perfette per tale scopo.
Detto in soldoni, il flusso del programma che scriviamo, cambia a seconda del verificarsi di un determinato evento.
Tipico esempio di programmazione EVENT-DRIVEN è un programma dotato di interfaccia grafica.
Cliccare su un pulsante implica il verificarsi di un evento e di conseguenza l’esecuzione di un codice ben definito.
Ecco ad esempio come wx utilizza e gestisce gli eventi: wx events and event handlers.

I programmi event-driven sono composti tipicamente da brevi sotto-programmi, chiamati event-handlers (gestori degli eventi), che sono eseguiti in risposta agli eventi esterni, e da un dispatcher, che effettua materialmente la chiamata.
Il dispatcher utilizza una coda degli eventi, che contiene l’elenco degli eventi già verificatisi, ma non ancora “processati”.
In molti casi i gestori degli eventi possono, al loro interno, innescare (“trigger”) altri eventi, producendo una cascata di eventi.

Ecco un primo esempio semplicissimo di programmazione event-driven.

# eventdrivensimple.py

class Observer():
    _observers = []
    def __init__(self):
        self._observers.append(self)
        self._observables = {}
    def observe(self, event_name, callback):
        self._observables[event_name] = callback


class Event():
    def __init__(self, event_name, data, autofire=True):
        self.name = event_name
        self.data = data
        if autofire:
            self.fire()
    def fire(self):
        for observer in Observer._observers:
            if self.name in observer._observables:
                observer._observables[self.name](self.data)


class Listener(Observer):
    def __init__(self, name):
        self.name = name
        super(Listener, self).__init__()
        print("<%s> is waiting for events..." % self.name)

    def on_event(self, event_type, data=None):
        print("[%s] event '%s' received!" % (self.name, event_type))

Analizziamo la prima classe definita, Observe.

class Observer():
    _observers = []
    def __init__(self):
        self._observers.append(self)
        self._observables = {}
    def observe(self, event_name, callback):
        self._observables[event_name] = callback

La classe Observer definisce, a livello di classe, un lista di observers (ovvero le istanze della classe Listener
che vedremo più avanti) e in fase di creazione dell’istanza, un dizionario che avrà come chiavi, i tipi di evento e come valori, le callbacks da chiamare al verificarsi di tale evento.

La seconda classe definita è l’oggetto evento vero e proprio, Event.

class Event():
    def __init__(self, event_name, data, autofire=True):
        self.name = event_name
        self.data = data
        if autofire:
            self.fire()
    def fire(self):
        for observer in Observer._observers:
            if self.name in observer._observables:
                observer._observables[self.name](self.data)

In fase di creazione di un’istanza evento, diamo un nome all’evento e possiamo anche passargli dei dati, ad esempio l’istanza stessa.
L’argomento autofire=True serve per chiamare un automatico il metodo fire() in fase di creazione.
Questo metodo cicla sulla lista _observers della classe Observer.
Per ogni observer della lista, controlla che nel dizionario _observables, sia presente quello specifico evento.
In caso di presenza, chiama la callback associata a tale chiave, passando gli eventuali argomenti (data).

Infine abbiamo la classe Listener, che semplicemente estende la prima classe Observer

class Listener(Observer):
    def __init__(self, name):
        self.name = name
        super(Listener, self).__init__()
        print("<%s> is waiting for events..." % self.name)

    def on_event(self, event_type, data=None):
        print("[%s] event '%s' received!" % (self.name, event_type))

In fase di creazione dell’istanza, tramite super il listener stesso viene aggiunto alla lista degli observers.

Ora vediamo il funzionamento, creiamo per prima cosa un listener:

>>> from eventdrivensimple import Observer, Event, Listener
>>> listener1 = Listener(name="listener1")
<listener1> is waiting for events...

Siccome listener eredita da Observer, con il metodo observe() ereditato, gli diciamo
di aggiungere al dizionario la coppia evento:callback ovvero “event1”: listener.on_event

listener1.observe('event1',  listener1.on_event)

Quando si verificherà il tal evento (event1), si innescherà il ciclo precedentemente visto, ciòè tutti i listeners che avranno tale
evento nel proprio dizionario, chiameranno la callback associata:

>>> listener1.observe(event_name="event1", callback=listener1.on_event)
>>> Event(event_name="event1", data="foo")
[listener1] event 'foo' received!
<eventdrivensimple.Event object at 0x00866650>
>>> Event(event_name="event2", data="bar")
<eventdrivensimple.Event object at 0x00866690>

Come si nota il secondo evento non viene ricevuto, non avendo registrato il listener per tale evento.
Chiaramente la cosa funziona anche con diversi listeners:

>>> listener1 = Listener(name="listener1")
<listener1> is waiting for events...
>>> listener2 = Listener(name="listener2")
<listener2> is waiting for events...
>>> listener1.observe(event_name="event1", callback=listener1.on_event)
>>> listener1.observe(event_name="event2", callback=listener1.on_event)
>>> listener2.observe(event_name="event2", callback=listener2.on_event)
>>> Event(event_name="event1", data="foo")
[listener1] event 'foo' received!
<eventdrivensimple.Event object at 0x00625E90>
>>> Event(event_name="event2", data="bar")
[listener1] event 'bar' received!
[listener2] event 'bar' received!
<eventdrivensimple.Event object at 0x00625EB0>

Questo è uno dei metodi più semplici di utilizzo degli eventi, con tutti i contro del caso.
In realtà i pattern di questo tipo sono molto più complicati, ma esistono librerie apposite che compiono questo lavoro perfettamente.
Il fine è solo quello di capirne il meccanismo.

Ad esempio si potrebbe utilizzare una classe Dispatcher che esegua il compito che fino ad ora abbiamo svolto
all’interno della stessa classe Event, autochiamando il metodo fire() ad ogni creazione di un’istanza evento.

Ecco un esempio un po’ più complesso:

# eventdrivenmedium.py

class Event:
    def __init__(self, event_name, data=None):
        self._name = event_name.lower()
        self._data = data
        
    @property 
    def name(self):
        return self._name
        
    @property
    def data(self):
        return self._data


class EventDispatcher:
    def __init__(self):
        self._events = dict()
        
    def __del__(self):
        self._events = None
    
    def has_listener(self, event_name, listener):
        if event_name in self._events.keys():
            return listener in self._events[event_name]
        else:
            return False
        
    def add_event_listener(self, event_name, listener):
        if not self.has_listener(event_name, listener):
            listeners = self._events.get(event_name, [])
            listeners.append(listener)
            self._events[event_name] = listeners
    
    def remove_event_listener(self, event_name, listener):
        if self.has_listener(event_name, listener):
            listeners = self._events[event_name]
            if len(listeners) == 1:
                # Only this listener remains so remove the key
                del self._events[event_name]
            else:
                # Update listeners value
                listeners.remove(listener)
                self._events[event_name] = listeners

    def dispatch_event(self, event):
        if event.name in self._events.keys():
            listeners = self._events[event.name]
            
            for listener in listeners:
                listener(event)  # fire!


class Listener:
    def __init__(self, name, event_dispatcher):
        self.name = name
        self.event_dispatcher = event_dispatcher

    def add_to_events(self, event_name, listener):
        self.event_dispatcher.add_event_listener(event_name=event_name, 
                                                 listener=self.on_event)

    def on_event(self, event):
        """
        Callback to override when extend Listener class
        """
        pass

La classe Event, sempre quella è.
La classe EventDispatcher crea un dizionario del tipo event_name:listeners, dove event_name è il tipo di evento, mentre listeners è la lista di tutte le callbacks associate a tal evento.
I metodi del dispatcher sono:

has_listener(event_name, listener):

...
    def has_listener(self, event_name, listener):
        if event_name in self._events.keys():
            return listener in self._events[event_name]
        else:
            return False

controlla che event_name sia presente tra le chiavi del dizionario degli eventi, se sì, ritorna True se nella lista dei listeners associati a tale evento, è presente il listener passato come argomento, oppure ritorna False.

add_event_listener(event_name, listener):

...
    def add_event_listener(self, event_name, listener):
        if not self.has_listener(event_name, listener):
            listeners = self._events.get(event_name, [])
            listeners.append(listener)
            self._events[event_name] = listeners

Se il listener non è già presente nel dizionario, ottengo dal dizionario stesso la lista dei listeners ed aggiungo il listener passato come argomento, poi aggiorno il valore per quella chiave evento, con la nuova lista di listeners aggiornata.

remove_event_listener(event_name, listener):

...
    def remove_event_listener(self, event_name, listener):
        if self.has_listener(event_name, listener):
            listeners = self._events[event_name]
            if len(listeners) == 1:
                # Only this listener remains so remove the key
                del self._events[event_name]
            else:
                # Update listeners value
                listeners.remove(listener)
                self._events[event_name] = listeners

fa il percorso inverso del metodo precedente. Se il listener è presente, lo rimuove ed aggiorna il valore con la lista dei listeners aggiornata, se la lista rimane vuota, viene eliminata la chiave di quell’evento, dal dizionario degli eventi.

dispatch_event(event):

Il cuore del dispatcher! Se l’evento è nel dizionario degli eventi, chiamo tutti i listeners associati ad esso.

...
    def dispatch_event(self, event):
        if event.name in self._events.keys():
            listeners = self._events[event.name]
            
            for listener in listeners:
                listener(event)  # fire!

L’ultima classe definita, è un classe generica di Listener, che estenderemo al bisogno.

class Listener:
    def __init__(self, name, event_dispatcher):
        self.name = name
        self.event_dispatcher = event_dispatcher

    def add_to_events(self, event_name, listener):
        self.event_dispatcher.add_event_listener(event_name=event_name, 
                                                 listener=self.on_event)

    def on_event(self, event):
        """
        Callback to override when extend Listener class
        """
        pass

Riceve, oltre al nome, un’istanza di dispatcher.

add_to_events(event_name, listener)

Aggiunge al dizionario degli eventi del dispatcher, un evento (event_name) associato alla callback (listener) da chiamare quando si verifica tale evento.

on_event(event)

callback vuota che verrà sovrascritta quando erediteremo dalla classe Listener.
E’ la callback che verrà invocata al presentarsi di un determinato evento.

Ecco un esempio chiarificatore: creiamo una classe Site ed una User che ereditino da Listener.
Poi definiamo i due event_name che utilizzeremo nell’esempio.

CHECK = "check logged in users"
RESPOND = "respond to check"


class Site(Listener):
    def __init__(self, name, event_dispatcher):
    	super(Site, self).__init__(name, event_dispatcher)
    	self.add_to_events(event_name=RESPOND, listener=self.on_event)
        
    def on_event(self, event):
        """
        Event handler for the RESPOND event type
        """
        print("<<< I'm logged in [{0}]".format(event.data.name))

    def check(self):
        print(">>> Who are logged in? [{0}]".format(self.name))
        self.event_dispatcher.dispatch_event(Event(event_name=CHECK, data=self))
        

class User(Listener):
    def __init__(self, name, event_dispatcher):
    	super(User, self).__init__(name, event_dispatcher)
    	self.add_to_events(event_name=CHECK, listener=self.on_event)
        
    def on_event(self, event):
        """
        Event handler for 'CHECK' event type
        """
        self.event_dispatcher.dispatch_event(Event(event_name=RESPOND, 
        	                                       data=self))

L’istanza di Site una volta creata aggiungerà al dizionario degli eventi l’evento RESPOND e la callback on_event ereditata ed opportunamente modificata. Stessa cosa per User; l’istanza registrerà la propria callback all’evento CHECK.
Quando site effettuerà un check con l’apposito metodo, chiamerà il metodo dispatch_event del dispatcher sull’evento CHECK.
Il dispatcher cercherà sul dizionario tutti i listener collegati a CHECK, nel nostro caso gli users che, una volta istanziati hanno aggiunto la propria callback all’elenco di listeners relativi all’evento stesso.
Trovati tali listeners, verranno invocate le relative callbacks (metodo on_event di User).
Tali callbacks, tramite il metodo dispatch_event del dispatcher, genereranno l’evento RESPOND.
Il listener collegato a tale evento è on_event di site, che risponde pertanto alla chiamata.

>>> from eventdrivenmedium import EventDispatcher, Site, User
>>> dispatcher = EventDispatcher()
>>> site = Site(name="examlpe.com", event_dispatcher=dispatcher)
>>> user1 = User(name="bancaldo", event_dispatcher=dispatcher)
>>> user2 = User(name="anonymous", event_dispatcher=dispatcher)
>>> site.check()
>>> Who are logged in? [examlpe.com]
<<< I'm logged in [bancaldo]
<<< I'm logged in [anonymous]

Questi sono appunti sui principi di funzionamento della programmazione event-driven. Gli esempi sono MOLTO semplificati.

Categorie:python Tag: ,