Object-Oriented Event Framework

I’m not sure if anyone’s doing something like this already (or if there’s a better way to do it), but here’s what I’m doing for my event system. I find that keeping the arguments contained in an object and passing the object to the functions is a lot nicer than sending the arguments on their own, especially if you need to alter the arguments later.

Every event inherits ‘Event’. To fire an event, call Fire(), which uses messenger to send the event and passes the event object as an argument.

class Event():
    
    Eventname = 'Event'
    
    def __init__(self, eventName = Eventname):
        self.eventName = eventName
        
    def GetEventName(self):
        return self.eventName
    
    def Fire(self, task = None):
        messenger.send(self.eventName, [self])

Here’s some example other events.

from event.Event import Event

class PlayerEvent(Event):
    
    EventName = 'PlayerEvent'
    
    def __init__(self, player, eventName = EventName):
        Event.__init__(self, eventName)
        self.player = player
        
    def GetPlayer(self):
        return self.player    

class PlayerDeathEvent(PlayerEvent):
    
    EventName = 'PlayerDeathEvent'
    
    def __init__(self, player, attacker, wasHeadshot):
        PlayerEvent.__init__(self, player, PlayerDeathEvent.EventName)
        self.attacker = attacker
        self.wasHeadshot = wasHeadshot
        
    def GetVictim(self):
        return self.player
        
    def GetAttacker(self):
        return self.attacker
    
    def WasHeadshot(self):
        return self.wasHeadshot
        
class PlayerRespawnEvent(PlayerEvent):
    
    EventName = 'PlayerRespawnEvent'
    
    def __init__(self, player, pos):
        PlayerEvent.__init__(self, player, PlayerRespawnEvent.EventName)
        self.pos = pos
        
    def GetPos(self):
        return self.pos
    
  
class PlayerJoinEvent(PlayerEvent):
    
    EventName = 'PlayerJoinEvent'
    
    def __init__(self, player, pid, name, playingState):
        PlayerEvent.__init__(self, player, PlayerJoinEvent.EventName)
        self.pid = pid
        self.name = name
        self.playingState = playingState
        
    def GetPid(self):
        return self.pid
    
    def GetName(self):
        return self.name
    
    def GetPlayingState(self):
        return self.playingState
    
class PlayerDisconnectEvent(PlayerEvent):
    
    EventName = 'PlayerDisconnectEvent'
    
    def __init__(self, player):
        PlayerEvent.__init__(self, player, PlayerDisconnectEvent.EventName)
        
        
class PlayerSelectedEvent(PlayerEvent):
    
    EventName = 'PlayerSelectedEvent'
    
    def __init__(self, player):
        PlayerEvent.__init__(self, player, PlayerSelectedEvent.EventName)
        
class PlayerAttackEvent(PlayerEvent):
    
    EventName = 'PlayerAttackEvent'
    
    def __init__(self, player, victim, damage, wasHeadshot):
        PlayerEvent.__init__(self, player, PlayerAttackEvent.EventName)
        self.victim = victim
        self.damage = damage
        self.wasHeadshot = wasHeadshot
        
    def GetAttacker(self):
        return self.player
        
    def GetVictim(self):
        return self.victim
    
    def GetDamage(self):
        return self.damage
    
    def WasHeadshot(self):
        return self.wasHeadshot
    
class PlayerHitEvent(PlayerEvent):
    
    EventName = 'PlayerHitEvent'
    
    def __init__(self, player, victim, wasHeadshot):
        PlayerEvent.__init__(self, player, PlayerHitEvent.EventName)
        self.victim = victim
        self.wasHeadshot = wasHeadshot
        
    def GetAttacker(self):
        return self.player
        
    def GetVictim(self):
        return self.victim
    
    def GetDamage(self):
        return self.damage
    
    def WasHeadshot(self):
        return self.wasHeadshot
    
class PlayerHealthEvent(PlayerEvent):
    
    EventName = 'PlayerHealthEvent'
    
    def __init__(self, player, health):
        PlayerEvent.__init__(self, player, PlayerHealthEvent.EventName)
        self.health = health
        
    def GetHealth(self):
        return self.health
    
class PlayerReloadEvent(PlayerEvent):
    
    EventName = 'PlayerReloadEvent'
    
    def __init__(self, player, health):
        PlayerEvent.__init__(self, player, PlayerReloadEvent.EventName)
        self.health = health
        
    def GetHealth(self):
        return self.health

To listen for events, do something like this:

self.accept(PlayerRespawnEvent.EventName, self.OnPlayerRespawnEvent)
self.accept(PlayerDeathEvent.EventName, self.OnPlayerDeathEvent)
def OnPlayerRespawnEvent(self, event):
        player = event.GetPlayer()
        ...

def OnPlayerDeathEvent(self, event):
        player = event.GetPlayer()
        attacker = event.GetAttacker()
        wasHS = event.WasHeadshot()
        ...

Interesting idea, but I think it offers little more features over the current event system and a penality of way more code. Still, it seems more thought out than the default.

On a sidenote: why do you write a getter for each attribute? I mean, you could as well use the attributes directly, since in Python they’re all public anyway.

My problems with the event system we have in Panda are:

  • It’s global. Every messenger instance sends events and every messenger instance can accept those.

  • I wish there were categories and/or a defined hierarchy or order, so that you can clearly define what is capable of catching events and in which order they’re catched.

  • Panda uses the same global event system internally a lot. So you need to take care of the names (send an event called “a” and the engine reacts as if you typed the a-key) and debugging gets harder if you can’t distinguish the engine’s work from that of your application.

  • The messenger keeps references to objects it calls functions on, so garbage collecting gets difficult. You can work around this calling destroy() on every DO or ignoreAll() on every messenger, sure, but that’s unnecessary boilerplate IMHO.

This is more or less why I don’t use Events in Panda at all.

Cool, I do something pretty similar:

class Event(object):
    """Basic event manager
    Use __iadd__ or add_handler to add a handler for an Event instance.
    Next, use __call__ or fire to trigger all bound handlers.
    You can also use __isub__ or remove_handler to remove a bound handler.

    Sample usage:

        >>> def callback():
        ...     print 'Callback called!'
        ...
        >>> event = Event()
        >>> event += callback
        >>> event()
        Callback called!
        
    Event callbacks can also be removed:
    
        >>> event -= callback
        >>> event()
        >>>

    Arguments given to the Event instance are passed to each callback, as well:

        >>> def callback_with_args(arg1, arg2):
        ...     print 'arg1:', arg1, 'arg2:', arg2
        ...
        >>> event += callback_with_args
        >>> event('Hello,', 'World!')
        arg1: Hello, arg2: World!

    You can temporarily disable an event, too:

        >>> event.disable()
        >>> event('Hello,', 'World!')
        >>> event.enable()
        >>> event('Hello,', 'World!')
        arg1: Hello, arg2: World!
        >>> event.enabled = False
        >>> event('Hello,', 'World!')
        >>> event.enabled = True
        >>> event('Hello,', 'World!')
        arg1: Hello, arg2: World!

    You can also clear all the handlers:

        >>> event.clear()
        >>> event('Hello,', 'World!')
        >>>
    """

    def __init__(self):

        self.enabled = True
        self.handlers = obengine.datatypes.orderedset()

    def add_handler(self, handler):

        self.handlers.add(handler)

        # Return self, for __iadd__ compatibility
        return self

    def remove_handler(self, handler):
        """
        Removes handler handler from this Event's set of bound handlers.
        Raises ValueError if the handler isn't in the set in the first place.
        """

        try:
            
            self.handlers.remove(handler)
            return self

        except:
            raise ValueError('Given handler is not handling this event, and thus cannot be removed')

    def fire(self, *args, **kwargs):
        """
        Triggers this Event.
        All handlers are fired, one after another, with all given arguments to this
        method passed on to them.
        """

        if self.enabled is False:
            return

        for handler in list(self.handlers):
            handler(*args, **kwargs)

    def handler_count(self):
        """
        Returns the current number of handlers.
        """
        return len(self.handlers)

    def clear(self):
        self.handlers = obengine.datatypes.orderedset()

    def enable(self):
        self.enabled = True

    def disable(self):
        self.enabled = False

    __iadd__ = add_handler
    __isub__ = remove_handler
    __call__ = fire
    __len__ = handler_count

This is basically the way C# handles events, which I really liked, so I copied C#-style events to work with Python. :slight_smile: I think the main differences from my snippet and EdBighead’s is that mine does away with event names and uses instance names C#-style, and mine works independently of Panda’s event system, both of which may or may not be a good thing depending on where you come from :slight_smile:

I think the cost of writing a custom event system - my implementation is 143 lines not counting supporting custom datatypes - is outweighed by the ease of use and flexibility events bring to your code in the long run. Events have really made coding easier for me, and they make adding hooks and plugins simpler. Plus, my game has a Lua API - I don’t think it would be possible for me to make one without events.

Addressing the sidenote: In general, it’s good practice to hide direct access to public attributes, even with Python - say you want to take this code and use it in some other code which uses threads; adding the necessary locks would be easy with access functions. You could write a metaclass that adds the locks implicitly, but I think access functions are the easy and explicit solution here.

I’ve just implemented my own, extremely simple event manager.

Highlighted source: bpaste.net/show/24313/

import types

class Event(object):
    """Base class for all event classes. You can add methods and attributes in
    derived classes.
    """
    pass

class Sender(object):
    """Use instances of this to send events to a messenger.
    Preferred way of instancing this class is through the LightMessenger class.
    """
    def __init__(self, messenger):
        self.messenger = messenger

    def send(self, event):
        """Dispatch an event.
        Arguments:
        event -- can be either a string or an instance of Event.
        """
        self.messenger._send(event)

class Reciever(object):
    """Use instances of this to recieve events.
    Preferred way of instancing this class is through the LightMessenger class.
    
    WARNING: multiple calls to accept() with the same event parameter will
    result in multiple functions assigned.
    """
    def __init__(self, messenger):
        self.messenger = messenger

    def accept(self, event, function):
        """Listen for an event and assign a function to be called.
        Arguments:
        event -- can be either a string, the Event class, or an Event derived
                 class.
        function -- can be anything callable. For extra arguments use lambda.
        """
        self.messenger._accept(event, function)

    def ignore(self, event):
        """Do no longer listen for this event.
        Arguments:
        event -- reverts a previously attached function(s) from that event.
        """
        self.messenger._ignore(event)

class LightMessenger(object):
    """Easy and lightweight event manager. Every single messenger instance is
    independent of others. Unlike Panda's internal messenger this is not
    prepared to be used in a multithreading environment.
    
    Example usage:
    
    >>> from __future__ import print_function
    >>> messenger = LightMessenger()
    >>> rec = messenger.getReciever()
    >>> sender = messenger.getSender()
    >>> reciever.accept("custom event", lambda arg: print(arg))
    >>> sender.send("custom event")
    custom event
    >>> reciever.accept(Event, lambda arg: print(arg))
    >>> sender.send(Event())
    <lightmessenger.Event object at 0x7f63f745e790>
    """
    def __init__(self):
        self.listeners_actions = {}
    
    def _accept(self, event, func):
        """This method should be called by recievers."""
        if self.listeners_actions.has_key(event):
            self.listeners_actions[event] += [func]
        else:
            self.listeners_actions[event] = [func]

    def _ignore(self, event):
        """This method should be called by recievers."""
        if event in self.listeners_actions:
            del self.listeners_actions[event]

    def _send(self, event):
        """This method should be called by senders."""
        if isinstance(event, types.StringTypes) and \
            event in self.listeners_actions:
            for func in self.listeners_actions[event]:
                func.__call__(event)
        elif isinstance(event, Event) and \
            event.__class__ in self.listeners_actions:
            for func in self.listeners_actions[event.__class__]:
                func.__call__(event)

    def getReciever(self):
        """Returns a reciever object that is intended for requesting new
        listeners.
        """
        return Reciever(self)

    def getSender(self):
        """Returns a sender object that is intended for sending events to this
        messenger.
        """
        return Sender(self)

    def reset(self):
        """Resets this messenger to its initial state. All recievers and senders
        stay in tact.
        """
        self.listeners_actions = {}