Un amigo y yo hemos estado jugando con Pygame y nos encontramos con este tutorial para crear juegos usando pygame. Realmente nos gustó cómo estalló el juego en un sistema modelo-controlador de vista con eventos como intermedios, pero el código hace que sea pesado el uso de isinstance verificaciones para el sistema de eventos.

Ejemplo:

class CPUSpinnerController:
    ...
    def Notify(self, event):
        if isinstance( event, QuitEvent ):
            self.keepGoing = 0

Esto da como resultado un código extremadamente poco pitónico. ¿Alguien tiene alguna sugerencia sobre cómo esto podría mejorarse? ¿O una metodología alternativa para implementar MVC?


Este es un poco de código que escribí basado en la respuesta de @ Mark-Hildreth (¿cómo puedo vincular a los usuarios?) ¿Alguien más tiene alguna buena sugerencia? Voy a dejar esto abierto para otro día más o menos antes de elegir una solución.

class EventManager:
    def __init__(self):
        from weakref import WeakKeyDictionary
        self.listeners = WeakKeyDictionary()

    def add(self, listener):
        self.listeners[ listener ] = 1

    def remove(self, listener):
        del self.listeners[ listener ]

    def post(self, event):
        print "post event %s" % event.name
        for listener in self.listeners.keys():
            listener.notify(event)

class Listener:
    def __init__(self, event_mgr=None):
        if event_mgr is not None:
            event_mgr.add(self)

    def notify(self, event):
        event(self)


class Event:
    def __init__(self, name="Generic Event"):
        self.name = name

    def __call__(self, controller):
        pass

class QuitEvent(Event):
    def __init__(self):
        Event.__init__(self, "Quit")

    def __call__(self, listener):
        listener.exit(self)

class RunController(Listener):
    def __init__(self, event_mgr):
        Listener.__init__(self, event_mgr)
        self.running = True
        self.event_mgr = event_mgr

    def exit(self, event):
        print "exit called"
        self.running = False

    def run(self):
        print "run called"
        while self.running:
            event = QuitEvent()
            self.event_mgr.post(event)

em = EventManager()
run = RunController(em)
run.run()

Esta es otra compilación que utiliza los ejemplos de @Paul, ¡impresionantemente simple!

class WeakBoundMethod:
    def __init__(self, meth):
        import weakref
        self._self = weakref.ref(meth.__self__)
        self._func = meth.__func__

    def __call__(self, *args, **kwargs):
        self._func(self._self(), *args, **kwargs)

class EventManager:
    def __init__(self):
        # does this actually do anything?
        self._listeners = { None : [ None ] }

    def add(self, eventClass, listener):
        print "add %s" % eventClass.__name__
        key = eventClass.__name__

        if (hasattr(listener, '__self__') and
            hasattr(listener, '__func__')):
            listener = WeakBoundMethod(listener)

        try:
            self._listeners[key].append(listener)
        except KeyError:
            # why did you not need this in your code?
            self._listeners[key] = [listener]

        print "add count %s" % len(self._listeners[key])

    def remove(self, eventClass, listener):
        key = eventClass.__name__
        self._listeners[key].remove(listener)

    def post(self, event):
        eventClass = event.__class__
        key = eventClass.__name__
        print "post event %s (keys %s)" % (
            key, len(self._listeners[key]))
        for listener in self._listeners[key]:
            listener(event)

class Event:
    pass

class QuitEvent(Event):
    pass

class RunController:
    def __init__(self, event_mgr):
        event_mgr.add(QuitEvent, self.exit)
        self.running = True
        self.event_mgr = event_mgr

    def exit(self, event):
        print "exit called"
        self.running = False

    def run(self):
        print "run called"
        while self.running:
            event = QuitEvent()
            self.event_mgr.post(event)

em = EventManager()
run = RunController(em)
run.run()
8
Petriborg 31 ago. 2011 a las 00:28

3 respuestas

La mejor respuesta

Una forma más limpia de manejar eventos (y también mucho más rápido, pero posiblemente consume un poco más de memoria) es tener múltiples funciones de controlador de eventos en su código. Algo en este sentido:

La interfaz deseada

class KeyboardEvent:
    pass

class MouseEvent:
    pass

class NotifyThisClass:
    def __init__(self, event_dispatcher):
        self.ed = event_dispatcher
        self.ed.add(KeyboardEvent, self.on_keyboard_event)
        self.ed.add(MouseEvent, self.on_mouse_event)

    def __del__(self):
        self.ed.remove(KeyboardEvent, self.on_keyboard_event)
        self.ed.remove(MouseEvent, self.on_mouse_event)

    def on_keyboard_event(self, event):
        pass

    def on_mouse_event(self, event):
        pass

Aquí, el método __init__ recibe un EventDispatcher como argumento. La función EventDispatcher.add ahora toma el tipo de evento que le interesa y el oyente.

Esto tiene beneficios para la eficiencia ya que el oyente solo es llamado para eventos en los que está interesado. También da como resultado un código más genérico dentro del EventDispatcher mismo:

Implementación EventDispatcher

class EventDispatcher:
    def __init__(self):
        # Dict that maps event types to lists of listeners
        self._listeners = dict()

    def add(self, eventcls, listener):
        self._listeners.setdefault(eventcls, list()).append(listener)

    def post(self, event):
        try:
            for listener in self._listeners[event.__class__]:
                listener(event)
        except KeyError:
            pass # No listener interested in this event

Pero hay un problema con esta implementación. Dentro de NotifyThisClass haces esto:

self.ed.add(KeyboardEvent, self.on_keyboard_event)

El problema es con self.on_keyboard_event: es un método enlazado que pasó a EventDispatcher. Los métodos enlazados contienen una referencia a self; esto significa que mientras EventDispatcher tenga el método enlazado, self no se eliminará.

WeakBoundMethod

Deberá crear una clase WeakBoundMethod que contenga solo una referencia débil a self (veo que ya sabe acerca de referencias débiles) para que EventDispatcher no impida la eliminación de {{ X3}}.

Una alternativa sería tener una función NotifyThisClass.remove_listeners a la que llame antes de eliminar el objeto, pero esa no es realmente la solución más limpia y me parece muy propenso a errores (fácil de olvidar).

La implementación de WeakBoundMethod se vería así:

class WeakBoundMethod:
    def __init__(self, meth):
        self._self = weakref.ref(meth.__self__)
        self._func = meth.__func__

    def __call__(self, *args, **kwargs):
        self._func(self._self(), *args, **kwargs)

Aquí está una implementación más sólida que publiqué en CodeReview, y aquí hay un ejemplo de cómo usaría la clase:

from weak_bound_method import WeakBoundMethod as Wbm

class NotifyThisClass:
    def __init__(self, event_dispatcher):
        self.ed = event_dispatcher
        self.ed.add(KeyboardEvent, Wbm(self.on_keyboard_event))
        self.ed.add(MouseEvent, Wbm(self.on_mouse_event))

Connection Objetos (Opcional)

Al eliminar oyentes del administrador / despachador, en lugar de hacer que EventDispatcher busque innecesariamente entre los oyentes hasta que encuentre el tipo de evento correcto, luego busque en la lista hasta que encuentre el oyente correcto, podría tener algo como esto:

class NotifyThisClass:
    def __init__(self, event_dispatcher):
        self.ed = event_dispatcher
        self._connections = [
            self.ed.add(KeyboardEvent, Wbm(self.on_keyboard_event)),
            self.ed.add(MouseEvent, Wbm(self.on_mouse_event))
        ]

Aquí EventDispatcher.add devuelve un objeto Connection que sabe en qué parte de la lista de listas EventDispatcher reside. Cuando se elimina un objeto NotifyThisClass, también lo es self._connections, que llamará a Connection.__del__, que eliminará al oyente de EventDispatcher.

Esto podría hacer que su código sea más rápido y fácil de usar porque solo tiene que agregar explícitamente las funciones, se eliminan automáticamente, pero depende de usted decidir si desea hacer esto. Si lo hace, tenga en cuenta que EventDispatcher.remove ya no debería existir.

13
Community 13 abr. 2017 a las 12:40

Me topé con el mismo problema (¡casi una década después!), Y aquí hay una implementación que he estado usando para que EventManager notifique solo a un subconjunto de oyentes.
Se basa en defaultdict: el atributo _listeners de EventManager es un defaultdict de WeakKeyDictionary().
Todos los eventos se heredan de una clase vacía abstracta Event, por lo que los oyentes pueden concentrarse solo en algunas clases de eventos que desean escuchar.
Aquí hay un código minimalista para entender la idea:

from collections import defaultdict
from weakref import WeakKeyDictionary

class Event:
    def __init__(self):
       pass

class KeyboardEvent(Event): # for instance, a keyboard event class with the key pressed
    def __init__(self, key):
        self._key = key

class EventManager:
    def __init__(self):
        self._listeners = defaultdict(lambda: WeakKeyDictionary())

    def register_listener(self, event_types, listener):
        for event_type in event_types:
            self._listeners[event_type][listener] = 1

    def unregister_listener(self, listener):
        for event_type in self._listeners:
            self._listeners[event_type].pop(listener, None)

    def post_event(self, event):
        for listener in self._listeners[event.__class__]:
            listener.notify(event)

Al registrarse, un oyente le dice al administrador de eventos de qué tipo de evento desea que se le notifique.
Al publicar eventos, el administrador de eventos solo notificará a los oyentes que se registraron para recibir una notificación para ese tipo de evento.
Este código tiene, por supuesto, mucho menos alcance que la solución muy general (y muy elegante) propuesta por @Paul Manta, pero en mi caso, ayudó a eliminar algunas llamadas repetitivas a isinstance y otras comprobaciones mientras se mantenía cosas tan simples como pude.
Una de las desventajas de esto es que todo tipo de eventos tienen que ser objetos de alguna clase, pero en OO python, se supone que este es el camino a seguir.

0
m.raynal 13 dic. 2019 a las 15:47

Dele a cada evento un método (posiblemente incluso utilizando __call__), y pase el objeto Controlador como argumento. El método "call" debería llamar al objeto controlador. Por ejemplo...

class QuitEvent:
    ...
    def __call__(self, controller):
        controller.on_quit(self) # or possibly... controller.on_quit(self.val1, self.val2)

class CPUSpinnerController:
    ...
    def on_quit(self, event):
        ...

Cualquier código que esté utilizando para enrutar sus eventos a sus controladores llamará al método __call__ con el controlador correcto.

1
Mark Hildreth 30 ago. 2011 a las 20:37