Ga naar hoofdinhoud

Aangepaste contextmanagers schrijven in Python

Leer de geavanceerde aspecten van resourcebeheer in Python door te beheersen hoe je aangepaste contextmanagers schrijft.
Bijgewerkt 2 okt 2026

Verken met AI

ChatGPTClaudePerplexity

Motivatie

Je kent waarschijnlijk al de with-instructie, een beknopte manier om resources in Python te beheren. Maar heb je je ooit afgevraagd hoe dit onder de motorkap werkt? De kracht van het sleutelwoord with komt van contextmanagers.

Contextmanagers zijn een fundamenteel ontwerppatroon in Python dat een gestructureerde aanpak biedt voor resourcebeheer. Ze zorgen ervoor dat resources worden verkregen, correct gebruikt en uiteindelijk worden vrijgegeven of opgeruimd, zelfs bij fouten of uitzonderingen.

Dit is vooral belangrijk bij resources zoals bestanden, netwerkverbindingen of databasehandles. Door contextmanagers te gebruiken, kunnen we schonere en betrouwbaardere code schrijven, zonder ons zorgen te maken dat we vergeten een bestand te sluiten of een lock vrij te geven.

In deze tutorial gaan we verder dan de standaard contextmanagers van Python en leren we onze eigen te schrijven.

Contextmanagers in Python begrijpen

Onder de motorkap zijn contextmanagers objecten die twee speciale methoden definiëren: __enter__ en __exit__. De methode __enter__ wordt aangeroepen wanneer je de with-blok binnenkomt, en de teruggegeven waarde wordt binnen dat blok aan een variabele toegewezen. De methode __exit__ wordt daarentegen aangeroepen wanneer het with-blok wordt verlaten, ongeacht of het normaal eindigt of met een uitzondering.

Deze structuur zorgt voor correct resourcebeheer. Laten we naar enkele ingebouwde contextmanagers in Python kijken om dit beter te begrijpen:

1. Bestandsbeheer

Neem het klassieke voorbeeld van het openen van een bestand:

with open('file.txt', 'r') as file:
   data = file.read()

Hier fungeert open('file.txt', 'r') als de contextmanager. Wanneer je het with-blok binnenkomt, wordt de enter-methode van het bestandobject aangeroepen, waarmee het bestand wordt geopend en toegewezen aan de variabele file. Je kunt vervolgens file.read() gebruiken om de inhoud van het bestand te lezen.

Vervolgens wordt gegarandeerd dat de __exit__-methode van het bestandobject wordt aangeroepen wanneer het with-blok wordt verlaten, zelfs als er een uitzondering optreedt. Deze methode zorgt ervoor dat het bestand wordt gesloten, zodat je geen open filehandles achterlaat.

2. Thread-locking

Naast bestanden kunnen contextmanagers ook worden gebruikt voor thread-synchronisatie met threading.Lock():

import threading

lock = threading.Lock()
with lock:
   # Critical section
   print("This code is executed under lock protection.")

Hier is lock een threading.Lock-object, opnieuw een contextmanager. Wanneer je het with-blok binnenkomt, wordt de __enter__-methode gebruikt om de lock te verkrijgen, zodat slechts één thread tegelijk de kritieke sectie kan uitvoeren. Tot slot geeft de __exit__-methode de lock vrij wanneer je het with-blok verlaat, zodat andere threads verder kunnen.

3. Databaseverbindingen

Op dezelfde manier kunnen contextmanagers databaseverbindingen beheren:

import sqlite3

with sqlite3.connect('database.db') as connection:
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM table")
    rows = cursor.fetchall()

De aanroep sqlite3.connect('database.db') is een contextmanager. Het betreden van het with-blok maakt een verbinding met de database en wijst die toe aan connection. Je kunt vervolgens een cursor gebruiken om met de database te werken. De __exit__-methode garandeert dat de verbinding wordt gesloten wanneer het with-blok wordt verlaten, zodat er geen resources lekken.

4. Netwerksockets

Contextmanagers kunnen zelfs netwerkcommunicatie afhandelen:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(('localhost', 8080))
    s.sendall(b'Hello, world')
    data = s.recv(1024)

Hier maakt socket.socket(socket.AF_INET, socket.SOCK_STREAM) een socketobject dat fungeert als contextmanager. De __enter__-methode maakt de socket aan, en binnen het with-blok kun je verbinden, data versturen en ontvangen. De __exit__-methode zorgt ervoor dat de socket netjes wordt gesloten wanneer je klaar bent.

5. Mappen scannen

Dit keer biedt os.scandir('.') een manier om over mapitems te itereren:

with os.scandir('.') as entries:
   for entry in entries:
       print(entry.name)

os.scandir('.') fungeert hier als contextmanager. De __enter__-methode start een mapscan, en je kunt binnen het with-blok over de items itereren. De __exit__-methode ruimt de mapscan op bij het verlaten.

Zoals je ziet vereenvoudigt de with-instructie, mogelijk gemaakt door contextmanagers, het resourcebeheer door automatisch allocatie en vrijgave af te handelen.

Aangepaste contextmanagers in Python schrijven

Laten we nu eindelijk kijken hoe je je eigen managers schrijft om fijnmazige controle over resourcebeheer te krijgen.

Er zijn twee hoofdbenaderingen om aangepaste contextmanagers te schrijven: op basis van een klasse en op basis van een functie.

Klasse-gebaseerde benadering

De klasse-gebaseerde benadering is de meest gestructureerde en flexibele methode om contextmanagers te schrijven. Hierbij definieer je een klasse die de speciale methoden __enter__ en __exit__ implementeert.

Laten we kijken naar een voorbeeld van een Timer-klasse die de uitvoeringstijd meet:

import time


class Timer:
   def __enter__(self):
       self.start_time = time.time()
       return self

   def __exit__(self, exc_type, exc_value, traceback):
       self.end_time = time.time()
       elapsed_time = self.end_time - self.start_time
       print(f"Elapsed time: {elapsed_time} seconds")


# Example usage
if __name__ == "__main__":
   with Timer() as timer:
       # Code block to measure the execution time
       time.sleep(2)  # Simulate some time-consuming operation
Elapsed time: 2.002082347869873 seconds

Hier definieert de klasse Timer de methode __enter__ om de starttijd vast te leggen wanneer je het with-blok binnenkomt. Hij retourneert self zodat je binnen het blok toegang hebt tot het object. De methode __exit__ berekent de verstreken tijd bij het verlaten van het with-blok en print deze.

Functie-gebaseerde benadering

Als je de voorkeur geeft aan een beknoptere aanpak, mogelijk ten koste van flexibiliteit, dan is de functie-gebaseerde benadering een goede optie. Hierbij gebruik je de decorator contextmanager uit de module contextlib om elke functie in een contextmanager te veranderen.

Laten we zien hoe dat werkt:

import time
from contextlib import contextmanager


@contextmanager
def timer():
   start_time = time.time()
   yield
   end_time = time.time()
   elapsed_time = end_time - start_time
   print(f"Elapsed time: {elapsed_time} seconds")


# Example usage
if __name__ == "__main__":
   with timer():
       time.sleep(2)
Elapsed time: 2.0020740032196045 seconds

De decorator @contextmanager transformeert de functie timer in een contextmanager. In de functie wordt start_time vastgelegd, en de yield-instructie pauzeert de uitvoering, zodat code binnen het with-blok kan draaien.

Tot slot wordt de functionaliteit van __exit__ bereikt door de eindtijd vast te leggen en de verstreken tijd te printen.

Kort gezegd: je schrijft de logica voor __enter__ vóór het sleutelwoord yield, terwijl de logica voor __exit__ erna komt. Beide benaderingen bereiken hetzelfde resultaat; de keuze hangt af van je voorkeur voor structuur en leesbaarheid.

Praktische voorbeelden van contextmanagers

Nu je begrijpt hoe je aangepaste contextmanagers in Python schrijft, kijken we naar enkele praktische voorbeelden waarin ze enorm handig zijn.

Bestandsbewerkingen beheren

Bestandsbewerkingen komen vaak voor in veel applicaties. Of je nu leest uit of schrijft naar bestanden, goed beheer zorgt voor dataintegriteit en efficiënt gebruik van resources. Laten we een scenario bekijken waarin we tekst naar een bestand willen schrijven met een aangepaste contextmanager.

class FileManager:
   def __init__(self, filename, mode):
       self.filename = filename
       self.mode = mode
       self.file = None

   def __enter__(self):
       self.file = open(self.filename, self.mode)
       return self.file

   def __exit__(self, exc_type, exc_value, traceback):
       if self.file:
           self.file.close()


# Example usage
if __name__ == "__main__":
   with FileManager("example.txt", "w") as file:
       file.write("Hello, world!\n")

In dit voorbeeld definiëren we een klasse FileManager om de functie open in Python na te bootsen die bestandsbewerkingen afhandelt. Laten we bekijken wat elke methode doet:

  • __init__(self, filename, mode): Deze methode is de constructor van de klasse. Hij initialiseert het FileManager-object met de opgegeven bestandsnaam en modus, die aangeven welk bestand geopend moet worden en in welke modus (bijv. lezen, schrijven, toevoegen).
  • __enter__(self): Deze methode wordt, zoals altijd, aangeroepen bij het betreden van het with-blok. Hij opent het bestand dat is opgegeven door filename in de modus die is opgegeven door mode. Het geopende bestandobject wordt toegewezen aan self.file en geretourneerd om binnen het with-blok te gebruiken.
  • __exit__(self, exc_type, exc_value, traceback): Deze methode wordt aangeroepen bij het verlaten van het with-blok, ongeacht of er binnen het blok een uitzondering is opgetreden. Hij zorgt ervoor dat het bestand correct wordt gesloten door self.file.close() aan te roepen als self.file niet None is. Dit voorkomt resourcelekken en zorgt voor een juiste opruiming.

Als je de inhoud van example.txt bekijkt na het uitvoeren van de bovenstaande code, zou je de geschreven tekst moeten zien:

$ cat example.txt
Hello, world!

Het voordeel van de klasse FileManager is natuurlijk dat je eigen logica kunt implementeren om met bestanden te werken. Je kunt hem bijvoorbeeld aanpassen zodat de klasse bijhoudt welke bestanden geopend of gesloten worden in een tekstlogbestand.

import datetime


class FileManager:
   def __init__(self, filename, mode, log_filename="file_log.txt"):
       self.filename = filename
       self.mode = mode
       self.log_filename = log_filename
       self.file = None

   def log_action(self, action):
       timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
       with open(self.log_filename, "a") as log_file:
           log_file.write(f"{timestamp} - {action}: {self.filename}\n")

   def __enter__(self):
       self.file = open(self.filename, self.mode)
       self.log_action("Opened")
       return self.file

   def __exit__(self, exc_type, exc_value, traceback):
       if self.file:
           self.file.close()
           self.log_action("Closed")


# Example Usage
with FileManager("example.txt", "r") as file:
   content = file.read()
   print(content)

Hello, world!

In deze aangepaste versie:

  • De module datetime wordt geïmporteerd om met tijdstempels te werken.
  • De methode log_action is toegevoegd om bestandsacties te loggen.
  • De parameter log_filename is toegevoegd om de bestandsnaam voor het logbestand op te geven. De standaardwaarde is 'file_log.txt'.
  • De bestandsnaam en actie (geopend of gesloten) worden aan het logbestand toegevoegd via de methode log_action binnen de methoden __enter__ en __exit__.
  • In de methode log_action wordt datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") gebruikt om de huidige tijdstempel op te halen in het formaat "YYYY-MM-DD HH:MM:SS".
  • De tijdstempel wordt vervolgens in het log opgenomen samen met de bestandsnaam en de actie.

Als we nu de inhoud van file_log.txt printen, zouden we de logs moeten zien:

$ cat file_log.txt
2024-04-03 12:37:02 - Opened: example.txt
2024-04-03 12:37:02 - Closed: example.txt

Een andere praktische usecase die we als aangepaste contextmanager kunnen implementeren, is een SQLite-databasemanagementtool:

import sqlite3


class DatabaseConnection:
   def __init__(self, database_name):
       self.database_name = database_name
       self.connection = None

   def __enter__(self):
       self.connection = sqlite3.connect(self.database_name)
       return self.connection

   def __exit__(self, exc_type, exc_value, traceback):
       if self.connection:
           self.connection.close()

Ook dit bootst de functie sqlite3.connect na, maar je kunt het verder verbeteren door eigen databaselogica toe te voegen. Laten we controleren of de manager werkt met enkele voorbeeldfuncties:

# Example usage
def create_table():
   with DatabaseConnection("example.db") as connection:
       cursor = connection.cursor()
       cursor.execute(
           """CREATE TABLE IF NOT EXISTS users (
                           id INTEGER PRIMARY KEY,
                           username TEXT,
                           email TEXT)"""
       )


def insert_data(username, email):
   with DatabaseConnection("example.db") as connection:
       cursor = connection.cursor()
       cursor.execute(
           "INSERT INTO users (username, email) VALUES (?, ?)", (username, email)
       )
       connection.commit()


def fetch_data():
   with DatabaseConnection("example.db") as connection:
       cursor = connection.cursor()
       cursor.execute("SELECT * FROM users")
       return cursor.fetchall()


if __name__ == "__main__":
   create_table()
   insert_data("john_doe", "john@example.com")
   insert_data("jane_doe", "jane@example.com")
   users = fetch_data()
   print("Users in the database:")
   for user in users:
       print(user)

Users in the database:
(1, 'john_doe', 'john@example.com')
(2, 'jane_doe', 'jane@example.com')
(3, 'john_doe', 'john@example.com')
(4, 'jane_doe', 'jane@example.com')

Door de databasebewerkingen op te splitsen in functies en de zojuist gemaakte aangepaste manager te gebruiken, kunnen we veel beter onderhoudbare code schrijven.

Geavanceerde onderwerpen rond contextmanagers in Python

In deze sectie bespreken we geavanceerde onderwerpen rond contextmanagers in Python. Denk aan foutafhandeling en het nesten van contextmanagers.

Foutafhandeling

Foutafhandeling is een essentieel onderdeel van programmeren, en contextmanagers in Python bieden een handige manier om fouten binnen resourcebeheer te behandelen.

In het FileManager-klassevoorbeeld kan foutafhandeling worden geïmplementeerd binnen de methode __exit__ om voor een correcte opruiming te zorgen, zelfs als er een uitzondering optreedt. Als er bijvoorbeeld een fout optreedt tijdens het schrijven naar het bestand (binnen het with-blok), willen we nog steeds zeker weten dat het bestand wordt gesloten om resourcelekken te voorkomen. We kunnen de methode __exit__ als volgt aanpassen:

def __exit__(self, exc_type, exc_value, traceback):
   if self.file:
       self.file.close()
       if exc_type is not None:
           print(f"An error occurred: {exc_value}")

Hier sluiten we eerst het bestand met self.file.close() om een correcte opruiming te garanderen. Vervolgens controleren we of er een uitzondering is opgetreden (exc_type is not None). Zo ja, dan printen we een foutmelding om aan te geven wat er misging.

Hier:

  • exc_type is de uitzonderingklasse, zoals AttributeError en IndexError.
  • exc_value is de uitzonderinginstantie met het bericht.
  • traceback is de volledige traceback van de fout.

Laten we de klasse FileManager uitvoeren met opzettelijk onjuiste code:

with FileManager("example.txt", "r") as file:
   # Simulate a sample error
   content = file.read()
   print(content[150])

An error occurred: string index out of range

Dus, om aangepaste logica te implementeren bij het afhandelen van uitzonderingen in je aangepaste managers, kun je de volgende voorbeeldworkflow volgen:

def __exit__(self, exc_type, exc_value, traceback):
   # Perform cleanup
   ...

   # Handle the exception
   if isinstance(exc_value, AttributeError):
       # Handle AttributeError here
       print(f"Exception message: {exc_value}")
   # Handle another exception
   elif isinstance(exc_value, AnotherError):
       ...

Contextmanagers nesten

Door contextmanagers te nesten kun je meerdere resources tegelijkertijd beheren, elk met zijn eigen contextmanager, binnen één with-instructie. Dit is vooral handig bij complexe operaties met meerdere resources. Laten we bekijken hoe we contextmanagers kunnen nesten in onze voorbeelden.

Stel dat we een bestand willen openen en binnen dezelfde context databasebewerkingen willen uitvoeren. Misschien willen we de inhoud van een CSV-bestand als rijen in een database invoeren. Dat kunnen we bereiken door de contextmanagers FileManager en DatabaseConnection binnen één with-instructie te nesten:

if __name__ == "__main__":
   with FileManager("example.txt", "w") as file, DatabaseConnection("example.db") as connection:
       ...

In dit voorbeeld zijn de contextmanagers FileManager en DatabaseConnection genest binnen dezelfde with-instructie. Zo kunnen we het bestand openen en tegelijk een databaseverbinding opzetten. Daarna worden het schrijven naar het bestand en de databasebewerkingen binnen deze gecombineerde context uitgevoerd.

Eén ding om in gedachten te houden is de leesbaarheid van code. Ik raad aan om het aantal gelijktijdige contexten niet boven de twee te laten uitkomen.

Conclusie

Contextmanagers in Python bieden een gestructureerde aanpak voor resourcebeheer binnen een with-instructie, waarmee je de juiste allocatie en vrijgave van resources waarborgt.

In deze tutorial hebben we de basis van contextmanagers behandeld, praktische voorbeelden doorgenomen en geavanceerde onderwerpen zoals foutafhandeling en nesten besproken. Door het schrijven van aangepaste contextmanagers onder de knie te krijgen, kun je schonere, betrouwbaardere code schrijven, de foutafhandeling verbeteren en resources efficiënt beheren.

Het onderwerp van het schrijven van aangepaste contextmanagers valt onder de paraplu van de categorie “Advanced Python”. Hier zijn enkele gerelateerde bronnen om je vaardigheden verder te verbeteren:


Bexruz (Bex) Tuychiev's photo
Author
Bexruz (Bex) Tuychiev
LinkedIn

Ik ben een contentmaker op het gebied van data science met meer dan 2 jaar ervaring en een van de grootste achterbannen op Medium. Ik schrijf graag uitgebreide artikelen over AI en ML met een vleugje sarcastische stijl, want je moet toch íets doen om ze wat minder saai te maken. Ik heb meer dan 130 artikelen en een DataCamp-cursus gemaakt, en er is nog een nieuwe in de maak. Mijn content is door meer dan 5 miljoen paar ogen gezien, van wie 20k volgers werden op zowel Medium als LinkedIn. 

Onderwerpen
Python

Zet je Python-reis vandaag voort!

Leerpad

Python programmeren

19 uur
Maak je programmeervaardigheden nog beter. Leer hoe je code kunt optimaliseren, functies en tests kunt schrijven en de beste software-engineeringtechnieken kunt gebruiken.
Bekijk detailsRight Arrow
Start Cursus
Meer bekijkenRight Arrow
Gerelateerd

blog

AI vanaf nul leren in 2026: een complete gids van de experts

Ontdek alles wat je moet weten om in 2026 AI te leren, van tips om te beginnen tot handige resources en inzichten van industrie-experts.
Adel Nehme's photo

Adel Nehme

15 min

Meer BekijkenMeer Bekijken