Hone logo
Hone
Problems

Python Event Publisher/Subscriber System

Many applications require components to communicate with each other without direct coupling. The publish-subscribe (pub/sub) pattern, often implemented using an event system, is a common solution. This challenge asks you to build a basic event system in Python that allows objects to "publish" events and other objects to "subscribe" to those events, reacting when they occur.

Problem Description

You need to create a Python class that acts as an event manager. This manager should allow other objects to register callback functions (listeners) for specific event types. When an event is published, the manager should then invoke all registered listeners for that event.

Key Requirements:

  1. Event Publishing: A mechanism to signal that an event has occurred, optionally passing data with the event.
  2. Event Subscription: A way for objects to register a function (callback) to be executed when a specific event type is published.
  3. Unsubscription: The ability to remove a previously registered callback for an event.
  4. Event Handling: When an event is published, all registered callbacks for that event type must be executed.

Expected Behavior:

  • When publish(event_type, *args, **kwargs) is called, all functions subscribed to event_type should be executed with the provided *args and **kwargs.
  • When subscribe(event_type, callback_function) is called, callback_function should be added as a listener for event_type.
  • When unsubscribe(event_type, callback_function) is called, callback_function should be removed from the listeners of event_type.

Edge Cases to Consider:

  • Publishing an event with no subscribers.
  • Unsubscribing a callback that was never subscribed or already unsubscribed.
  • Subscribing the same callback multiple times for the same event. (Should it be allowed? For this challenge, assume it's okay, but it will be called multiple times).

Examples

Example 1:

class EventManager:
    def __init__(self):
        self._listeners = {}

    def subscribe(self, event_type, callback):
        if event_type not in self._listeners:
            self._listeners[event_type] = []
        self._listeners[event_type].append(callback)

    def unsubscribe(self, event_type, callback):
        if event_type in self._listeners and callback in self._listeners[event_type]:
            self._listeners[event_type].remove(callback)
            if not self._listeners[event_type]: # Clean up empty lists
                del self._listeners[event_type]

    def publish(self, event_type, *args, **kwargs):
        if event_type in self._listeners:
            # Create a copy to avoid issues if a listener unsubscribes during iteration
            for callback in list(self._listeners[event_type]):
                callback(*args, **kwargs)

def handler1(message):
    print(f"Handler 1 received: {message}")

def handler2(message, sender="Unknown"):
    print(f"Handler 2 received: {message} from {sender}")

manager = EventManager()

# Subscribe handlers
manager.subscribe("user_login", handler1)
manager.subscribe("user_login", handler2)
manager.subscribe("system_message", handler1)

# Publish events
manager.publish("user_login", "Welcome, John!")
manager.publish("system_message", "Server is starting.")

# Expected Output:
# Handler 1 received: Welcome, John!
# Handler 2 received: Welcome, John! from Unknown
# Handler 1 received: Server is starting.

# Unsubscribe handler1 from user_login
manager.unsubscribe("user_login", handler1)

# Publish again
manager.publish("user_login", "Welcome back, Jane!")

# Expected Output:
# Handler 2 received: Welcome back, Jane! from Unknown

Explanation: handler1 and handler2 are defined to react to events. They are subscribed to "user_login". handler1 is also subscribed to "system_message". When "user_login" is published, both handler1 and handler2 are called with the message. When "system_message" is published, only handler1 is called. After unsubscribing handler1 from "user_login", subsequent publishes to "user_login" only trigger handler2.

Example 2:

# ... (EventManager class from Example 1) ...

def data_processor(data_value, timestamp=None):
    print(f"Processing data: {data_value}. Timestamp: {timestamp}")

def logger(message, level="INFO"):
    print(f"[{level}] {message}")

manager = EventManager()

manager.subscribe("data_update", data_processor)
manager.subscribe("log_event", logger)

# Publish with keyword arguments
manager.publish("data_update", data_value=100, timestamp="2023-10-27T10:00:00Z")
manager.publish("log_event", message="User logged out.", level="DEBUG")

# Expected Output:
# Processing data: 100. Timestamp: 2023-10-27T10:00:00Z
# [DEBUG] User logged out.

Explanation: This example demonstrates publishing events with keyword arguments. data_processor correctly receives data_value and timestamp, and logger receives message and level.

Example 3: Edge Case - Publishing an event with no subscribers.

# ... (EventManager class from Example 1) ...

manager = EventManager()

# Publish an event that no one is subscribed to
manager.publish("non_existent_event", "This should not be printed.")

# Expected Output: (No output related to the event)

Explanation: When publish is called for an event_type that has no registered listeners, the EventManager gracefully does nothing, and no errors occur.

Constraints

  • The EventManager class must be implemented in Python.
  • Callback functions can accept any number of positional and keyword arguments.
  • The solution should be efficient enough for a moderate number of event types and subscribers. The number of event types will not exceed 100, and the total number of subscribers across all event types will not exceed 1000.

Notes

  • Consider how to store the subscriptions. A dictionary mapping event types to lists of callbacks is a common approach.
  • When iterating through listeners to call them, be mindful of potential modifications to the listener list (e.g., a listener unsubscribing itself or another listener). Creating a copy of the listener list before iterating can prevent issues.
  • You are free to design the EventManager class as you see fit, but the core subscribe, unsubscribe, and publish methods are essential.
Loading editor...
python