Когда вызывается isinstance(obj, cls), Python проверяет, определяет ли type(cls) метод __instancecheck__(self, instance). Если он присутствует, этот метод мета-класса определяет членство, позволяя "виртуальное наследование" без наследования. Опасность возникает, когда реализация использует hasattr() или доступ к атрибутам объекта с помощью точечной нотации; если obj реализует __getattr__ или дескрипторы, которые вызывают проверки isinstance(), метод мета-класса повторно входит, что вызывает неограниченную рекурсию.
Мы спроектировали фреймворк валидации, где плагины должны были удовлетворять интерфейсам без явного наследования. Мы определили ABC Processor и хотели, чтобы isinstance(plugin, Processor) возвращал True, если plugin предоставляет метод process, поддерживая duck typing для сторонних библиотек.
Первый подход требовал, чтобы все плагины наследовались от Processor или вызывали Processor.register(). Это было безопасно с точки зрения типов, но непрактично для нашего случая; это запрещало классы, созданные во время выполнения, и требовало изменения стороннего кода, который мы не могли изменить. В итоге это не поддерживало динамическое обнаружение плагинов из ненадежных источников.
Второй подход реализовал __instancecheck__ в мета-классе Processor, используя hasattr(candidate, 'process'). Хотя это было гибко, это завершалось с RecursionError, когда плагин использовал декоратор свойства, который проверял тип возвращаемого значения через isinstance, снова вызывая метод мета-класса перед тем, как первый вызов вернулся.
Мы приняли третье решение: реализовать __instancecheck__, используя object.__getattribute__, чтобы обойти логику дескрипторов. Проверяя type(candidate).__dict__.get('process') и подтверждая, что это вызываемый объект, мы избегали триггеров пользовательского __getattr__ или побочных эффектов свойств. Это устранило риск рекурсии, сохранив динамический duck typing, позволив фреймворку безопасно интегрировать тысячи гетерогенных плагинов без модификации исходного кода.
class MetaProcessor(type): def __instancecheck__(cls, candidate): # Безопасно: обходит __getattr__ и дескрипторы try: attr = type(candidate).__dict__.get('process') return callable(attr) except Exception: return False class Processor(metaclass=MetaProcessor): pass
Почему isinstance() обращается к мета-классу второго аргумента, а не первого?
Протокол назначает полномочия на тип, с которым выполняется проверка (cls), а не на объект-кандидат (obj). Помещая __instancecheck__ на type(cls), Python гарантирует, что только определение класса контролирует свою семантику членства. Это предотвращает подделку проверок экземпляров объектами; тип единолично определяет, что составляет его экземпляр, поддерживая целостность в проверках типов, зависящих от безопасности.
Какова связь между __instancecheck__ и __subclasscheck__, и почему они должны оставаться последовательными?
__instancecheck__ проверяет объекты для isinstance(), в то время как __subclasscheck__ проверяет типы для issubclass(). Если __instancecheck__ принимает виртуальные экземпляры (объекты, не наследующие от класса), __subclasscheck__ должен, как правило, принимать соответствующие виртуальные подклассы, чтобы сохранить инвариант, согласно которому isinstance(obj, cls) подразумевает issubclass(type(obj), cls). Нарушение этого вызывает сбой кода контейнеров, когда он проверяет отношения подклассов после успешных проверок экземпляров.
Как использование getattr() внутри __instancecheck__ конкретно вызывает бесконечную рекурсию по сравнению с object.__getattribute__?
getattr(obj, 'name') вызывает полный протокол дескрипторов и может вызвать obj.__getattr__('name'), если атрибут отсутствует. Если obj.__getattr__ реализует отложенную загрузку, ведение журнала или приведение типов, которые внутренне вызывают isinstance(obj, OurClass), метод мета-класса __instancecheck__ повторно входит перед возвратом. В отличие от этого, object.__getattribute__(obj, 'name') (или исследование type(obj).__dict__) полностью обходит __getattr__ и пользовательские дескрипторы, получая доступ к сырым деталям реализации без триггеров пользовательского кода, который может вызывать рекурсию.