10

I'm working on my first Python project, and I'm already missing events in my classes. Perhaps it's not even called events in Python, but I would like to create "groups" in my classes to which function references can be added. At some point in my class all function references in my group would execute.

Is this built into Python? (I'm using 2.7 at the moment)

Hubro
  • 56,214
  • 69
  • 228
  • 381
  • 3
    Please explain what you mean by events. There are exceptions (of various names and purposes), but they're not really events. If you're referring to things such as "onClick" or "onKeyDown", you get that from the built-in Tkinter library or some other GUI library written and integrated with Python (as far as I know, you can't integrate that with a non-GUI program). What behaviour are you seeking exactly? –  May 27 '11 at 23:32
  • 1
    Perhaps you should search for "python observer pattern". There are lots of posts on this subject. – tkerwin May 27 '11 at 23:36
  • Of course you can implement it. Of course your specific need isn't catered for by the stdlib as it's likely highly domain specific. It might be as simple as `self.events[eventtype].add(handler)` and `for handler in self.events[eventtype]: handler(...)` or require much more bookkeeping depending on your needs. Give details. –  May 27 '11 at 23:40
  • Chrono Kitsune: I'm seeking a very simple behaviour. I have classes that handle various tasks (Hosting servers, processing databases etc.) . I would like my Python scripts to be able to tell these class objects to run given functions on the occurrence of a certain event, for instance `clientConnected` or `processingComplete`. – Hubro May 28 '11 at 00:29

3 Answers3

15

Python doesn't have any sort of event system built-in, but it's could be implemented pretty simply. For example:

class ObjectWithEvents(object):
    callbacks = None

    def on(self, event_name, callback):
        if self.callbacks is None:
            self.callbacks = {}

        if event_name not in self.callbacks:
            self.callbacks[event_name] = [callback]
        else:
            self.callbacks[event_name].append(callback)

    def trigger(self, event_name):
        if self.callbacks is not None and event_name in self.callbacks:
            for callback in self.callbacks[event_name]:
                callback(self)

class MyClass(ObjectWithEvents):
    def __init__(self, contents):
        self.contents = contents

    def __str__(self):
        return "MyClass containing " + repr(self.contents)

def echo(value): # because "print" isn't a function...
    print value

o = MyClass("hello world")
o.on("example_event", echo)
o.on("example_event", echo)
o.trigger("example_event") # prints "MyClass containing \"Hello World\"" twice
Jeremy
  • 1
  • 85
  • 340
  • 366
  • 5
    Three notes though: (1) It's propably better to set `self.callbacks = {}` in `__init__` and avoid the bogus class variable (especially since some poor fool might overwrite it). (2) In the same vein, `self.callbacks` should be private, i.e. `self._callbacks`. (3) `collections.defaultdict(list)` simplifies adding handlers as you don't need to differentiate whether there are already callbacks for a given event name. –  May 27 '11 at 23:46
  • 1
    Generally, I agree. I chose to use this method because it was the most self-contained, for the convenience of people who are less familiar with the language (if I had used `__init__` then the user would have to deal with the joy that is `super()`). – Jeremy May 28 '11 at 02:30
  • You profile is freaking me out! How did you manage to get "1" with 70+ gold badges!? – not2qubit Nov 13 '20 at 01:36
  • 1
    @not2qubit It looks like Jeremy's account is temporarily suspended by the Overflow moderators. As a side effect, this means that the account's reputation scores are locked at "1". The details are here: https://stackoverflow.blog/2009/04/06/a-day-in-the-penalty-box/?_ga=2.168645217.1934485764.1616424759-1339734529.1612502453 – Neils Schoenfelder Mar 24 '21 at 14:35
  • @NeilsSchoenfelder Wow. I had no idea this even existed. Thx for clarifying link. – not2qubit Mar 24 '21 at 14:40
12

While Jeremy Banks' answer works just fine, it's not what most would call "pythonic". Since this question comes up quite easily through search engines, here's an alternative answer that attemps to use the best conventions from my experience:

class Event:
    def __init__(self):
        self.listeners = []

    def __iadd__(self, listener):
        """Shortcut for using += to add a listener."""
        self.listeners.append(listener)
        return self

    def notify(self, *args, **kwargs):
        for listener in self.listeners:
            listener(*args, **kwargs)

To use it you simply create an Event object and then register listener callbacks by either manipulating the listeners list directly, or using the += shortcut. You then use the notify() method to call all the listeners. Any arguments and keyword arguments passed to the notify() method will be forwarded to the listeners.

Here's a full example:

>>> my_event = Event()
>>> def print_person_info(name, age, sex):
...     print("Hello! I am {}, I'm a {}-year-old {}".format(name, age, sex))
...
>>> my_event += print_person_info
>>> my_event.notify('Markus', 23, 'male')
Hello! I am Markus, I'm a 23-year-old male

These event objects can easily be added to a class or an instance as well:

class Soldier:
    # An event on a class level.
    # Listening to just this will notify you of *any* person dying. 
    e_death = Event()

    def __init__(self, name, health):
        self.name = name
        self.health = health

        # Instance level event.
        # Using this you need to listen to each person separately.
        self.e_eat = Event()

    def eat(self, amount):
        self.health += amount
        self.e_eat.notify(self, amount=amount)

    def hurt(self, damage):
        self.health -= damage
        if self.health <= 0:
            Soldier.e_death.notify(self)

Of course it's usually a bad idea to mix class and instance level events like this, I've only done if for demonstration purposes. If unsure, use the instance level events.

Markus Meskanen
  • 19,939
  • 18
  • 80
  • 119
  • where is `Person` defined? – Timo Sep 10 '20 at 14:26
  • 2
    @Timo That was remains from an earlier scratch, I fixed it to use the `Soldier` class instead. Thanks! – Markus Meskanen Sep 17 '20 at 09:15
  • @MarkusMeskanen Can you please update your "full example" to also use the import (or whatever) is needed to do before actually running that code? Also I don't quite see the connection of *those* to the last *Soldier* example. Maybe you can make your example self contained, instead of mixing with the Py CLI? – not2qubit Nov 15 '20 at 14:16
1

In case someone is interested in event support for python version 3+ there is event-notifier library available ( https://pypi.org/project/event-notifier/ )

Also there is also a great observation list of alternatives available here:

mandeeppp
  • 19
  • 1