[Python] Gestire eventi con callback

Marco Giusti marco.giusti a posteo.de
Dom 8 Mar 2015 16:12:02 CET


On Sun, Mar 08 2015, Alessandro Re wrote:
> Ciao Marco,
> 
> intanto grazie per la risposta :)

Non c'è di che :)

> 2015-03-08 7:54 GMT+00:00 Marco Giusti <marco.giusti a posteo.de>:
> > Come registri le callback? Gli oggetti in python sono referenziati da
> > del codice python o solo dalle callback di c++? In questo caso basta
> > che, nel momento in cui distruggi l'oggetto C, deregistri le callback
> > e riduci il numero di referenze del metodo. Se i metodi di un oggetto
> > P sono usati come callback di un solo oggetto C, al momento che
> > tu rilasci tutte le callback non esistono più riferimenti all'oggetto
> > python.
> 
> Il problema è proprio la registrazione delle callback... Per ora ho un
> meccanismo piuttosto primitivo:
> quando un evento accade, viene chiamato un metodo on_evento(*args) di python.
> L'idea è che l'oggetto tiene una lista di callback da
> chiamare per quell'evento, e quel metodo è definito più o meno come
> 
> def on_evento(self, *args):
>   for callback in self._callbacks:
>     callback(*args)
> 
> Però è per l'appunto primitivo, e non viene assolutamente gestita la
> rimozione delle callback. Prima di avventurarmi ed inventare qualcosa
> che cerchi di automatizzare la rimozione, volevo capire se c'era già
> qualche pattern usato in questi casi.

Il patter che più gli assomigli è l'Observer ma non sono sicuro che la
terminologia sia quella giusta in questo caso. Comunque
l'implementazione è all'incirca quella:

    ...
        self._events = {}

    def register(self, event, callback):
        self._events.setdefault(event, []).append(callback)

    def unregister(self, event, callback):
        callbacks = self._events[event]
        callbacks.pop(callbacks.index(callback))

    def on_event(self, event, *args):
        for callback in self._events[event]:
            callback(*args)

Così come registri una callback, così la rimuovi. Se non riesci a
definire formalmente un ciclo di vita per i tuoi oggetti, allora è
probabile che nel tuo design qualcosa sia sbagliato.

Ancora una volta ti offro una soluzione semplicistica. Se l'oggetto
prima di essere distrutto scatena l'evento "on_remove":

    def on_remove(self, *args):
        self.on_event("remove", *args)
        del self._events.clear()


    def use(...):
        obj = Object(...)
        event_handler.register("foo", obj.foo)
        event_handler.register("remove", obj.remove)
        event_handler.register("foo", Object(...).foo)

in questa maniera le uniche referenze ai due oggetti "Object" sono
nell'event_handler. Prima che l'oggetto C++ venga distrutto scatena
l'evento "on_remove" e le referenze dei due oggetti "Object" vengono
riportate a zero.

Fossi in te inizierei con una implementazione di questo tipo. Se è vero
che è primitiva, le specifiche non sembrano ancora definite e trovo
inutile andare a complicarsi troppo la vita. Scrivi un paio di test,
vedi se il codice che viene fuori è soddisfacente o se richiede qualche
hack non troppo elegante e parti da lì per migliorarlo.

> >> Come posso rendere sincrona (bloccante) l'invocazione di un comando?
> >
> > La prima idea che mi viene in mente è di infilare la callback dentro la
> > send e di chiamare sleep per salvare qualche ciclo macchina:
> >
> >     def send_data_blocking(data):
> >         def go():
> >             free_to_go = True
> >         free_to_go = False
> >         send_data(data, callback=go)
> >         while not free_to_go:
> >              sleep(0.1)
> 
> Grazie, proverò con la sleep :) (btw, in go() non ci andrebbe un
> "nonlocal free_to_go"?)

Per un momento ho creduto che "go" fosse il linguaggio. Non conosco
nonlocal ma hai ragione se dici che c'è un errore grossolano. Più o meno
è questo che avevo in mente, ma non avendo neanche provato il
codice...

    def send_data_blocking(data):
        def go():
            free_to_go[0] = True
        free_to_go = [False]
        send_data(data, callback=go)
        while not free_to_go[0]:
             sleep(0.1)

Ciao
m.


Maggiori informazioni sulla lista Python