Asynchroon programmeren in Python: de complete gids
Versnel je code met asynchroon programmeren in Python. Een stapsgewijze gids voor asyncio, concurrency, efficiënte HTTP-verzoeken en database-integratie.
Terwijl je Python-script geduldig wacht tot API-responses, databasequeries of bestandsbewerkingen zijn afgerond, blijft die tijd vaak onbenut. Met asynchroon programmeren in Python kan je code meerdere taken tegelijk afhandelen. Dus terwijl de ene operatie wacht, gaan andere door, waardoor lege momenten productief worden en wachttijden van minuten vaak teruglopen naar seconden.
In deze gids leer ik je de basis van async programmeren in Python aan de hand van mini-projecten. Je ziet hoe coroutines, event loops en async I/O je code veel responsiever kunnen maken.
Wil je leren hoe je asynchrone web-API's bouwt? Bekijk dan zeker deze cursus over FastAPI.
Wat is asynchroon programmeren in Python?
In traditioneel synchroon Python voert je code één regel tegelijk uit. Als je bijvoorbeeld een API aanroept, stopt je programma en wacht op het antwoord. Duurt dat twee seconden, dan staat je hele programma twee seconden stil. Asynchroon programmeren laat je code een API-aanroep starten en vervolgens doorgaan met andere taken.
Wanneer de response binnenkomt, pakt je code de draad weer op. In plaats van te wachten tot elke operatie klaar is, kun je meerdere operaties tegelijk uitvoeren. Dit is vooral van belang wanneer je code wacht op externe systemen zoals databases, API's of bestandssystemen om te reageren.
Om dit te laten werken, gebruikt het async-systeem van Python een paar kernconcepten:
Coroutines: functies gedefinieerd met async def in plaats van def. Ze kunnen de uitvoering pauzeren en hervatten, waardoor ze ideaal zijn voor operaties waarbij je moet wachten.
await: Dit trefwoord zegt tegen Python: "pauzeer deze coroutine tot deze operatie klaar is, maar laat in de tussentijd andere code draaien."
Event loop: De motor die al je coroutines beheert en beslist welke wanneer draait en wanneer ertussen wordt gewisseld.
Taken (tasks): Coroutines verpakt voor gelijktijdige uitvoering. Je maakt ze met asyncio.create_task() om meerdere operaties tegelijk te laten draaien.
Om verwarring te voorkomen over wat async programmeren wel (en niet) kan, houd dit in gedachten:
Async werkt het best met I/O-gebonden werk zoals HTTP-verzoeken, databasequeries en bestandsoperaties, waarbij je code wacht op externe systemen.
Async helpt niet bij CPU-gebonden werk zoals complexe berekeningen of dataverwerking, waarbij je code actief rekent in plaats van wacht.
De beste manier om deze concepten eigen te maken, is door echte async code te schrijven. In de volgende sectie maak je je eerste async functie en zie je precies hoe coroutines en de event loop samenwerken.
Je eerste asynchrone Python-functie
Voordat we async code schrijven, kijken we naar een reguliere synchrone functie die wacht voordat hij iets doet:
import time
defgreet_after_delay():print("Starting...")
time.sleep(2)# Blocks for 2 secondsprint("Hello!")
greet_after_delay()Mogelijk gemaakt door
De functie werkt, maar time.sleep(2) blokkeert je hele programma. Er kan tijdens die twee seconden niets anders draaien.
Hier is de async-versie:
import asyncio
asyncdefgreet_after_delay():print("Starting...")await asyncio.sleep(2)# Pauses, but doesn't blockprint("Hello!")
asyncio.run(greet_after_delay())Mogelijk gemaakt door
De uitvoer lijkt identiek, maar onder de motorkap gebeurt iets anders. Drie veranderingen maken dit async:
async def in plaats van def declareert dit als een coroutine.
await asyncio.sleep(2) in plaats van time.sleep(2) pauzeert zonder te blokkeren.
asyncio.run() start de event loop en voert de coroutine uit.
Let op: asyncio.sleep() is zelf een async functie, daarom heeft het await nodig. Dit is een belangrijke regel: elke async functie moet worden aangeroepen met await. Of het nu een ingebouwde functie zoals asyncio.sleep() is of eentje die je zelf schrijft: als je await vergeet, wordt hij niet echt uitgevoerd.
Op dit moment lijkt de async-versie niet sneller. Dat komt omdat we maar één taak hebben. Het echte voordeel zie je wanneer je meerdere coroutines tegelijk draait, wat we in de volgende sectie behandelen.
Nog iets belangrijks: je kunt een async functie niet rechtstreeks aanroepen zoals een gewone functie. Laten we het proberen:
<coroutine object greet_after_delay at 0x...>
<class 'coroutine'>Mogelijk gemaakt door
Het aanroepen van greet_after_delay() geeft een coroutine-object terug, niet het resultaat. De functie draait niet echt. Je hebt asyncio.run() of await nodig om hem binnen een andere functie uit te voeren.
Hoe de event loop werkt
De event loop is de motor achter async programmeren. Hij beheert je coroutines en beslist wat wanneer draait. Dit gebeurt stap voor stap wanneer je de async-functie greet_after_delay() uitvoert:
asyncio.run() maakt een event loop.
De event loop start greet_after_delay().
"Starting..." wordt geprint.
Komt bij await asyncio.sleep(2) → coroutine pauzeert.
Event loop checkt: "Zijn er andere taken om te draaien?" (nu nog niet).
2 seconden gaan voorbij, sleep is klaar.
Event loop hervat greet_after_delay().
"Hello!" wordt geprint.
Functie eindigt → event loop stopt.
Stap 5 is waar async interessant wordt. Met één coroutine is er niets anders te doen. Maar als je meerdere coroutines hebt, schakelt de event loop over naar ander werk terwijl er één wacht. In plaats van stil te zitten tijdens een sleep van twee seconden, kan hij andere code uitvoeren.
Zie de event loop als een verkeersleider. Hij maakt individuele auto's niet sneller. Hij houdt het verkeer in beweging door andere auto's te laten gaan terwijl er één stilstaat.
Veelgemaakte async-fout: await vergeten
Een veelgemaakte beginnersfout is await vergeten bij het aanroepen van een coroutine binnen een andere async functie:
import asyncio
asyncdefget_message():await asyncio.sleep(1)return"Hello!"asyncdefmain():
message = get_message()# Missing await!print(message)
asyncio.run(main())Mogelijk gemaakt door
<coroutine object get_message at 0x...>
RuntimeWarning: coroutine 'get_message' was never awaitedMogelijk gemaakt door
Zonder await krijg je het coroutine-object in plaats van de returnwaarde. Python waarschuwt ook dat de coroutine nooit is uitgevoerd.
De oplossing is simpel:
asyncdefmain():
message =await get_message()# Added awaitprint(message)
asyncio.run(main())Mogelijk gemaakt door
Zie je een RuntimeWarning over een niet-geawaite coroutine, controleer dan of je await hebt gebruikt bij elke async aanroep.
Gelijktijdige asynchrone Python-taken
In de vorige sectie hebben we een sync functie naar async omgezet. Maar het was niet sneller. Dat komt doordat we slechts één coroutine draaiden. De echte kracht van async zie je wanneer je meerdere coroutines tegelijk laat lopen.
Waarom sequentieel await nog steeds sequentieel is
Je zou denken dat het aanroepen van meerdere async functies ze automatisch gelijktijdig laat draaien. Maar kijk wat er gebeurt als we greet_after_delay() drie keer aanroepen:
import asyncio
import time
asyncdefgreet_after_delay(name):print(f"Starting {name}...")await asyncio.sleep(2)print(f"Hello, {name}!")asyncdefmain():
start = time.perf_counter()await greet_after_delay("Alice")await greet_after_delay("Bob")await greet_after_delay("Charlie")
elapsed = time.perf_counter()- start
print(f"Total time: {elapsed:.2f} seconds")
asyncio.run(main())Mogelijk gemaakt door
Starting Alice...
Hello, Alice!
Starting Bob...
Hello, Bob!
Starting Charlie...
Hello, Charlie!
Total time: 6.01 secondsMogelijk gemaakt door
Zes seconden voor drie taken van twee seconden. Elke await wacht tot zijn coroutine klaar is voordat de volgende regel doorgaat. De code is async, maar draait sequentieel.
Async gelijktijdig uitvoeren met asyncio.gather()
Om coroutines tegelijk te laten draaien, gebruik je asyncio.gather(). Dat neemt meerdere coroutines en voert ze gelijktijdig uit:
asyncdefmain():
start = time.perf_counter()await asyncio.gather(
greet_after_delay("Alice"),
greet_after_delay("Bob"),
greet_after_delay("Charlie"),)
elapsed = time.perf_counter()- start
print(f"Total time: {elapsed:.2f} seconds")
asyncio.run(main())Mogelijk gemaakt door
Starting Alice...
Starting Bob...
Starting Charlie...
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Total time: 2.00 secondsMogelijk gemaakt door
Twee seconden in plaats van zes. Alle drie de coroutines startten meteen, sliepen tegelijk en eindigden samen. Dat is een 3x versnelling met één wijziging.
Let op de volgorde van de uitvoer: alle drie de "Starting..."-berichten verschijnen voordat er "Hello..."-berichten komen. Dit laat zien dat alle coroutines tijdens hetzelfde venster van twee seconden draaien in plaats van op elkaar te wachten.
asyncio.gather() retourneert een lijst met resultaten in dezelfde volgorde als waarin je de coroutines hebt meegegeven. Als je coroutines waarden teruggeven, kun je die opvangen:
asyncdeffetch_number(n):await asyncio.sleep(1)return n *10asyncdefmain():
results =await asyncio.gather(
fetch_number(1),
fetch_number(2),
fetch_number(3),)print(results)
asyncio.run(main())Mogelijk gemaakt door
De resultaten komen terug in de volgorde [10, 20, 30], overeenkomend met de volgorde van de coroutines die je aan gather() doorgaf.
Asynchrone HTTP-verzoeken in Python met aiohttp
Tot nu toe hebben we asyncio.sleep() gebruikt om vertragingen te simuleren. Nu gaan we echte HTTP-verzoeken doen. Je grijpt misschien naar de bibliotheek requests, maar die werkt hier niet. requests is synchroon en blokkeert de event loop, wat het doel van async tenietdoet.
Gebruik in plaats daarvan aiohttp, een async HTTP-client die hiervoor is gebouwd.
Introductie tot aiohttp
Zo haal je een URL op met aiohttp:
import aiohttp
import asyncio
asyncdeffetch(url):asyncwith aiohttp.ClientSession()as session:asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
html =await fetch("https://example.com")print(f"Fetched {len(html)} characters")
asyncio.run(main())Mogelijk gemaakt door
Let op de twee geneste async with-blokken. Elk beheert een andere resource, en begrijpen wat ze doen is de sleutel tot correct gebruik van aiohttp.
Hoe ClientSession werkt in aiohttp
Dit gebeurt stap voor stap wanneer je verzoeken doet met aiohttp:
aiohttp.ClientSession() maakt een connectiepool (eerst leeg).
session.get(url) checkt de pool: "Is er een open verbinding met deze host (de server van de website)?"
Als er geen verbinding is, worden een nieuwe TCP-verbinding (het basisprotocol voor dataverkeer op internet) en een SSL-handshake (de encryptieopzet voor HTTPS) opgezet.
Er wordt een HTTP-verzoek verzonden en we wachten op de responseheaders.
Het response-object houdt de verbinding vast.
await response.text() leest de bodydata van het netwerk.
Exit van de binnenste async with-lus: de verbinding gaat terug naar de pool (blijft open!).
Het volgende verzoek naar dezelfde host wordt gedaan en hergebruikt de verbinding uit de pool (stap 3 wordt overgeslagen).
Exit van de buitenste async with-lus: alle verbindingen in de pool sluiten.
Stap 7 en 8 zijn de kerninzichten. De connectiepool houdt verbindingen tussen verzoeken in leven. Als je een nieuw verzoek naar dezelfde host doet, sla je de TCP- en SSL-handshake volledig over.
Dit is belangrijk omdat het opzetten van een nieuwe verbinding traag is. Een TCP-handshake kost één round-trip naar de server. Een SSL-handshake kost er nog twee. Afhankelijk van de latentie is dat 100-300 ms voordat je zelfs maar je eerste byte data verstuurt.
Een gedeelde sessie voor alle verzoeken gebruiken
Nu zie je waarom voor elk verzoek een nieuwe sessie maken problematisch is:
# Wrong: new session for each requestasyncdeffetch_bad(url):asyncwith aiohttp.ClientSession()as session:asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
urls =["https://example.com"]*10
results =await asyncio.gather(*[fetch_bad(url)for url in urls])Mogelijk gemaakt door
Elke aanroep van fetch_bad() maakt een nieuwe sessie met een lege pool. Elk verzoek betaalt de volledige handshake-kost, zelfs als ze allemaal naar dezelfde host gaan.
De oplossing is één sessie maken en die aan je fetch-functie doorgeven:
# Right: reuse a single sessionasyncdeffetch_good(session, url):asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
urls =["https://example.com"]*10asyncwith aiohttp.ClientSession()as session:
results =await asyncio.gather(*[fetch_good(session, url)for url in urls])Mogelijk gemaakt door
Met een gedeelde sessie zet het eerste verzoek de verbinding op en hergebruiken de overige negen die. Één handshake in plaats van tien.
Voorbeeld van async HTTP-verzoek: Hacker News scrapen
Laten we dit toepassen met de Hacker News API. Deze API is perfect om asynchroon gedrag te demonstreren omdat het ophalen van verhalen meerdere verzoeken vereist. Als je nieuw bent met het werken met REST API's in Python, bekijk dan Python API's: een gids voor het bouwen en gebruiken van API's voor de basisconcepten.
De structuur van de Hacker News API:
https://hacker-news.firebaseio.com/v0/topstories.json geeft een lijst met story-ID's (alleen nummers)
https://hacker-news.firebaseio.com/v0/item/{id}.json geeft details van één verhaal terug
Om 10 verhalen te krijgen, heb je 11 verzoeken nodig: één voor de ID-lijst en vervolgens één per verhaal. Precies daar blinkt async programmeren in uit.
Laten we eerst kijken wat de API teruggeeft als we proberen het eerste verhaal op te halen:
import aiohttp
import asyncio
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdefmain():asyncwith aiohttp.ClientSession()as session:# Get top story IDsasyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()print(f"Found {len(story_ids)} stories")print(f"First 5 IDs: {story_ids[:5]}")# Fetch first story details
first_id = story_ids[0]asyncwith session.get(f"{HN_API}/item/{first_id}.json")as response:
story =await response.json()print(f"\nStory structure:")for key, value in story.items():print(f" {key}: {repr(value)[:50]}")
asyncio.run(main())Mogelijk gemaakt door
Found 500 stories
First 5 IDs: [46051449, 46055298, 46021577, 46053566, 45984864]
Story structure:
by: 'mikeayles'
descendants: 22
id: 46051449
kids: [46054027, 46053889, 46053275, 46053515, 46053002,
score: 217
text: 'I got DOOM running in KiCad by rendering it with
time: 1764108815
title: 'Show HN: KiDoom – Running DOOM on PCB Traces'
type: 'story'
url: 'https://www.mikeayles.com/#kidoom'Mogelijk gemaakt door
De API retourneert 500 story-ID's en elk verhaal heeft velden zoals title, url, score en by (de auteur).
Meerdere resultaten sequentieel vs. gelijktijdig ophalen
Laten we nu 10 verhalen sequentieel ophalen:
import aiohttp
import asyncio
import time
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdeffetch_story(session, story_id):asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
start = time.perf_counter()
stories =[]for story_id in story_ids[:10]:
story =await fetch_story(session, story_id)
stories.append(story)
elapsed = time.perf_counter()- start
print(f"Sequential: Fetched {len(stories)} stories in {elapsed:.2f} seconds")
asyncio.run(main())Mogelijk gemaakt door
Laten we nu dezelfde verhalen gelijktijdig ophalen:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
start = time.perf_counter()
tasks =[fetch_story(session, story_id)for story_id in story_ids[:10]]
stories =await asyncio.gather(*tasks)
elapsed = time.perf_counter()- start
print(f"Concurrent: Fetched {len(stories)} stories in {elapsed:.2f} seconds")print("\nTop 3 stories:")for story in stories[:3]:print(f" - {story.get('title','No title')}")
asyncio.run(main())Mogelijk gemaakt door
Concurrent: Fetched 10 stories in 0.69 seconds
Top 3 stories:
- Show HN: KiDoom – Running DOOM on PCB Traces
- AWS is 10x slower than a dedicated server for the same price [video]
- Surprisingly, Emacs on Android is pretty goodMogelijk gemaakt door
De gelijktijdige versie is 3,5x sneller. In plaats van te wachten tot elk verzoek klaar is voordat het volgende start, draaien alle 10 verzoeken tegelijk. Hier betaalt async programmeren zich uit bij echte netwerk-I/O.
Foutafhandeling en rate limiting bij asynchroon Python
Bij gelijktijdig data ophalen kan er van alles misgaan. Je kunt de server overspoelen met te veel verzoeken. Sommige verzoeken blijven eeuwig hangen. Andere mislukken meteen. En als er fouten optreden, heb je een herstelstrategie nodig.
Deze sectie doorloopt elke zorg in de volgorde waarin ze voorkomen: regelen hoeveel verzoeken eruit gaan, tijdslimieten instellen, fouten afhandelen en opnieuw proberen waar dat logisch is. Heb je een opfrisser nodig over de basis van exception handling in Python? Zie Exception & Error Handling in Python. We gebruiken deze basisopzet door de sectie heen:
import aiohttp
import asyncio
import time
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdeffetch_story(session, story_id):asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()Mogelijk gemaakt door
Rate limiting met semaforen
In de vorige sectie stuurden we 10 verzoeken tegelijk. Dat ging prima. Maar wat gebeurt er als je 500 verhalen moet ophalen? Of 10.000 pagina's scrapen?
De meeste API's hanteren rate limits. Ze staan bijvoorbeeld 10 verzoeken per seconde toe, of 100 gelijktijdige verbindingen. Overschrijd je die limieten, dan word je geblokkeerd, afgeremd of verbannen. Zelfs als de API geen limieten afdwingt, kan het afvuren van duizenden verzoeken tegelijk je eigen systeem of de server overbelasten.
Je hebt een manier nodig om te regelen hoeveel verzoeken er op elk moment "in de lucht" zijn. Dat is wat een semafoor doet.
Een semafoor werkt als een vergunningensysteem. Stel dat je drie vergunningen hebt. Elke taak die een verzoek wil doen, moet eerst een vergunning krijgen. Als hij klaar is, geeft hij de vergunning terug, zodat een nieuw verzoek die kan gebruiken. Als er geen vergunningen beschikbaar zijn, wacht de taak tot er één vrijkomt.
Zo speelt het zich af met 3 vergunningen en 4 of meer taken:
Er zijn drie vergunningen beschikbaar.
Taak A neemt een vergunning (nog 2), start zijn verzoek.
Taak B neemt een vergunning (nog 1), start zijn verzoek.
Taak C neemt een vergunning (nog 0), start zijn verzoek.
Taak D wil een vergunning, maar er is geen beschikbaar—die wacht.
Taak A is klaar, geeft zijn vergunning terug (1 beschikbaar).
Taak D neemt die vergunning en start zijn verzoek.
Dit gaat zo door tot alle taken klaar zijn.
Het wachten in stap 5 is efficiënt. De taak draait niet in een lusje "is er al een vergunning vrij?". Hij wordt geschorst en laat andere code draaien. De event loop wekt hem pas als er een vergunning beschikbaar komt.
Nu de code. In asyncio maak je een semafoor met asyncio.Semaphore(n), waarbij n het aantal vergunningen is. Om hem te gebruiken, wikkel je je code in async with semaphore:. Dit verkrijgt een vergunning bij binnenkomst van het blok en geeft die automatisch vrij bij het verlaten:
asyncdeffetch_story_limited(session, story_id, semaphore):asyncwith semaphore:# Acquire permit (or wait if none available)asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()# Permit automatically released hereMogelijk gemaakt door
Laten we 30 verhalen ophalen met en zonder semafoor:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =(await response.json())[:30]# Without rate limiting: all 30 at once
start = time.perf_counter()await asyncio.gather(*[fetch_story(session, sid)for sid in story_ids])print(f"No limit: {time.perf_counter()- start:.2f}s (30 concurrent)")# With Semaphore(5): max 5 at a time
semaphore = asyncio.Semaphore(5)
start = time.perf_counter()await asyncio.gather(*[fetch_story_limited(session, sid, semaphore)for sid in story_ids])print(f"Semaphore(5): {time.perf_counter()- start:.2f}s (5 concurrent)")
asyncio.run(main())Mogelijk gemaakt door
No limit: 0.62s (30 concurrent)
Semaphore(5): 1.50s (5 concurrent)Mogelijk gemaakt door
De versie met semafoor is langzamer omdat die verzoeken in groepjes van vijf verwerkt. Maar dat is de afweging: je levert snelheid in voor voorspelbaar, servervriendelijk gedrag.
Let op: een semafoor limiteert gelijktijdige verzoeken, niet verzoeken per tijdseenheid. Semaphore(10) betekent "maximaal 10 verzoeken tegelijk", niet "10 verzoeken per seconde". Als je strikte, tijdgebaseerde rate limiting nodig hebt (zoals exact 10 verzoeken per seconde), kun je een semafoor combineren met pauzes tussen batches, of een bibliotheek zoals aiolimiter gebruiken.
Timeouts met asyncio.wait_for()
Zelfs met gecontroleerde gelijktijdigheid kunnen individuele verzoeken blijven hangen. Een server kan je verbinding accepteren maar nooit antwoorden. Zonder timeout wacht je programma eindeloos.
De functie asyncio.wait_for() omhult elke coroutine met een deadline. Je geeft de coroutine en een timeout in seconden. Als de operatie niet op tijd klaar is, wordt asyncio.TimeoutError opgegooid:
asyncdefslow_operation():print("Starting slow operation...")await asyncio.sleep(5)return"Done"asyncdefmain():try:
result =await asyncio.wait_for(slow_operation(), timeout=2.0)print(f"Success: {result}")except asyncio.TimeoutError:print("Operation timed out after 2 seconds")
asyncio.run(main())Mogelijk gemaakt door
Wanneer de timeout verloopt, canceltwait_for() de coroutine. Je kunt TimeoutError opvangen en beslissen wat je doet: het verzoek overslaan, een standaardwaarde teruggeven of opnieuw proberen.
Voor gelijktijdige verzoeken wikkel je elk verzoek afzonderlijk in. Hier is een helper die een fout-dictionary teruggeeft in plaats van een error op te werpen:
asyncdeffetch_story_with_timeout(session, story_id, timeout=5.0):try:
coro = fetch_story(session, story_id)returnawait asyncio.wait_for(coro, timeout=timeout)except asyncio.TimeoutError:return{"error":f"Story {story_id} timed out"}Mogelijk gemaakt door
Wanneer een coroutine wordt geannuleerd (door een timeout of een andere reden), geeft Python asyncio.CancelledError op binnen die coroutine. Als je coroutine resources vasthoudt zoals bestandsdescriptors of verbindingen, gebruik dan try/finally om te garanderen dat opschonen gebeurt, zelfs bij annulering:
asyncdeffetch_with_cleanup(session, url):print("Starting fetch...")try:asyncwith session.get(url)as response:returnawait response.text()finally:print("Cleanup complete")# Runs even on cancellationMogelijk gemaakt door
Foutafhandeling met asyncio.gather()
Timeouts vangen trage verzoeken. Maar sommige verzoeken mislukken meteen met een fout. Laten we kijken wat er gebeurt wanneer één verzoek in een batch faalt.
Eerst hebben we een versie van fetch_story() nodig die een exceptie opwerpt bij ongeldige ID's:
asyncdeffetch_story_strict(session, story_id):
story =await fetch_story(session, story_id)if story isNone:raise ValueError(f"Story not found: {story_id}")return storyMogelijk gemaakt door
Laten we nu vier geldige verhalen plus één ongeldige ID ophalen:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
ids_to_fetch = story_ids[:4]+[99999999999]# 4 valid + 1 invalidtry:
stories =await asyncio.gather(*[fetch_story_strict(session, sid)for sid in ids_to_fetch])print(f"Got {len(stories)} stories")except ValueError as e:print(f"ERROR: {e}")
asyncio.run(main())Mogelijk gemaakt door
Met één ongeldige ID verliezen we alle vier de geslaagde resultaten. Standaard gebruikt gather() fail-fast-gedrag: één exceptie cancelt alles en wordt omhoog doorgegeven.
Om gedeeltelijke resultaten te behouden, voeg je return_exceptions=True toe. Dit verandert het gedrag van gather(): in plaats van excepties op te werpen, retourneert het ze als items in de resultatenlijst naast de geslaagde waarden:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
ids_to_fetch = story_ids[:4]+[99999999999]
results =await asyncio.gather(*[fetch_story_strict(session, sid)for sid in ids_to_fetch],
return_exceptions=True# Don't raise, return exceptions in list)# Separate successes from failures using isinstance()
stories =[r for r in results ifnotisinstance(r, Exception)]
errors =[r for r in results ifisinstance(r, Exception)]print(f"Got {len(stories)} stories, {len(errors)} failed")
asyncio.run(main())Mogelijk gemaakt door
Met de check isinstance(result, Exception) kun je geslaagde resultaten scheiden van fouten. Vervolgens kun je verwerken wat gelukt is en de mislukkingen loggen of opnieuw proberen.
Retry-logica met exponentiële backoff
Sommige fouten zijn tijdelijk. Een server kan even overbelast zijn, of een netwerk-hapering kan je verbinding verbreken. In zulke gevallen is opnieuw proberen zinvol.
Maar meteen opnieuw proberen kan het erger maken. Als een server het moeilijk heeft, vererger je het probleem door hem te bestoken met retries. Exponentiële backoff lost dit op door tussen elke poging langer te wachten.
Het patroon gebruikt 2 ** attempt om wachttijden te berekenen: poging 0 wacht één seconde (2⁰), poging 1 wacht twee seconden (2¹), poging 2 wacht vier seconden (2²), enzovoort. Dit geeft de server steeds meer tijd om te herstellen:
asyncdeffetch_with_retry(session, story_id, max_retries=3):for attempt inrange(max_retries):try:
story =await fetch_story(session, story_id)if story isNone:raise ValueError(f"Story {story_id} not found")return story
except(aiohttp.ClientError, ValueError):# Catch specific exceptionsif attempt == max_retries -1:print(f"Story {story_id}: Failed after {max_retries} attempts")returnNone
backoff =2** attempt # 1s, 2s, 4s...print(f"Story {story_id}: Attempt {attempt +1} failed, retrying in {backoff}s...")await asyncio.sleep(backoff)Mogelijk gemaakt door
Let op dat we specifieke excepties opvangen (aiohttp.ClientError, ValueError) in plaats van een kale except. Zo proberen we alleen opnieuw bij fouten die mogelijk tijdelijk zijn. Een KeyError door slechte code moet geen retries triggeren.
Laten we testen met een mix van geldige en ongeldige ID's:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
test_ids =[story_ids[0],99999999999, story_ids[1],88888888888, story_ids[2]]
results =await asyncio.gather(*[fetch_with_retry(session, sid)for sid in test_ids])
successful =[r for r in results if r isnotNone]print(f"\nSuccessful: {len(successful)}, Failed: {len(test_ids)-len(successful)}")
asyncio.run(main())Mogelijk gemaakt door
Story 99999999999: Attempt 1 failed, retrying in 1s...
Story 88888888888: Attempt 1 failed, retrying in 1s...
Story 99999999999: Attempt 2 failed, retrying in 2s...
Story 88888888888: Attempt 2 failed, retrying in 2s...
Story 99999999999: Failed after 3 attempts
Story 88888888888: Failed after 3 attempts
Successful: 3, Failed: 2Mogelijk gemaakt door
In productie voeg je ook jitter toe (kleine willekeurige vertragingen) om te voorkomen dat meerdere mislukte verzoeken op exact hetzelfde moment opnieuw proberen. Daarnaast zou je alleen tijdelijke fouten (server-side netwerkproblemen, zoals 503) opnieuw proberen en meteen stoppen bij permanente fouten (bijvoorbeeld 404 of 401).
Asynchrone databaseopslag in Python met aiosqlite
We hebben Hacker News-verhalen opgehaald met de juiste rate limiting, timeouts en foutafhandeling. Nu slaan we ze op in een database.
Een reguliere synchrone databasebibliotheek zoals sqlite3 zou de event loop blokkeren tijdens queries, wat het doel van asynchroon programmeren ondermijnt. Terwijl je code op de database wacht, kunnen er geen andere coroutines draaien. Voor async toepassingen heb je een async databasebibliotheek nodig.
aiosqlite wikkelt Python's ingebouwde sqlite3 in een async-interface. Het draait databaseoperaties in een threadpool zodat ze de event loop niet blokkeren. SQLite vereist geen serverconfiguratie—het is gewoon een bestand—dus je kunt deze code meteen draaien. Als je nieuw bent met databases in Python, behandelt de cursus Introduction to Databases in Python de synchrone basis waar aiosqlite op voortbouwt.
De database opzetten
Het patroon zal bekend voorkomen. Net als aiohttp.ClientSession gebruik je async with om de verbinding te beheren:
import aiosqlite
asyncdefinit_db(db_path):asyncwith aiosqlite.connect(db_path)as db:await db.execute("""
CREATE TABLE IF NOT EXISTS stories (
id INTEGER PRIMARY KEY,
title TEXT,
url TEXT,
score INTEGER,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")await db.commit()
asyncio.run(init_db("stories.db"))Mogelijk gemaakt door
De belangrijkste functies:
aiosqlite.connect(path) opent (of maakt) een databasebestand.
await db.execute(sql) voert een SQL-statement uit.
await db.commit() slaat wijzigingen op de schijf op.
Verhalen opslaan
Hier is een functie om één verhaal op te slaan:
asyncdefsave_story(db, story):await db.execute("INSERT OR REPLACE INTO stories (id, title, url, score) VALUES (?, ?, ?, ?)",(story["id"], story.get("title",""), story.get("url",""), story.get("score",0)))Mogelijk gemaakt door
De ?-plaatsaanduidingen voorkomen SQL-injectie—gebruik nooit f-strings om waarden in SQL te plaatsen. INSERT OR REPLACE werkt bestaande verhalen bij als we ze opnieuw ophalen.
Complete async-pijplijn in Python: ophalen en opslaan
Laten we nu alles uit deze tutorial combineren tot één complete pijplijn. We halen 20 Hacker News-verhalen op met rate limiting en slaan ze op in een database:
import aiohttp
import aiosqlite
import asyncio
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdeffetch_story(session, story_id):asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()asyncdeffetch_story_limited(session, story_id, semaphore):asyncwith semaphore:
story =await fetch_story(session, story_id)if story:return story
returnNoneasyncdefsave_story(db, story):await db.execute("INSERT OR REPLACE INTO stories (id, title, url, score) VALUES (?, ?, ?, ?)",(story["id"], story.get("title",""), story.get("url",""), story.get("score",0)))asyncdefmain():# Initialize databaseasyncwith aiosqlite.connect("hn_stories.db")as db:await db.execute("""
CREATE TABLE IF NOT EXISTS stories (
id INTEGER PRIMARY KEY,
title TEXT,
url TEXT,
score INTEGER,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")# Fetch storiesasyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
semaphore = asyncio.Semaphore(5)
tasks =[fetch_story_limited(session, sid, semaphore)for sid in story_ids[:20]]
stories =await asyncio.gather(*tasks)# Save to databasefor story in stories:if story:await save_story(db, story)await db.commit()# Query and display
cursor =await db.execute("SELECT id, title, score FROM stories ORDER BY score DESC LIMIT 5")
rows =await cursor.fetchall()print(f"Saved {len([s for s in stories if s])} stories. Top 5 by score:")for row in rows:print(f" [{row[2]}] {row[1][:50]}")
asyncio.run(main())Mogelijk gemaakt door
Saved 20 stories. Top 5 by score:
[671] Google Antigravity exfiltrates data via indirect p
[453] Trillions spent and big software projects are stil
[319] Ilya Sutskever: We're moving from the age of scali
[311] Show HN: We built an open source, zero webhooks pa
[306] FLUX.2: Frontier Visual IntelligenceMogelijk gemaakt door
De pijplijn gebruikt patronen uit elke sectie: ClientSession voor connection pooling, Semaphore(5) voor rate limiting, gather() voor gelijktijdig ophalen en nu aiosqlite voor asynchrone opslag. Elk onderdeel handelt zijn deel af zonder de anderen te blokkeren.
Elke keer dat je deze workflow draait, ontvang je de topverhalen van de dag.
Conclusie
Deze tutorial nam je mee van de basis-syntaxis van async/await naar een complete datapijplijn. Je leerde hoe coroutines pauzeren en hervatten, hoe de event loop gelijktijdige taken beheert en hoe asyncio.gather() meerdere operaties tegelijk uitvoert. Je voegde echte HTTP-verzoeken toe met aiohttp, regelde gelijktijdigheid met semaforen, handelde fouten af met timeouts en retries, en sloeg resultaten op in een database met aiosqlite.
Gebruik async wanneer je code wacht op externe systemen: HTTP-API's, databases, bestands-I/O of netwerk-sockets. Voor CPU-zwaar werk zoals dataverwerking of number crunching helpt async niet—kijk dan naar multiprocessing of concurrent.futures. Wil je verder de diepte in, bekijk dan de asyncio-documentatie en overweeg FastAPI voor het bouwen van async web-API's.
Wil je voortbouwen op deze kennis en leren hoe je intelligente applicaties ontwerpt? Bekijk dan de Associate AI Engineer for Developers-carrièreroute.
Veelgestelde vragen over Python async
Wat is het verschil tussen async en sync programmeren in Python?⌄
Bij synchroon programmeren voert code één regel tegelijk uit en wacht op elke operatie tot die klaar is. Async programmeren laat je code een operatie starten en doorgaan met ander werk tijdens het wachten, om vervolgens te hervatten wanneer het resultaat klaar is. Dit wordt beheerd door een event loop die tussen taken schakelt.
Wanneer moet ik async programmeren gebruiken in plaats van regulier Python?⌄
Gebruik async voor I/O-gebonden taken waarbij je code wacht op externe systemen: HTTP-verzoeken, databasequeries, bestandsoperaties of netwerk-sockets. Async helpt niet bij CPU-gebonden werk zoals dataverwerking of berekeningen—gebruik daarvoor multiprocessing of concurrent.futures.
Waarom krijg ik een waarschuwing "coroutine was never awaited"?⌄
Dit gebeurt wanneer je een async functie aanroept zonder await te gebruiken. Het aanroepen van een async functie zoals get_data() retourneert een coroutine-object, niet het resultaat. Je moet await get_data() gebruiken om hem echt uit te voeren en de returnwaarde te krijgen.
Kan ik de requests-bibliotheek gebruiken met asyncio?⌄
Nee, de bibliotheek requests is synchroon en blokkeert de event loop, wat het doel van async ondermijnt. Gebruik in plaats daarvan aiohttp—dat is een async HTTP-client ontworpen voor gelijktijdige verzoeken. Vergeet niet één enkele ClientSession te hergebruiken voor connection pooling.
Hoe beperk ik gelijktijdige verzoeken om te voorkomen dat ik een API overweldig?⌄
Gebruik asyncio.Semaphore om te regelen hoeveel verzoeken er tegelijk draaien. Maak een semafoor met je gewenste limiet (bijv. asyncio.Semaphore(5)) en wikkel elk verzoek in async with semaphore. Zo zijn er nooit meer dan dat aantal verzoeken tegelijk "in de lucht".
Leer hoe je AI in softwareapplicaties kunt integreren met behulp van API's en open-sourcebibliotheken. Begin vandaag nog aan je reis om AI-ingenieur te worden!
Terwijl je Python-script geduldig wacht tot API-responses, databasequeries of bestandsbewerkingen zijn afgerond, blijft die tijd vaak onbenut. Met asynchroon programmeren in Python kan je code meerdere taken tegelijk afhandelen. Dus terwijl de ene operatie wacht, gaan andere door, waardoor lege momenten productief worden en wachttijden van minuten vaak teruglopen naar seconden.
In deze gids leer ik je de basis van async programmeren in Python aan de hand van mini-projecten. Je ziet hoe coroutines, event loops en async I/O je code veel responsiever kunnen maken.
Wil je leren hoe je asynchrone web-API's bouwt? Bekijk dan zeker deze cursus over FastAPI.
Wat is asynchroon programmeren in Python?
In traditioneel synchroon Python voert je code één regel tegelijk uit. Als je bijvoorbeeld een API aanroept, stopt je programma en wacht op het antwoord. Duurt dat twee seconden, dan staat je hele programma twee seconden stil. Asynchroon programmeren laat je code een API-aanroep starten en vervolgens doorgaan met andere taken.
Wanneer de response binnenkomt, pakt je code de draad weer op. In plaats van te wachten tot elke operatie klaar is, kun je meerdere operaties tegelijk uitvoeren. Dit is vooral van belang wanneer je code wacht op externe systemen zoals databases, API's of bestandssystemen om te reageren.
Om dit te laten werken, gebruikt het async-systeem van Python een paar kernconcepten:
Coroutines: functies gedefinieerd met
async defin plaats vandef. Ze kunnen de uitvoering pauzeren en hervatten, waardoor ze ideaal zijn voor operaties waarbij je moet wachten.await: Dit trefwoord zegt tegen Python: "pauzeer deze coroutine tot deze operatie klaar is, maar laat in de tussentijd andere code draaien."Event loop: De motor die al je coroutines beheert en beslist welke wanneer draait en wanneer ertussen wordt gewisseld.
Taken (tasks): Coroutines verpakt voor gelijktijdige uitvoering. Je maakt ze met
asyncio.create_task()om meerdere operaties tegelijk te laten draaien.Om verwarring te voorkomen over wat async programmeren wel (en niet) kan, houd dit in gedachten:
Async werkt het best met I/O-gebonden werk zoals HTTP-verzoeken, databasequeries en bestandsoperaties, waarbij je code wacht op externe systemen.
Async helpt niet bij CPU-gebonden werk zoals complexe berekeningen of dataverwerking, waarbij je code actief rekent in plaats van wacht.
De beste manier om deze concepten eigen te maken, is door echte async code te schrijven. In de volgende sectie maak je je eerste async functie en zie je precies hoe coroutines en de event loop samenwerken.
Je eerste asynchrone Python-functie
Voordat we async code schrijven, kijken we naar een reguliere synchrone functie die wacht voordat hij iets doet:
De functie werkt, maar
time.sleep(2)blokkeert je hele programma. Er kan tijdens die twee seconden niets anders draaien.Hier is de async-versie:
De uitvoer lijkt identiek, maar onder de motorkap gebeurt iets anders. Drie veranderingen maken dit async:
async defin plaats vandefdeclareert dit als een coroutine.await asyncio.sleep(2)in plaats vantime.sleep(2)pauzeert zonder te blokkeren.asyncio.run()start de event loop en voert de coroutine uit.Let op:
asyncio.sleep()is zelf een async functie, daarom heeft hetawaitnodig. Dit is een belangrijke regel: elke async functie moet worden aangeroepen metawait. Of het nu een ingebouwde functie zoalsasyncio.sleep()is of eentje die je zelf schrijft: als jeawaitvergeet, wordt hij niet echt uitgevoerd.Op dit moment lijkt de async-versie niet sneller. Dat komt omdat we maar één taak hebben. Het echte voordeel zie je wanneer je meerdere coroutines tegelijk draait, wat we in de volgende sectie behandelen.
Nog iets belangrijks: je kunt een async functie niet rechtstreeks aanroepen zoals een gewone functie. Laten we het proberen:
Het aanroepen van
greet_after_delay()geeft een coroutine-object terug, niet het resultaat. De functie draait niet echt. Je hebtasyncio.run()ofawaitnodig om hem binnen een andere functie uit te voeren.Hoe de event loop werkt
De event loop is de motor achter async programmeren. Hij beheert je coroutines en beslist wat wanneer draait. Dit gebeurt stap voor stap wanneer je de async-functie
greet_after_delay()uitvoert:asyncio.run()maakt een event loop.De event loop start
greet_after_delay()."Starting..." wordt geprint.
Komt bij await
asyncio.sleep(2)→ coroutine pauzeert.Event loop checkt: "Zijn er andere taken om te draaien?" (nu nog niet).
2 seconden gaan voorbij, sleep is klaar.
Event loop hervat
greet_after_delay()."Hello!" wordt geprint.
Functie eindigt → event loop stopt.
Stap 5 is waar async interessant wordt. Met één coroutine is er niets anders te doen. Maar als je meerdere coroutines hebt, schakelt de event loop over naar ander werk terwijl er één wacht. In plaats van stil te zitten tijdens een sleep van twee seconden, kan hij andere code uitvoeren.
Zie de event loop als een verkeersleider. Hij maakt individuele auto's niet sneller. Hij houdt het verkeer in beweging door andere auto's te laten gaan terwijl er één stilstaat.
Veelgemaakte async-fout: await vergeten
Een veelgemaakte beginnersfout is
awaitvergeten bij het aanroepen van een coroutine binnen een andere async functie:Zonder
awaitkrijg je het coroutine-object in plaats van de returnwaarde. Python waarschuwt ook dat de coroutine nooit is uitgevoerd.De oplossing is simpel:
Zie je een
RuntimeWarningover een niet-geawaite coroutine, controleer dan of jeawaithebt gebruikt bij elke async aanroep.Gelijktijdige asynchrone Python-taken
In de vorige sectie hebben we een sync functie naar async omgezet. Maar het was niet sneller. Dat komt doordat we slechts één coroutine draaiden. De echte kracht van async zie je wanneer je meerdere coroutines tegelijk laat lopen.
Waarom sequentieel await nog steeds sequentieel is
Je zou denken dat het aanroepen van meerdere async functies ze automatisch gelijktijdig laat draaien. Maar kijk wat er gebeurt als we
greet_after_delay()drie keer aanroepen:Zes seconden voor drie taken van twee seconden. Elke
awaitwacht tot zijn coroutine klaar is voordat de volgende regel doorgaat. De code is async, maar draait sequentieel.Async gelijktijdig uitvoeren met asyncio.gather()
Om coroutines tegelijk te laten draaien, gebruik je
asyncio.gather(). Dat neemt meerdere coroutines en voert ze gelijktijdig uit:Twee seconden in plaats van zes. Alle drie de coroutines startten meteen, sliepen tegelijk en eindigden samen. Dat is een 3x versnelling met één wijziging.
Let op de volgorde van de uitvoer: alle drie de "Starting..."-berichten verschijnen voordat er "Hello..."-berichten komen. Dit laat zien dat alle coroutines tijdens hetzelfde venster van twee seconden draaien in plaats van op elkaar te wachten.
asyncio.gather()retourneert een lijst met resultaten in dezelfde volgorde als waarin je de coroutines hebt meegegeven. Als je coroutines waarden teruggeven, kun je die opvangen:De resultaten komen terug in de volgorde
[10, 20, 30], overeenkomend met de volgorde van de coroutines die je aangather()doorgaf.Asynchrone HTTP-verzoeken in Python met aiohttp
Tot nu toe hebben we
asyncio.sleep()gebruikt om vertragingen te simuleren. Nu gaan we echte HTTP-verzoeken doen. Je grijpt misschien naar de bibliotheekrequests, maar die werkt hier niet.requestsis synchroon en blokkeert de event loop, wat het doel van async tenietdoet.Gebruik in plaats daarvan
aiohttp, een async HTTP-client die hiervoor is gebouwd.Introductie tot aiohttp
Zo haal je een URL op met
aiohttp:Let op de twee geneste
async with-blokken. Elk beheert een andere resource, en begrijpen wat ze doen is de sleutel tot correct gebruik vanaiohttp.Hoe ClientSession werkt in aiohttp
Dit gebeurt stap voor stap wanneer je verzoeken doet met
aiohttp:aiohttp.ClientSession()maakt een connectiepool (eerst leeg).session.get(url)checkt de pool: "Is er een open verbinding met deze host (de server van de website)?"Als er geen verbinding is, worden een nieuwe TCP-verbinding (het basisprotocol voor dataverkeer op internet) en een SSL-handshake (de encryptieopzet voor HTTPS) opgezet.
Er wordt een HTTP-verzoek verzonden en we wachten op de responseheaders.
Het response-object houdt de verbinding vast.
await response.text()leest de bodydata van het netwerk.Exit van de binnenste
async with-lus: de verbinding gaat terug naar de pool (blijft open!).Het volgende verzoek naar dezelfde host wordt gedaan en hergebruikt de verbinding uit de pool (stap 3 wordt overgeslagen).
Exit van de buitenste
async with-lus: alle verbindingen in de pool sluiten.Stap 7 en 8 zijn de kerninzichten. De connectiepool houdt verbindingen tussen verzoeken in leven. Als je een nieuw verzoek naar dezelfde host doet, sla je de TCP- en SSL-handshake volledig over.
Dit is belangrijk omdat het opzetten van een nieuwe verbinding traag is. Een TCP-handshake kost één round-trip naar de server. Een SSL-handshake kost er nog twee. Afhankelijk van de latentie is dat 100-300 ms voordat je zelfs maar je eerste byte data verstuurt.
Een gedeelde sessie voor alle verzoeken gebruiken
Nu zie je waarom voor elk verzoek een nieuwe sessie maken problematisch is:
Elke aanroep van
fetch_bad()maakt een nieuwe sessie met een lege pool. Elk verzoek betaalt de volledige handshake-kost, zelfs als ze allemaal naar dezelfde host gaan.De oplossing is één sessie maken en die aan je fetch-functie doorgeven:
Met een gedeelde sessie zet het eerste verzoek de verbinding op en hergebruiken de overige negen die. Één handshake in plaats van tien.
Voorbeeld van async HTTP-verzoek: Hacker News scrapen
Laten we dit toepassen met de Hacker News API. Deze API is perfect om asynchroon gedrag te demonstreren omdat het ophalen van verhalen meerdere verzoeken vereist. Als je nieuw bent met het werken met REST API's in Python, bekijk dan Python API's: een gids voor het bouwen en gebruiken van API's voor de basisconcepten.
De structuur van de Hacker News API:
https://hacker-news.firebaseio.com/v0/topstories.jsongeeft een lijst met story-ID's (alleen nummers)https://hacker-news.firebaseio.com/v0/item/{id}.jsongeeft details van één verhaal terugOm 10 verhalen te krijgen, heb je 11 verzoeken nodig: één voor de ID-lijst en vervolgens één per verhaal. Precies daar blinkt async programmeren in uit.
Laten we eerst kijken wat de API teruggeeft als we proberen het eerste verhaal op te halen:
De API retourneert 500 story-ID's en elk verhaal heeft velden zoals
title,url,scoreenby(de auteur).Meerdere resultaten sequentieel vs. gelijktijdig ophalen
Laten we nu 10 verhalen sequentieel ophalen:
Laten we nu dezelfde verhalen gelijktijdig ophalen:
De gelijktijdige versie is 3,5x sneller. In plaats van te wachten tot elk verzoek klaar is voordat het volgende start, draaien alle 10 verzoeken tegelijk. Hier betaalt async programmeren zich uit bij echte netwerk-I/O.
Foutafhandeling en rate limiting bij asynchroon Python
Bij gelijktijdig data ophalen kan er van alles misgaan. Je kunt de server overspoelen met te veel verzoeken. Sommige verzoeken blijven eeuwig hangen. Andere mislukken meteen. En als er fouten optreden, heb je een herstelstrategie nodig.
Deze sectie doorloopt elke zorg in de volgorde waarin ze voorkomen: regelen hoeveel verzoeken eruit gaan, tijdslimieten instellen, fouten afhandelen en opnieuw proberen waar dat logisch is. Heb je een opfrisser nodig over de basis van exception handling in Python? Zie Exception & Error Handling in Python. We gebruiken deze basisopzet door de sectie heen:
Rate limiting met semaforen
In de vorige sectie stuurden we 10 verzoeken tegelijk. Dat ging prima. Maar wat gebeurt er als je 500 verhalen moet ophalen? Of 10.000 pagina's scrapen?
De meeste API's hanteren rate limits. Ze staan bijvoorbeeld 10 verzoeken per seconde toe, of 100 gelijktijdige verbindingen. Overschrijd je die limieten, dan word je geblokkeerd, afgeremd of verbannen. Zelfs als de API geen limieten afdwingt, kan het afvuren van duizenden verzoeken tegelijk je eigen systeem of de server overbelasten.
Je hebt een manier nodig om te regelen hoeveel verzoeken er op elk moment "in de lucht" zijn. Dat is wat een semafoor doet.
Een semafoor werkt als een vergunningensysteem. Stel dat je drie vergunningen hebt. Elke taak die een verzoek wil doen, moet eerst een vergunning krijgen. Als hij klaar is, geeft hij de vergunning terug, zodat een nieuw verzoek die kan gebruiken. Als er geen vergunningen beschikbaar zijn, wacht de taak tot er één vrijkomt.
Zo speelt het zich af met 3 vergunningen en 4 of meer taken:
Er zijn drie vergunningen beschikbaar.
Taak A neemt een vergunning (nog 2), start zijn verzoek.
Taak B neemt een vergunning (nog 1), start zijn verzoek.
Taak C neemt een vergunning (nog 0), start zijn verzoek.
Taak D wil een vergunning, maar er is geen beschikbaar—die wacht.
Taak A is klaar, geeft zijn vergunning terug (1 beschikbaar).
Taak D neemt die vergunning en start zijn verzoek.
Dit gaat zo door tot alle taken klaar zijn.
Het wachten in stap 5 is efficiënt. De taak draait niet in een lusje "is er al een vergunning vrij?". Hij wordt geschorst en laat andere code draaien. De event loop wekt hem pas als er een vergunning beschikbaar komt.
Nu de code. In asyncio maak je een semafoor met
asyncio.Semaphore(n), waarbijnhet aantal vergunningen is. Om hem te gebruiken, wikkel je je code inasync with semaphore:. Dit verkrijgt een vergunning bij binnenkomst van het blok en geeft die automatisch vrij bij het verlaten:Laten we 30 verhalen ophalen met en zonder semafoor:
De versie met semafoor is langzamer omdat die verzoeken in groepjes van vijf verwerkt. Maar dat is de afweging: je levert snelheid in voor voorspelbaar, servervriendelijk gedrag.
Let op: een semafoor limiteert gelijktijdige verzoeken, niet verzoeken per tijdseenheid.
Semaphore(10)betekent "maximaal 10 verzoeken tegelijk", niet "10 verzoeken per seconde". Als je strikte, tijdgebaseerde rate limiting nodig hebt (zoals exact 10 verzoeken per seconde), kun je een semafoor combineren met pauzes tussen batches, of een bibliotheek zoalsaiolimitergebruiken.Timeouts met asyncio.wait_for()
Zelfs met gecontroleerde gelijktijdigheid kunnen individuele verzoeken blijven hangen. Een server kan je verbinding accepteren maar nooit antwoorden. Zonder timeout wacht je programma eindeloos.
De functie
asyncio.wait_for()omhult elke coroutine met een deadline. Je geeft de coroutine en eentimeoutin seconden. Als de operatie niet op tijd klaar is, wordtasyncio.TimeoutErroropgegooid:Wanneer de timeout verloopt,
canceltwait_for()de coroutine. Je kuntTimeoutErroropvangen en beslissen wat je doet: het verzoek overslaan, een standaardwaarde teruggeven of opnieuw proberen.Voor gelijktijdige verzoeken wikkel je elk verzoek afzonderlijk in. Hier is een helper die een fout-dictionary teruggeeft in plaats van een error op te werpen:
Wanneer een coroutine wordt geannuleerd (door een timeout of een andere reden), geeft Python
asyncio.CancelledErrorop binnen die coroutine. Als je coroutine resources vasthoudt zoals bestandsdescriptors of verbindingen, gebruik dantry/finallyom te garanderen dat opschonen gebeurt, zelfs bij annulering:Foutafhandeling met asyncio.gather()
Timeouts vangen trage verzoeken. Maar sommige verzoeken mislukken meteen met een fout. Laten we kijken wat er gebeurt wanneer één verzoek in een batch faalt.
Eerst hebben we een versie van
fetch_story()nodig die een exceptie opwerpt bij ongeldige ID's:Laten we nu vier geldige verhalen plus één ongeldige ID ophalen:
Met één ongeldige ID verliezen we alle vier de geslaagde resultaten. Standaard gebruikt
gather()fail-fast-gedrag: één exceptie cancelt alles en wordt omhoog doorgegeven.Om gedeeltelijke resultaten te behouden, voeg je
return_exceptions=Truetoe. Dit verandert het gedrag vangather(): in plaats van excepties op te werpen, retourneert het ze als items in de resultatenlijst naast de geslaagde waarden:Met de check
isinstance(result, Exception)kun je geslaagde resultaten scheiden van fouten. Vervolgens kun je verwerken wat gelukt is en de mislukkingen loggen of opnieuw proberen.Retry-logica met exponentiële backoff
Sommige fouten zijn tijdelijk. Een server kan even overbelast zijn, of een netwerk-hapering kan je verbinding verbreken. In zulke gevallen is opnieuw proberen zinvol.
Maar meteen opnieuw proberen kan het erger maken. Als een server het moeilijk heeft, vererger je het probleem door hem te bestoken met retries. Exponentiële backoff lost dit op door tussen elke poging langer te wachten.
Het patroon gebruikt
2 ** attemptom wachttijden te berekenen: poging 0 wacht één seconde (2⁰), poging 1 wacht twee seconden (2¹), poging 2 wacht vier seconden (2²), enzovoort. Dit geeft de server steeds meer tijd om te herstellen:Let op dat we specifieke excepties opvangen (
aiohttp.ClientError,ValueError) in plaats van een kaleexcept. Zo proberen we alleen opnieuw bij fouten die mogelijk tijdelijk zijn. EenKeyErrordoor slechte code moet geen retries triggeren.Laten we testen met een mix van geldige en ongeldige ID's:
In productie voeg je ook jitter toe (kleine willekeurige vertragingen) om te voorkomen dat meerdere mislukte verzoeken op exact hetzelfde moment opnieuw proberen. Daarnaast zou je alleen tijdelijke fouten (server-side netwerkproblemen, zoals 503) opnieuw proberen en meteen stoppen bij permanente fouten (bijvoorbeeld 404 of 401).
Asynchrone databaseopslag in Python met aiosqlite
We hebben Hacker News-verhalen opgehaald met de juiste rate limiting, timeouts en foutafhandeling. Nu slaan we ze op in een database.
Een reguliere synchrone databasebibliotheek zoals
sqlite3zou de event loop blokkeren tijdens queries, wat het doel van asynchroon programmeren ondermijnt. Terwijl je code op de database wacht, kunnen er geen andere coroutines draaien. Voor async toepassingen heb je een async databasebibliotheek nodig.aiosqlitewikkelt Python's ingebouwdesqlite3in een async-interface. Het draait databaseoperaties in een threadpool zodat ze de event loop niet blokkeren. SQLite vereist geen serverconfiguratie—het is gewoon een bestand—dus je kunt deze code meteen draaien. Als je nieuw bent met databases in Python, behandelt de cursus Introduction to Databases in Python de synchrone basis waaraiosqliteop voortbouwt.De database opzetten
Het patroon zal bekend voorkomen. Net als
aiohttp.ClientSessiongebruik jeasync withom de verbinding te beheren:De belangrijkste functies:
aiosqlite.connect(path)opent (of maakt) een databasebestand.await db.execute(sql)voert een SQL-statement uit.await db.commit()slaat wijzigingen op de schijf op.Verhalen opslaan
Hier is een functie om één verhaal op te slaan:
De
?-plaatsaanduidingen voorkomen SQL-injectie—gebruik nooit f-strings om waarden in SQL te plaatsen.INSERT OR REPLACEwerkt bestaande verhalen bij als we ze opnieuw ophalen.Complete async-pijplijn in Python: ophalen en opslaan
Laten we nu alles uit deze tutorial combineren tot één complete pijplijn. We halen 20 Hacker News-verhalen op met rate limiting en slaan ze op in een database:
De pijplijn gebruikt patronen uit elke sectie:
ClientSessionvoor connection pooling,Semaphore(5)voor rate limiting,gather()voor gelijktijdig ophalen en nuaiosqlitevoor asynchrone opslag. Elk onderdeel handelt zijn deel af zonder de anderen te blokkeren.Elke keer dat je deze workflow draait, ontvang je de topverhalen van de dag.
Conclusie
Deze tutorial nam je mee van de basis-syntaxis van
async/awaitnaar een complete datapijplijn. Je leerde hoe coroutines pauzeren en hervatten, hoe de event loop gelijktijdige taken beheert en hoeasyncio.gather()meerdere operaties tegelijk uitvoert. Je voegde echte HTTP-verzoeken toe metaiohttp, regelde gelijktijdigheid met semaforen, handelde fouten af met timeouts en retries, en sloeg resultaten op in een database metaiosqlite.Gebruik async wanneer je code wacht op externe systemen: HTTP-API's, databases, bestands-I/O of netwerk-sockets. Voor CPU-zwaar werk zoals dataverwerking of number crunching helpt async niet—kijk dan naar
multiprocessingofconcurrent.futures. Wil je verder de diepte in, bekijk dan de asyncio-documentatie en overweeg FastAPI voor het bouwen van async web-API's.Wil je voortbouwen op deze kennis en leren hoe je intelligente applicaties ontwerpt? Bekijk dan de Associate AI Engineer for Developers-carrièreroute.