I have created the following class, made to manage all instances created and to enable fast lookup and retrieval of specific values by the specified lookup keys:
class InstanceManager[InstanceT, LookupT: Hashable]():
def __init__(
self,
get_lookups: Callable[[InstanceT], Iterable[LookupT]],
on_add: Opt[Callable[[InstanceT], None]] = None,
on_remove: Opt[Callable[[InstanceT], None]] = None
) -> None:
self._on_add = on_add
self._on_remove = on_remove
self._get_lookups = get_lookups
self._all: list[InstanceT] = []
self._lookup: dict[LookupT, InstanceT] = {}
self._LOCK = threading.RLock()
def lookup(self, lookup: LookupT) -> InstanceT:
return self._lookup[lookup] # atomic operation
def all_instances(self) -> list[InstanceT]:
with self._LOCK:
return list(self._all)
def get_lookups(self, instance: InstanceT) -> tuple[LookupT, ...]:
with self._LOCK:
return tuple(k for k, v in self._lookup.items() if v == instance)
def add(self, instance: InstanceT) -> None:
with self._LOCK:
lookup_values = self._get_lookups(instance)
if instance in self._all:
raise ValueError("Instance already in added")
self._all.append(instance)
for lv in lookup_values:
if lv in self._lookup:
raise ValueError("lookup value already used for different instance")
self._lookup[lv] = instance
if self._on_add is not None:
self._on_add
def remove(self, instance: InstanceT):
with self._LOCK:
for k, v in self._lookup.items():
if v == instance:
self._lookup.pop(k)
self._all.remove(instance)
if self._on_remove is not None:
self._on_remove()
Now I wonder if a class using this functionality would benefit more from inheriting from it or by making a class field be the instance manager. I know that inheritance is a "is a" relationship while composition is a "has a" relationship, but I feel like in this case it doesn't really help me answer the question. To consider is also that many classes I would use it for would have other classes they should inherit from, so it would lead to multiple inheritance, which I want to avoid if possible.
To use inheritance, you would need to implement a metaclass (a subclass of type
). Otherwise, all instances of a class inheriting from InstanceManager
would become instances of InstanceManager
themselves. So, without a metaclass, you can only use composition (class attributes and @classmethod
).
Despite that, I think that your abstraction overcomplicates things without providing a much benefit, because the "heavy lifting" needs to be implemented in the class anyway.
__new__()
in order to "capture" all newly created instances.get_loopkup
).Another approach could be to implement an InstanceT
in ignorance of any InstanceManager
and to create a class decorator that creates a subclasses with instance tracking, i.e. adding all the stuff that InstanceManager
does.