PythonProgrammazioneSviluppatore Python Senior

Quale specifico metodo dunder definito su una **metaclasse Python** intercetta le chiamate `isinstance()` mirate alle sue istanze, e quale pericolo di ricorsione infinita sorge se questo metodo accede a attributi arbitrari sull'oggetto candidato?

Supera i colloqui con l'assistente IA Hintsage

Risposta alla domanda.

Quando isinstance(obj, cls) viene invocato, Python controlla se type(cls) definisce __instancecheck__(self, instance). Se presente, questo metodo di metaclasse determina l'appartenenza, consentendo il "sottoclassamento virtuale" senza eredità. Il pericolo sorge quando l'implementazione utilizza hasattr() o l'accesso agli attributi a punto su obj; se obj implementa __getattr__ o descrittori che attivano controlli isinstance(), il metodo della metaclasse rientra, causando una ricorsione illimitata.

Situazione della vita reale

Abbiamo progettato un framework di validazione dove i plugin dovevano soddisfare le interfacce senza eredità esplicita. Abbiamo definito un ABC Processor e desideravamo che isinstance(plugin, Processor) avesse successo se plugin esponeva un metodo process, supportando il duck typing per le librerie esterne.

Il primo approccio richiedeva che tutti i plugin ereditassero da Processor o chiamassero Processor.register(). Questo era sicuro per i tipi ma impraticabile per il nostro caso d'uso; vietava le classi generate a runtime e richiedeva la modifica di codice di terze parti che non potevamo cambiare. Di conseguenza, non supportava la scoperta dinamica dei plugin da fonti non fidate.

Il secondo approccio implementava __instancecheck__ sulla metaclasse Processor utilizzando hasattr(candidate, 'process'). Sebbene flessibile, questo si bloccava con un RecursionError quando un plugin utilizzava un decoratore di proprietà che convalidava il suo tipo di ritorno tramite isinstance, invocando di nuovo il metodo della metaclasse prima che il primo chiamata restituisse.

Abbiamo adottato una terza soluzione: implementare __instancecheck__ utilizzando object.__getattribute__ per bypassare la logica dei descrittori. Controllando type(candidate).__dict__.get('process') e verificando che sia chiamabile, abbiamo evitato di attivare effetti collaterali di __getattr__ o proprietà definite dall'utente. Questo ha eliminato il rischio di ricorsione mantenendo il duck typing dinamico, permettendo al framework di integrare in sicurezza migliaia di plugin eterogenei senza modifica del codice sorgente.

class MetaProcessor(type): def __instancecheck__(cls, candidate): # Sicuro: bypassa __getattr__ e descrittori try: attr = type(candidate).__dict__.get('process') return callable(attr) except Exception: return False class Processor(metaclass=MetaProcessor): pass

Cosa spesso manca ai candidati


Perché isinstance() consulta la metaclasse del secondo argomento piuttosto che del primo?

Il protocollo assegna l'autorità al tipo che viene controllato (cls), non all'oggetto candidato (obj). Posizionando __instancecheck__ su type(cls), Python garantisce che solo la definizione della classe controlli la sua semantica di appartenenza. Questo impedisce agli oggetti di simulare controlli di istanza; il tipo definisce unilateralmente cosa costituisce la sua istanza, mantenendo l'integrità nei controlli di tipo sensibili alla sicurezza.


Qual è la relazione tra __instancecheck__ e __subclasscheck__, e perché devono rimanere coerenti?

__instancecheck__ valida gli oggetti per isinstance(), mentre __subclasscheck__ valida i tipi per issubclass(). Se __instancecheck__ accetta istanze virtuali (oggetti che non ereditano dalla classe), __subclasscheck__ deve tipicamente accettare le corrispondenti sottoclassi virtuali per mantenere l'invariante che isinstance(obj, cls) implica issubclass(type(obj), cls). Violare questo causa il fallimento del codice del contenitore generico quando controlla le relazioni di sottoclasse dopo che i controlli delle istanze sono passati.


Come l'utilizzo di getattr() all'interno di __instancecheck__ attiva specificamente la ricorsione infinita rispetto a object.__getattribute__?

getattr(obj, 'name') attiva l'intero protocollo dei descrittori e potrebbe chiamare obj.__getattr__('name') se l'attributo è mancante. Se obj.__getattr__ implementa il caricamento pigro, la registrazione o la coercizione di tipo che chiama internamente isinstance(obj, OurClass), la metaclasse __instancecheck__ rientra prima di restituire. Al contrario, object.__getattribute__(obj, 'name') (o ispezionando type(obj).__dict__) bypassa completamente __getattr__ e i descrittori personalizzati, accedendo ai dettagli di implementazione grezzi senza attivare codice utente che potrebbe ricorsivamente.