Search code examples
pythoninheritanceinstancecomposition

Python inheritance vs. composition for InstanceManager


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.


Solution

  • 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.

    • Overriding __new__() in order to "capture" all newly created instances.
    • The computation of the lookup key (get_loopkup).
    • Classmethods for accessing the instance manager.

    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.