Geschiedenis van de vraag
In Python's datamodel volgt attribuuttoegang een strikt protocol waarbij __getattribute__ is gedefinieerd op de basis object klasse en dient als de primaire interceptor voor elke attribuutopzoeking. Deze methode wordt onvoorwaardelijk aangeroepen voor alle attribuuttoegang, zowel bestaande als niet-bestaande, waardoor het de eerste verdedigingslinie is in de resolutieketen. In tegenstelling tot __getattr__, dat een optionele haak is die de interpreter alleen aanroept wanneer de normale zoektocht in de instantie-dictionary en de klasse-hiërarchie niet in staat is om de gevraagde naam te vinden.
Het probleem
Wanneer een subclass __getattribute__ overschrijft om gedrag aan te passen zoals logging of toegangscontrole, triggert elke directe attribuuttoegang binnen het methodenlichaam—zoals self.attr of self.__dict__—dezelfde overschreven methode recursief. Dit creëert een oneindige lus omdat het opzoekmechanisme is gekidnapt zonder een basisgeval om de recursie te beëindigen, wat uiteindelijk de call stack uitput en een RecursionError genereert.
De oplossing
Om __getattribute__ veilig te implementeren, moet je delegeren naar de basisimplementatie met super().__getattribute__(name) of object.__getattribute__(self, name). Dit omzeilt de overschreven logica en voert de daadwerkelijke attribuutophaling uit van de instantie-dictionary of klasse-hiërarchie zonder het aangepaste method te herbetreden. Het patroon zorgt ervoor dat je het resultaat kunt wikkelen, valideren of transformeren terwijl de integriteit van het objectmodel wordt behouden en oneindige lussen worden voorkomen.
Codevoorbeeld
class SafeProxy: def __init__(self, wrapped): # Moet super() hier gebruiken om recursie tijdens initialisatie te vermijden super().__setattr__('_wrapped', wrapped) def __getattribute__(self, name): # Log de toegang voordat deze wordt opgehaald print(f"Toegang: {name}") # Delegeer aan object om oneindige recursie te vermijden return super().__getattribute__(name)
Scenario
Een ontwikkelingsteam moet een audit-trail implementeren voor een legacy ORM-model waarbij elke veldtoegang moet worden gelogd om te voldoen aan de compliance-eisen zonder de oorspronkelijke modelklassen te wijzigen. Ze hebben een oplossing nodig die transparant leest onderschept om te voorkomen dat bestaande bedrijfslogica in honderden modules wordt verbroken.
Probleembeschrijving
Het systeem vereist het onderscheppen van zowel bestaande als ontbrekende attributen om tijdstempels en gebruikersacties vast te leggen. Gewoon subclassing en logging aan individuele methoden toevoegen is niet haalbaar vanwege het grote aantal dynamische velden. De oplossing moet transparant zijn voor bestaande code en mag de publieke interface van de modellen niet wijzigen.
Oplossing 1: Monkey-patching van de modelmethoden
Deze aanpak omvat het dynamisch vervangen van methoden op de klasse tijdens runtime om logboekoproepen in te voegen, gericht op specifieke gedragingen zonder brondefinities te wijzigen. Het staat voorwaardelijke toepassing toe op basis van configuratie en voorkomt complicaties met overerving. Het kan echter geen directe attribuuttoegang tot gegevensdescriptors of eenvoudige waarden onderscheppen, vereist onderhoud voor elke nieuwe methode en breekt wanneer interne implementatiedetails veranderen.
Oplossing 2: Gebruik van __getattr__ voor logging
Het implementeren van __getattr__ om toegang tot ontbrekende attributen te loggen biedt alleen een eenvoudige fallback-mechanisme. Het is veilig van recursieproblemen en gemakkelijk te implementeren met minimale boilerplate. Helaas, het triggert alleen voor attributen die niet in de instantie of klasse zijn gevonden, waardoor de meeste toegang tot bestaande velden wordt gemist, wat de auditvereiste voor uitgebreide logging ondermijnt.
Oplossing 3: Proxyklasse met __getattribute__
Het creëren van een wrapperklasse die __getattribute__ implementeert, onderschept alle attribuutlezen voordat het delegeert aan de gewikkelde ORM-instantie, waardoor elke toegang uniform wordt vastgelegd. Dit behoudt transparantie via compositie en staat voor- en naverwerking toe zonder de legacy-code aan te raken. De trade-off is de vereiste voor zorgvuldige recursiebehandeling en een kleine prestatie-overhead door de extra methode-aanroep bij elke attribuuttoegang.
Gekozen oplossing
Het team koos voor de proxybenadering met __getattribute__ omdat de nalevingsregelingen vereisten dat elke attribuutlezen werd vastgelegd, inclusief eenvoudige data-velden die methoden nooit aanraakt. Het proxy-patroon biedt volledige onderscheppingsmogelijkheden en behoudt de encapsulatie, waardoor de legacy ORM ongerept en zich niet bewust van de auditlaag blijft. Deze keuze offerde minimale prestaties op voor uitgebreide dekking en auditintegriteit.
Resultaat
De implementatie heeft met succes meer dan 50.000 attribuuttoegang per uur in productie gelogd zonder een enkele recursiefout of wijziging aan de legacy-codebase. Het delegatiepatroon met super() zorgde voor een stabiele werking, en de proxy kon in testomgevingen worden uitgeschakeld door eenvoudig de wrapper-instantie te verwijderen, wat de flexibiliteit van de aanpak aantoont.
Waarom triggert toegang tot self.__dict__ binnen __getattribute__ oneindige recursie?
Wanneer je self.__dict__ schrijft binnen een overschreven __getattribute__-methode, moet Python het attribuut met de naam __dict__ op de instantie opzoeken. Deze opzoeking roept je aangepaste __getattribute__-methode weer aan, die opnieuw probeert self.__dict__ te benaderen, wat een eindeloze cyclus creëert. Om deze lus te doorbreken, moet je object.__getattribute__(self, '__dict__') gebruiken, wat je overschrijving omzeilt en de dictionary rechtstreeks uit de basisobjectimplementatie haalt.
Hoe beïnvloedt __getattribute__ descriptorprotocollen anders dan __getattr__?
__getattribute__ zit aan het begin van de attribuutresolutie-keten, wat betekent dat het opzoekingen onderschept voordat het descriptorprotocol zoekt naar __get__-methoden. Als je implementatie een waarde retourneert zonder te delegeren naar super(), worden descriptors zoals property of aangepaste gegevensdescriptors volledig omzeild. In tegenstelling, __getattr__ voert alleen uit nadat zowel het descriptorprotocol als de instantie-dictionary-opzoeking hebben gefaald, dus het onderschept nooit descriptors die in de klasse-hiërarchie bestaan.
Wat is de consequentie van het handmatig opwekken van AttributeError binnen __getattribute__?
In tegenstelling tot standaard attribuuttoegang waarbij een AttributeError __getattr__ kan triggeren als fallback, beschouwt Python __getattribute__ als de gezaghebbende bron. Als je aangepaste implementatie AttributeError oplevert, verspreidt de interpreter de uitzondering onmiddellijk zonder te proberen __getattr__ aan te roepen. Dit betekent dat je niet kunt rekenen op __getattr__ om ontbrekende attributen af te handelen als je primaire haak mislukt; in plaats daarvan moet je ontbrekende sleutels binnen __getattribute__ afhandelen of ervoor zorgen dat je delegeert naar de bovenliggende implementatie die de uitzondering correct opwekt.