Why isn't __instancecheck__ being called?

杀马特。学长 韩版系。学妹 提交于 2019-12-04 08:08:40

The isinstance function makes a quick check to see if the type of the instance supplied as an argument is the same as that of the class. If so, it returns early and doesn't invoke your custom __instancecheck__.

This is an optimization used in order to avoid an expensive call to __instancecheck__ (it's Pythonland code) when it isn't required.

You can see the specific test in PyObject_IsInstance, the function that handles the isinstance call in the CPython implementation:

/* Quick test for an exact match */
if (Py_TYPE(inst) == (PyTypeObject *)cls)
    return 1;

Of course, your __instancecheck__ fires correctly when that test isn't True:

>>> isinstance(2, A)
doing instance check
<class '__main__.A'>
2
False

I am not certain if this is implementation specific, I would of thought so, though, since there's no reference to this in the corresponding PEP section nor in the documentation on isinstance.


Interesting aside: issubclass actually doesn't behave this way. Due to its implementation it always calls __subclasscheck__. I had opened an issue on this a while back which is still pending.

Jim's answer seems to nail it.

But for whoever needs for some weid reason a fully customized instancheck (ok, now that I am writing this, there seems to be no correct reason for one to want that, let s hope I am wrong), a metaclass can get away with it, but it is tricky.

This one dynamically replaces the actual class of the object being instantiated by a "shadow class", that is a clone of the original. This way, the native "instancheck" always fail, and the metaclass one is called.

def sub__new__(cls, *args, **kw):
    metacls = cls.__class__
    new_cls = metacls(cls.__name__, cls.__bases__, dict(cls.__dict__), clonning=cls)
    return new_cls(*args, **kw)

class M(type):
    shadows = {}
    rev_shadows = {}
    def __new__(metacls, name, bases, namespace, **kwd):
        clonning = kwd.pop("clonning", None)
        if not clonning:
            cls = super().__new__(metacls, name, bases, namespace)
            # Assumes classes don't have  a `__new__` of them own.
            # if they do, it is needed to wrap it.
            cls.__new__ = sub__new__
        else:
            cls = clonning
            if cls not in metacls.shadows:
                clone = super().__new__(metacls, name, bases, namespace)
                # The same - replace for unwrapped new.
                del clone.__new__
                metacls.shadows[cls] = clone
                metacls.rev_shadows[clone] = cls
            return metacls.shadows[cls]

        return cls

    def __setattr__(cls, attr, value):

        # Keep class attributes in sync with shadoclass
        # This could be done with 'super', but we'd need a thread lock
        # and check for re-entering.
        type.__setattr__(cls, attr, value)
        metacls = type(cls)
        if cls in metacls.shadows:
            type.__setattr__(metacls.shadows[cls], attr, value)
        elif cls in metacls.rev_shadows:
            type.__setattr__(metacls.rev_shadows[cls], attr, value)    

    def call(cls, *args, **kw):
        # When __new__ don't return an instance of its class,
        # __init__ is not called by type's __call__
        instance = cls.__new__(*args, **kw)
        instance.__init__(*args, **kw)
        return instance

    def __instancecheck__(cls, other):
        print("doing instance check")
        print(cls)
        print(other)
        return False


class A(metaclass=M):
    pass

print(type(A))
print(isinstance(A(), A))

It even has a mechanism do sync attributes in the shadow class and actual class. The one thing it does not support is if classes handled in this way do implement a custom __new__. If such a __new__ makes use of parameterless super, it starts to become tricky, as the parameter to super would not be the shadow class.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!