pythoninheritancemetaclassisinstance

Metaclass isinstance not working as expected


I've got the following class hiearchy:

class Acting: pass

class Observing: pass

class AgentMeta(type):
    def __instancecheck__(self, instance: Any) -> bool:
        return isinstance(instance, Observing) and isinstance(instance, Acting)

class Agent(Acting, Observing, metaclass=AgentMeta): pass

class GridWorld: pass

class GridMoving(Acting, GridWorld): pass

class GridObserving(Observing, GridWorld): pass

class Blocking(GridMoving, GridObserving): pass

class Broadcasting(Agent, GridWorld): pass

I set it up this way so that I can check this

isinstance(Blocking(), Agent) -> True

instead of this

isinstance(Blocking(), Observing) and isinstance(Blocking(), Acting)

However, now I have the undesired side effect that Blocking is an instance of Broadcasting.

isinstance(Blocking(), Broadcasting) -> True

My understanding is that this is because Braodcasting inherits from Agent, which has the modification on the __instancecheck__ from the metaclass. I don't really understand metaclasses well enough to come up with an elegant solution, but I would like to do the following:

  1. When I want to check if something is both Acting and Observing, I would like to shortcut this by just checking if it is Agent.
  2. I would like to be able to inherit from Agent but still have the isinstance check the actual class and not use the __instancecheck__ from AgentMeta.

Is this possible? Are metaclasses overkill here?


Solution

  • Yes. What you write in a metaclass method is just plain Python code, so, in this case, the only thing you need, is to fine-tune your code inside the __instancecheck__ method, by using more checks until you get your desired effect matched.

    Currently, all that you require is that the instance being check inherit from two other classes - very well.

    What you require, for example, is that it inherit from the two other classes, but if the class being checked against itself (the first parameter in __instancecheck__ is not the first class in the inheritance chain to have AgentMeta as the metaclass, it should fail). (That is what I got from your text - your actual needs may need some further fine tuning)

    So, I suggest improving the code in __instancecheck__ to this:

    class AgentMeta(type):
        def __instancecheck__(cls, instance: Any) -> bool:
            if not (isinstance(instance, Observing) and isinstance(instance, Acting)):
                return False
            agent_meta_count = sum(isinstance(parent_cls, __class__) for parent_cls in cls.__mro__)
            return agent_meta_count == 1
    
    

    I used a shortened expression which might look confusing - but basically what sum(isinstance(parent_cls, __class__) for parent_cls in cls.__mro__) does is: walk all superclasses of the class that is being asked for, including itself. Sum the truth values when that class is an AgentMeta, i.e.: if AgentMeta is the metaclass of that class, count 1, else count 0. If this count differs from 1, then the class being checked is derived from the class that first had metaclass=AgentMeta in the declaration, and you want the "instancecheck" to return False.

    Another option is to leave __instancecheck__ as is in your code, and simply revert and not apply the metaclass for derived classes of the first AgentMeta implementing class. This can be done by writting the metaclass __new__ method instead:

    class AgentMeta(type):
        def __new__(mcls, name, bases, ns, **kw):
            if any(isinstance(base, mcls) for base in bases):
                # if any base is already an  "AgentMeta",
                # create the class as a regular "type" instance, skipping this metaclass:
                return type(name, bases, ns, **kw) # <- note that "mcls" is discarded in this call
            return super().__new__(mcls, name, bases, ns, **kw)
                
        
        def __instancecheck__(self, instance: Any) -> bool:
            return isinstance(instance, Observing) and isinstance(instance, Acting)