Cuando se invoca isinstance(obj, cls), Python verifica si type(cls) define __instancecheck__(self, instance). Si está presente, este método de metaclase determina la pertenencia, permitiendo una "subclase virtual" sin herencia. El riesgo ocurre cuando la implementación utiliza hasattr() o acceso a atributos con punto en obj; si obj implementa __getattr__ o descriptores que activan comprobaciones isinstance(), el método de metaclase vuelve a entrar, provocando una recursión ilimitada.
Arquitectamos un marco de validación donde los complementos necesitaban satisfacer interfaces sin herencia explícita. Definimos un ABC Processor y deseábamos que isinstance(plugin, Processor) tuviera éxito si plugin expone un método process, apoyando el duck typing para bibliotecas externas.
El primer enfoque requería que todos los complementos heredaran de Processor o llamaran a Processor.register(). Esto era seguro para tipos pero poco práctico para nuestro caso de uso; prohibía clases generadas en tiempo de ejecución y requería modificar código de terceros que no podíamos cambiar. Como resultado, no pudo soportar el descubrimiento dinámico de complementos de fuentes no confiables.
El segundo enfoque implementó __instancecheck__ en la metaclase Processor utilizando hasattr(candidate, 'process'). Aunque flexible, esto falló con RecursionError cuando un complemento utilizó un decorador de propiedad que validaba su tipo de retorno a través de isinstance, invocando nuevamente el método de metaclase antes de que la primera llamada regresara.
Adoptamos una tercera solución: implementar __instancecheck__ utilizando object.__getattribute__ para evitar la lógica de descriptores. Al verificar type(candidate).__dict__.get('process') y verificar que es llamable, evitamos activar __getattr__ definido por el usuario o efectos secundarios de propiedades. Esto eliminó el riesgo de recursión mientras se mantenía el duck typing dinámico, permitiendo al marco integrar de forma segura miles de complementos heterogéneos sin modificación de fuente.
class MetaProcessor(type): def __instancecheck__(cls, candidate): # Seguro: evita __getattr__ y descriptores try: attr = type(candidate).__dict__.get('process') return callable(attr) except Exception: return False class Processor(metaclass=MetaProcessor): pass
¿Por qué isinstance() consulta la metaclase del segundo argumento en lugar del primero?
El protocolo asigna autoridad al tipo que se está verificando (cls), no al objeto candidato (obj). Al colocar __instancecheck__ en type(cls), Python asegura que solo la definición de clase controle su semántica de pertenencia. Esto evita que los objetos engañen las comprobaciones de instancia; el tipo unilateralmente define qué constituye su instancia, manteniendo la integridad en verificaciones de tipo sensibles a la seguridad.
¿Cuál es la relación entre __instancecheck__ y __subclasscheck__, y por qué deben permanecer consistentes?
__instancecheck__ valida objetos para isinstance(), mientras que __subclasscheck__ valida tipos para issubclass(). Si __instancecheck__ acepta instancias virtuales (objetos que no heredan de la clase), __subclasscheck__ debe aceptar típicamente las subclases virtuales correspondientes para preservar la invariante de que isinstance(obj, cls) implica issubclass(type(obj), cls). Violando esto, el código de contenedor genérico fallará cuando verifique las relaciones de subclases después de que las comprobaciones de instancia pasen.
¿Cómo hace que el uso de getattr() dentro de __instancecheck__ despierte específicamente una recursión infinita en comparación con object.__getattribute__?
getattr(obj, 'name') invoca el protocolo completo de descriptores y puede llamar a obj.__getattr__('name') si falta el atributo. Si obj.__getattr__ implementa carga diferida, registro o coerción de tipos que internamente llama a isinstance(obj, OurClass), el metaclase __instancecheck__ regresa antes de regresar. En contraste, object.__getattribute__(obj, 'name') (o inspeccionar type(obj).__dict__) evita completamente __getattr__ y descriptores personalizados, accediendo a detalles de implementación sin activar código del usuario que podría recursar.