Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python duck-typing for MVC event handling in pygame

A friend and I have been playing around with pygame some and came across this tutorial for building games using pygame. We really liked how it broke out the game into a model-view-controller system with events as a go-between, but the code makes heavy use of isinstance checks for the event system.

Example:

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

This results in some extremely unpythonic code. Does anyone have any suggestions on how this could be improved? Or an alternative methodology for implementing MVC?


This is a bit of code I wrote based on @Mark-Hildreth answer (how do I link users?) Does anyone else have any good suggestions? I'm going to leave this open for another day or so before picking a solution.

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()

This is another build using the examples from @Paul - impressively 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()
like image 557
Petriborg Avatar asked Aug 30 '11 20:08

Petriborg


1 Answers

A cleaner way of handling events (and also a lot faster, but possibly consumes a bit more memory) is to have multiple event handler functions in your code. Something along these lines:

The Desired Interface

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

Here, the __init__ method receives an EventDispatcher as an argument. The EventDispatcher.add function now takes the type of the event you are interested in, and the listener.

This has benefits for efficiency since the listener only ever gets called for events that it is interested in. It also results in more generic code inside the EventDispatcher itself:

EventDispatcher Implementation

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

But there is a problem with this implementation. Inside NotifyThisClass you do this:

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

The problem is with self.on_keyboard_event: it is a bound method which you passed to the EventDispatcher. Bound methods hold a reference to self; this means that as long as the EventDispatcher has the bound method, self will not be deleted.

WeakBoundMethod

You will need to create a WeakBoundMethod class that holds only a weak reference to self (I see you already know about weak references) so that the EventDispatcher does not prevent the deletion of self.

An alternative would be to have a NotifyThisClass.remove_listeners function that you call before deleting the object, but that's not really the cleanest solution and I find it very error prone (easy to forget to do).

The implementation of WeakBoundMethod would look something like this:

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)

Here's a more robust implementation I posted on CodeReview, and here's an example of how you'd use the class:

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 Objects (Optional)

When removing listeners from the manager/ dispatcher, instead of making the EventDispatcher needlessly search through the listeners until it finds the right event type, then search through the list until it finds the right listener, you could have something like this:

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))
        ]

Here EventDispatcher.add returns a Connection object that knows where in the EventDispatcher's dict of lists it resides. When a NotifyThisClass object is deleted, so is self._connections, which will call Connection.__del__, which will remove the listener from the EventDispatcher.

This could make your code both faster and easier to use because you only have to explicitly add the functions, they are removed automatically, but it's up to you to decide if you want to do this. If you do it, note that EventDispatcher.remove shouldn't exist anymore.

like image 171
Paul Manta Avatar answered Oct 13 '22 10:10

Paul Manta