[Python] Gestire eventi con callback

Marco Giusti marco.giusti a posteo.de
Mar 10 Mar 2015 18:33:09 CET


On Mon, Mar 09 2015, Alessandro Re wrote:
> Alla fine *credo* di aver trovato una soluzione *quasi* soddisfacente.
> Alcune scelte fanno cagare, ma per ora può andare.
> Condivido, per un ovvio atto di giustizia cosmica.
> 
> 2015-03-08 15:12 GMT+00:00 Marco Giusti <marco.giusti a posteo.de>:
> > 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)
> 
> Questa implementazione va sicuramente bene per un meccanismo basilare,
> e le ho usate. Bene, Grazie Marco :)
> 
> Però poi mi sono accorto che volevo qualcosa di più elaborato ed
> automatico. Fondamentalmente, quello che ho fatto è mettere in
> register() un sistema che, quando passo un oggetto, io registro in
> quell'oggetto una callback che - all'atto di distruzione - rimuove la
> callback originale. E questa callback che rimuove, viene schedulata
> per essere eseguita una volta sola, e poi rimossa automaticamente.
> 
> Inoltre, per permettere di usare funzioni che incapsulassero i vari
> metodi, ho fatto in modo che register() prenda anche come parametro un
> oggetto da passare come primo argomento alla callback.
> 
> In sostanza, il codice è questo:
> 
> class Base:
>     def register(self, event, target_obj, callback):
>         if target_obj is None:
>             def _call_once(*args, **kwargs):
>                 callback(*args, **kwargs)
>                 self.unregister(event, None, _call_once)
>             self._callbacks.setdefault(event, []).append((None, _call_once))
>         else:
>             self._callbacks.setdefault(event, []).append((target_obj, callback))
>             def _remove_cb(unused, obj):
>                 self.unregister(event, target_obj, callback)
>             target_obj.register('on_remove', None, _remove_cb)
> 
>     def unregister(self, event, target_obj, callback):
>         self._callbacks[event].remove((target_obj, callback))
> 
>     def _run_callbacks(self, event, *args, **kwargs):
>         for obj, cb in list(it for it in self._callbacks.get(event, [])):
>             cb(obj, *args, **kwargs)
> 
>     def remove(self):
>         self._run_callbacks('on_remove', self)

Credo che puoi fare meglio che questo. Vuoi che ogni callback venga
invocata una sola volta?

    def _run_callbacks(self, event, *args, **kwds):
        assert event in self._callbacks
        callbacks = self._callbacks[event]
        while callbacks:
            obj, callback = callbacks.pop()  # .pop(0); deque?
            try:
                callback(obj, *args, **kwds)
            except:
                logger.exception("useful error message")

Inoltre passare un oggetto come primo argomento mi suona tanto da
antipattern. Non è sbagliato, ma chiediti che oggetto sia. Mi suona da
antipattern per come i metodi e le funzioni in python funzionano.
Confronta le due dichiarazioni e come vengono chiamate:

    def metodo(self, *args, **kwds):
        ...

    def funzione(*args, **kwds):
        ...

    m = Object().metodo
    m(*args, **kwds)
    funzione(*args, **kwds)

come vedi le due chiamate sono identiche ma in più il metodo riceve
un oggetto (su cui opera il metodo). Il tuo codice sembra reinventare
una ruota che ormai è ben collaudata.

Comunque l'ultima parola l'hai tu perché il codice che hai postato non
ci aiuta molto a capire esattamente il funzionamento di questi
fantomatici oggetti C++.

Ciao
m.

PS. Le tue linee di codice sono identiche:

    for obj, cb in list(it for it in self._callbacks.get(event, [])):
    for obj, cb in self._callbacks.get(event, [])[:]:

> Scelte di design:
>  - passare l'oggetto (target_obj) esplicitamente in register()
>  - schedulare una callback come "fire once" se target_obj è None.
> Avrei potuto usare un protocollo diverso, ma questo mi è sembrato un
> buon compromesso per ragioni che non sto a spiegare
>  - lasciare che quando la callback è "fire once", le venga passato
> None come primo parametro. Solo per consistenza col resto
>  - ogni callback registrata avrà una callback automatica di
> de-registrazione quando l'oggetto viene distrutto. Probabilmente è uno
> spreco di memoria e ci sono soluzioni migliori (tipo un metodo solo
> che rimuove tutte le callback associate all'oggetto, la lo lascio come
> esercizio al lettore :D).
> 
> Punti interessanti:
>  - in _run_callback costruisco una nuova lista perché può essere che
> cb chiami unregister() durante l'esecuzione. Probabilmente non è la
> soluzione migliore, comunque.
>  - remove fa altre cose, ma è anche un esempio di come usare _run_callbacks.
> 
> Non ho fatto dei gran test, ma per ora funzionicchia.
> Commenti ben accetti - ma considerate che è piuttosto legato al mio
> stile e alle necessità di questa applicazione, non l'ho pensato perché
> fosse generico.


Maggiori informazioni sulla lista Python