Почему не вызывается __instancecheck__?

У меня есть следующий код python3:

class BaseTypeClass(type):
    def __new__(cls, name, bases, namespace, **kwd):
        result = type.__new__(cls, name, bases, namespace)
        print("creating class '{}'".format(name))
        return result

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


class A(metaclass=BaseTypeClass):
    pass

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

и когда я запускаю его на Python 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32, я получаю следующий вывод

creating class 'A'
<class '__main__.BaseTypeClass'>
True

Почему он не выводит doing instance check? В документации говорится, что метод __instancecheck__ требует быть определен в метаклассе, а не в самом классе, что я сделал здесь. Я даже проверяю, используется ли метакласс, так как печатается creating class 'A'. Однако, когда я вызываю isinstance, он использует реализацию по умолчанию, а не ту, которую я определил в метаклассе.

Я, вероятно, неправильно использую метаклассы, но я не могу понять, где я допустил ошибку.


person drdrez    schedule 10.12.2017    source источник
comment
давайте попробуем обойти эту деталь реализации...   -  person jsbueno    schedule 11.12.2017
comment
как странное примечание, если вы определяете __instancecheck__ как метод в обычном классе, экземпляры класса могут использоваться в качестве второго аргумента isinstance   -  person drdrez    schedule 14.12.2017


Ответы (2)


Функция isinstance быстро проверяет, совпадает ли тип экземпляра, переданного в качестве аргумента, с типом класса. Если это так, он возвращается раньше и не вызывает ваш пользовательский __instancecheck__.

Это оптимизация, используемая для того, чтобы избежать дорогостоящего вызова __instancecheck__ (это код Pythonland), когда это не требуется.

Вы можете увидеть конкретный тест в PyObject_IsInstance, функция, которая обрабатывает вызов isinstance в реализации CPython:

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

Конечно, ваш __instancecheck__ срабатывает правильно, если этот тест не True:

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

Я не уверен, что это зависит от реализации, хотя я бы так и подумал, поскольку в соответствующий раздел PEP и документацию по isinstance.


Интересное в стороне: issubclass на самом деле так себя не ведет. Из-за своей реализации он всегда вызывает __subclasscheck__. Некоторое время назад я открыл проблему, которая все еще находится на рассмотрении.

person Dimitris Fasarakis Hilliard    schedule 10.12.2017
comment
Блин, я должен был догадаться, что это так, и попробовать проверить это с несколькими другими случаями. Спасибо! - person drdrez; 10.12.2017
comment
Хм. Кажется, это противоречит документации. документы по модели данных на __instancecheck__ и __subclasscheck__ тоже не упоминайте этот случай. Я чувствую, что проверку if (Py_TYPE(inst) == (PyTypeObject *)cls) следует переместить в блок if (PyType_CheckExact(cls)); это будет лучше соответствовать документам и, вероятно, не сильно замедлит работу, хотя я не проверял его. - person user2357112 supports Monica; 10.12.2017
comment
Это странная ситуация, потому что есть несоответствие между __instancecheck__ и __subclasscheck__ @user2357112 . Некоторое время назад я открыл проблему, где говорил об их различии (которое я так и не нашел опубликовать на python-dev еще). - person Dimitris Fasarakis Hilliard; 10.12.2017

Ответ Джима кажется верным.

Но для тех, кто нуждается по каким-то веским причинам полностью настроенной инстанчек (хорошо, теперь, когда я пишу это, кажется, что у кого-то нет правильной причины хотеть этого, будем надеяться, что я ошибаюсь), метаклассу это может сойти с рук, но это сложно.

Этот класс динамически заменяет фактический класс создаваемого объекта «теневым классом», который является клоном оригинала. Таким образом, нативная «инстанчек» всегда терпит неудачу, и вызывается метакласс.

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))

У него даже есть механизм синхронизации атрибутов в теневом классе и реальном классе. Единственное, что он не поддерживает, это если классы, обработанные таким образом, действительно реализуют пользовательский __new__. Если такой __new__ использует super без параметров, это становится сложным, поскольку параметр super не будет теневым классом.

person jsbueno    schedule 11.12.2017