The Listeners

A medium Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.

Domain
Python
Difficulty
medium
Seniority
L4
Asked in
Foursquare

Problem

A clickstream collector runs a small in-process event bus. Each event name holds its own list of subscriptions, and each subscription is one listener id. on(event, listener) adds a subscription, and the same listener may subscribe to an event more than once. off(event, listener) removes one of that listener's subscriptions on that event, and does nothing when it has none. emit(event, payload) delivers the payload once for each subscription on that event and returns the deliveries as a list. Implement the EventEmitter class. Then implement event_broadcaster(op_names, op_args): the first op builds the emitter, and every op after it runs against that emitter. Return one result per op: None for the constructor, on and off, and the delivered list for emit.

Subscribers show up, listen, and sometimes leave.

Example

Input:

op_names = ["EventEmitter","on","emit","off","emit"], op_args = [[],["click","log_click"],["click","event1"],["click","log_click"],["click","event2"]]

Output:

[null,null,["event1"],null,[]]

Worked solution and explanation

What this really is

Strip the pub/sub costume and this is two data structures pretending to be one. The emitter is a keyed registry: a dict of event to an ordered list of listeners. The driver is an op-replay loop, the exact shape you see in LRU-cache and stack harnesses where the test scripts a sequence of method calls. The skill being probed is keeping them apart. Anyone can append to a list; the tell is whether your emit and off touch only the target event, and whether you resist letting the driver's bookkeeping leak into the emitter. Blur the two and emit for one event starts returning another event's listeners, or off strips the wrong subscription.


Break down the requirements

Step 1: Keyed registration with first-match removal

Inside EventEmitter, keep self.listeners as a dict[str, list]. on(event, listener) appends listener to self.listeners.setdefault(event, []). off(event, listener) removes the first matching listener from that event's list, and does nothing if the list is empty or the listener isn't found. The keying is load-bearing: a single flat list would mix events, and a set would drop the duplicate ordering that off depends on.

Step 2: emit returns one payload per active listener

emit(event, payload) returns [payload] * len(self.listeners.get(event, [])). The length is the number of listeners on that event at the moment of emission, so a duplicate subscription counts twice. Listeners are opaque identifiers, so nothing is actually invoked; the contract is simply to deliver the payload once per current listener, and an unseen or emptied event returns [].

Step 3: Driver loop over the parallel op arrays

event_broadcaster(op_names, op_args) walks the two parallel lists in lockstep. Index 0 is always 'EventEmitter' and constructs the instance (result None). For each later op, dispatch on the name and append the result: on and off produce None, emit produces the payload list. The driver holds no event logic of its own.


The solution

EventEmitter plus a driver that replays `op_names` against `op_args`
class EventEmitter:
    def __init__(self) -> None:
        self.listeners: dict[str, list] = {}

    def on(self, event, listener):
        self.listeners.setdefault(event, []).append(listener)

    def emit(self, event, payload):
        return [payload] * len(self.listeners.get(event, []))

    def off(self, event, listener):
        bucket = self.listeners.get(event)
        if not bucket:
            return
        try:
            bucket.remove(listener)
        except ValueError:
            pass


def event_broadcaster(op_names: list[str], op_args: list[list]) -> list:
    emitter = None
    out = []
    for name, args in zip(op_names, op_args):
        if name == 'EventEmitter':
            emitter = EventEmitter()
            out.append(None)
        elif name == 'on':
            emitter.on(*args)
            out.append(None)
        elif name == 'emit':
            out.append(emitter.emit(*args))
        elif name == 'off':
            emitter.off(*args)
            out.append(None)
    return out

Common follow-up questions