FEAT: Add class-level subscribe to HasEmitters#3368
Conversation
|
Performance benchmarks:
|
|
Performance benchmarks:
|
| key in self.subscribers and len(self.subscribers[key]) > 0 | ||
| ) | ||
| has_class_subscribers = ( | ||
| hasattr(self, "_class_subscribers") |
There was a problem hiding this comment.
this check should not be needed if _class_subscribers is implemented correctly.
| active_observer(signal) | ||
| active_observers.append(observer) | ||
| # use iteration to also remove inactive observers | ||
| if key in self.subscribers: |
| class_observers = self._class_subscribers[key] | ||
| active_class_observers = [] | ||
| for observer in class_observers: | ||
| if active_class_observer := observer(): | ||
| active_class_observer(signal) | ||
| active_class_observers.append(observer) | ||
|
|
||
| if active_class_observers: | ||
| self._class_subscribers[key] = active_class_observers | ||
| else: | ||
| del self._class_subscribers[key] |
There was a problem hiding this comment.
This seems like code repetition. I am wondering whether this could not be implemented in a way that would avoid the apparent code duplication.
| return signal_type | ||
|
|
||
| @classmethod | ||
| def subscribe( |
There was a problem hiding this comment.
not sure about the naming here. We now have Class.subcribe and Instance.observe. What about e.g., Class.observe_class or something along those lines?
| names = ( | ||
| [observable_name] | ||
| if isinstance(observable_name, str) | ||
| else (cls.observables.keys() if observable_name is ALL else observable_name) | ||
| ) |
There was a problem hiding this comment.
please refactor this into something more readable.
| for st in signal_types: | ||
| cls._class_subscribers[(name, st)].append(ref) | ||
|
|
||
|
|
There was a problem hiding this comment.
there seems to be overlap with observe code, so can this be simplified in a way that avoids code duplications?
This second question points to a wider architectural question. At the moment, this PR maintains two lists of subscribers: instance-level subscriptions and class-level subscriptions. To me this is a sensible design. However, an alternative would be to let the instance at creation "inherit" all class-level subscriptions by adding these to the instance subscription dict. The advantage of this is that |
|
Regarding the architectural question, I definitely think keeping the two list design is the most robust approach. If we were to let instances inherit class-level subscriptions by copying them into the instance dictionary during
To resolve this and address the rest of your feedback, I have made the following changes:
Let me know if this implementation aligns with your vision. |
| else: | ||
| del self.subscribers[key] | ||
| @classmethod | ||
| def _clear_all_subscriptions_from_dict(cls, subs_dict: dict, name: ObservableName): |
There was a problem hiding this comment.
| def _clear_all_subscriptions_from_dict(cls, subs_dict: dict, name: ObservableName): | |
| def _clear_all_subscriptions(cls, subs_dict: dict, name: ObservableName): |
|
@quaquel, Is there still anything you wish to be changed? |
|
|
||
| signal_types = target_signals or self.observables[name] | ||
| for name in names: | ||
| signal_types = target_signals or cls.observables[name] |
There was a problem hiding this comment.
You might want to clarify the logic here. If signal_type is ALL, target_signals returns None and that is handled nex in this statement.
|
Sorry took me a while to review the code. It looks fine with one remaining request. |
Summary
This PR introduces the ability to subscribe to observable events at the class level via a new
@classmethod subscribe. Subscriptions made this way are inherited by all instances of that class, meaning any instance emitting the specified signal will trigger the registered handler.Motive
This addresses a specific enhancement requested in the Mesa Signals tracking issue #3227. Previously, observing agents globally required relying on
model.register_agentor manually bindingobserveto every instantiated object at runtime. This feature makes global data collection, UI updates, and logging significantly easier and more intuitive by allowing a one time class level binding.Implementation
HasObservables.__init_subclass__to initialize a distinct_class_subscribersdictionary for every subclass. This ensures that subscribing to a specific Agent subclass doesn't accidentally bind handlers to all other reactive classes.@classmethod subscribe(...)which mirrors the signature of the instance-levelobservemethod but registers the handler globally for the class.create_weakref(handler)for class-level subscribers. This directly addresses the concern raised in the tracking issue by ensuring that the global observer doesn't prevent model instances from being properly garbage-collected during large experiments or parameter sweeps._has_subscribersand_mesa_notifyto seamlessly aggregate and dispatch messages to both instance-level and class-level listeners.test_class_level_subscribetotests/experimental/test_mesa_signals.pyto verify functionality across multiple instances.Usage Examples
You can now easily observe all instances of an agent class without needing to register handlers on them individually:
Additional Notes