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:
Acting
and Observing
, I would like to shortcut this by just checking if it is Agent
.Agent
but still have the isinstance
check the actual class and not use the __instancecheck__
from AgentMeta
.Is this possible? Are metaclasses overkill here?
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)