[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