Python : contextmanagers zijn niet alleen om bestanden te openen

Contextmanagers zorgen ervoor dat Python kan garanderen dat een resource wordt opgeruimd, of de code in het blok nu slaagt of faalt. Een technisch artikel legt hun werking uit onder de instructie with, en verschillende manieren om ze te bouwen en te gebruiken, veel verder dan alleen bestandsbeheer.

Veel ontwikkelaars leren dit mechanisme kennen met with open(path), dat het bestand automatisch sluit. Volgens de auteur zijn bestanden echter het minst interessante geval: de with-instructie markeert vooral een blok waarvan de uitgang gegarandeerd is. Het protocol steunt op twee methoden, __enter__(), aangeroepen bij het betreden van het blok, en __exit__(), aangeroepen bij het verlaten, of dat nu normaal is, veroorzaakt door een return of door een uitzondering.

De waarde die __exit__() teruggeeft, speelt een beslissende rol. Als die waar is, wordt de uitzondering onderdrukt, wat onopgemerkt kan blijven: het programma gaat verder en de fout verdwijnt spoorloos. Als die False is of afwezig, stijgt de uitzondering op na de opruiming. Daarom geven de contextmanagers die in het artikel worden gepresenteerd nooit een waarachtige waarde terug.

Je eigen contextmanagers bouwen

Een klasse is geschikt wanneer de contextmanager een toestand moet bijhouden die de aanroeper kan uitlezen. Het artikel geeft het voorbeeld van een timer, die de duur van een blok meet dankzij time.perf_counter() en het resultaat toont zelfs als het blok faalt. Een onderbroken kenmerkenextractie toont zo « feature extraction: 12.418s (failed) », voordat de fout opstijgt.

Voor een lineaire taak, dat wil zeggen een opzet, een blok en dan een herstel, laat de decorator @contextmanager toe om een eenvoudige generatorfunctie te schrijven. Alles voor de yield wordt uitgevoerd bij het betreden, alles erna bij het verlaten. Het artikel raadt aan het herstel in een finally te plaatsen, en nooit de uitzondering te ‘slikken’ in een try/except zonder ze opnieuw te gooien. Slechts één waarde moet door de yield worden geproduceerd, anders wordt een RuntimeError opgegooid.

De oorspronkelijke staat herstellen en meerdere resources beheren

De standaardbibliotheek biedt al verschillende contextmanagers. TemporaryDirectory() creëert een map met een unieke naam en verwijdert die aan het einde van het blok, wat restbestanden van een vorige uitvoering voorkomt. Sinds Python 3.12 laat de parameter delete=False toe de bestanden te bewaren voor latere analyse. Onder Windows kan een bestand dat open blijft de verwijdering doen mislukken; de parameter ignore_cleanup_errors=True, beschikbaar sinds Python 3.10, verzacht dit probleem zonder het op te lossen.

Voor wijzigingen aan dictionaries herstelt patch.dict(), afkomstig van de module unittest.mock, de oorspronkelijke staat na het blok: een bestaande sleutel krijgt zijn oude waarde terug en een afwezige sleutel wordt verwijderd. Het artikel benadrukt echter dat os.environ globaal is voor het proces. Alle taken en threads zien dan de wijziging, wat het riskant maakt in gelijktijdige code.

Wanneer het aantal resources pas tijdens de uitvoering bekend is, bijvoorbeeld het aantal te mergen bestanden, groepeert ExitStack de contextmanagers. Zijn methode enter_context() registreert een contextmanager, en callback() een eenvoudige functie. Bij het verlaten worden de elementen in omgekeerde volgorde van de stapel gehaald. Het artikel preciseert dat dit afstapelen gebeurt aan het einde van het blok of bij een aanroep van close(), en niet bij de verwijdering van het object door de garbage collector. Een toegewijde klasse kan beter zijn wanneer de stapel te zwaar wordt.