Kiedy wywoływane jest isinstance(obj, cls), Python sprawdza, czy type(cls) definiuje __instancecheck__(self, instance). Jeśli jest obecna, ta metoda metaklasy określa przynależność, umożliwiając "wirtualne podklasowanie" bez dziedziczenia. Niebezpieczeństwo występuje, gdy implementacja używa hasattr() lub dostępu do atrybutów z kropką na obj; jeśli obj implementuje __getattr__ lub deskryptory, które wywołują sprawdzenia isinstance(), metoda metaklasy wchodzi ponownie, powodując nieograniczoną rekurencję.
Zaprojektowaliśmy framework walidacji, w którym wtyczki musiały zaspokajać interfejsy bez jawnego dziedziczenia. Zdefiniowaliśmy ABC Processor i pragnęliśmy, aby isinstance(plugin, Processor) zakończyło się sukcesem, jeśli plugin udostępniał metodę process, wspierając typowanie kaczki dla zewnętrznych bibliotek.
Pierwsze podejście wymagało od wszystkich wtyczek dziedziczenia z Processor lub wywołania Processor.register(). Było to bezpieczne pod względem typowania, ale niepraktyczne dla naszego przypadku; ograniczało to klasy generowane w czasie wykonywania i wymagało modyfikacji kodu stron trzecich, którego nie mogliśmy zmienić. W rezultacie nie wspierało to dynamicznego odkrywania wtyczek z nieznanych źródeł.
Drugie podejście zaimplementowało __instancecheck__ w metaklasie Processor, używając hasattr(candidate, 'process'). Chociaż było elastyczne, zakończyło się błędem RecursionError, gdy wtyczka używała dekoratora właściwości, który walidował typ zwracany poprzez isinstance, wywołując metodę metaklasy ponownie, zanim pierwszy wywołanie powróciło.
Przyjęliśmy trzecie rozwiązanie: zaimplementowanie __instancecheck__, używając object.__getattribute__, aby ominąć logikę deskryptora. Sprawdzając type(candidate).__dict__.get('process') i weryfikując, że jest to wywoływalne, uniknęliśmy wywoływania użytkownikowego __getattr__ lub skutków ubocznych właściwości. To wyeliminowało ryzyko rekurencji, zachowując dynamiczne typowanie kaczki, co pozwoliło frameworkowi na bezpieczne integrowanie tysięcy heterogenicznych wtyczek bez modyfikacji źródła.
class MetaProcessor(type): def __instancecheck__(cls, candidate): # Bezpieczne: omija __getattr__ i deskryptory try: attr = type(candidate).__dict__.get('process') return callable(attr) except Exception: return False class Processor(metaclass=MetaProcessor): pass
Dlaczego isinstance() konsultuje metaklasę drugiego argumentu, a nie pierwszego?
Protokół przypisuje władzę typowi, przeciw któremu jest sprawdzane (cls), a nie obiektowi kandydata (obj). Umieszczając __instancecheck__ na type(cls), Python zapewnia, że tylko definicja klasy kontroluje jej semantykę przynależności. Zapobiega to obiektom oszukującym sprawdzenia instancji; typ jednostronnie definiuje, co stanowi jego instancję, zachowując integralność w krytycznych pod względem bezpieczeństwa sprawdzeniach typów.
Jakie są relacje między __instancecheck__ a __subclasscheck__, i dlaczego muszą pozostać spójne?
__instancecheck__ weryfikuje obiekty dla isinstance(), podczas gdy __subclasscheck__ weryfikuje typy dla issubclass(). Jeśli __instancecheck__ akceptuje wirtualne instancje (obiekty, które nie dziedziczą z klasy), __subclasscheck__ musi zazwyczaj akceptować odpowiednie wirtualne podklasy, aby zachować inwariant, że isinstance(obj, cls) implikuje issubclass(type(obj), cls). Naruszenie tego powoduje, że ogólny kod kontenerowy nie działa, gdy sprawdza relacje między podklasami po pomyślnym zaliczeniu sprawdzeń instancji.
Jak używanie getattr() wewnątrz __instancecheck__ konkretnie wywołuje nieskończoną rekurencję w porównaniu do object.__getattribute__?
getattr(obj, 'name') wywołuje pełny protokół deskryptorów i może wywołać obj.__getattr__('name'), jeśli atrybut jest brakujący. Jeśli obj.__getattr__ implementuje ładowanie „na żądanie”, logowanie lub rzutowanie typów, które wewnętrznie wywołuje isinstance(obj, OurClass), metoda __instancecheck__ metaklasy wchodzi ponownie przed zwróceniem. W przeciwieństwie do tego, object.__getattribute__(obj, 'name') (lub przeglądanie type(obj).__dict__) całkowicie omija __getattr__ i niestandardowe deskryptory, uzyskując dostęp do surowych szczegółów implementacji bez wywoływania kodu użytkownika, który może powodować rekurencję.