[Python] Gestire eventi con callback

Alessandro Re ale a ale-re.net
Lun 9 Mar 2015 20:24:56 CET


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)

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.
Ciauz
~Ale


Maggiori informazioni sulla lista Python