Antwoord op de vraag
In Python zijn coroutines, die zijn gemaakt via async def, geïmplementeerd als eenmalige staatmachines die worden beheerd door de CO_ITERABLE_COROUTINE of native coroutine-vlaggen op het CPython bytecode-niveau. Wanneer je een async functie aanroept, retourneert deze onmiddellijk een coroutine-object dat een frame-object en uitvoeringsstatus bevat; het afwachten ervan drijft deze staatmachine naar voltooiing, waarbij het interne f_lasti (laatste instructie) marker het einde bereikt en het frame wordt gemarkeerd als uitgeput. De Python runtime beschermt expliciet tegen herintrede door deze voltooiingsvlag te controleren en een RuntimeError op te werpen als er vervolgafwachtingen plaatsvinden, omdat coroutines zijn ontworpen om enkele, discrete asynchrone operaties met een lineaire controleflow voor te stellen. Daarentegen zijn generatorfuncties fabrieken—elk oproep creëert een gloednieuwe PyGenObject met zijn eigen onafhankelijke stackframe en instructiepointer, waardoor de functie meerdere onafhankelijke iterators kan produceren die elk aparte uitvoeringscontexten onderhouden.
Situatie uit het leven
Een ontwikkelingsteam bouwde een veerkrachtige WebSocket-client die mislukte verbinding pogingen met exponentiële terugval moest herhalen. Ze definieerden oorspronkelijk een verbinding coroutine op het module niveau en probeerden deze opnieuw te gebruiken in de retry-logica.
import asyncio async def establish_connection(): return await websockets.connect("wss://api.example.com") # Modul-niveau instantiatie connection_coro = establish_connection() async def retry_connect(max_attempts=3): for attempt in range(max_attempts): try: ws = await connection_coro # Fails op de tweede iteratie return ws except Exception: await asyncio.sleep(2 ** attempt)
Het probleem ontstond toen de tweede lusiteratie probeerde om connection_coro opnieuw af te wachten, waardoor een RuntimeError werd getriggerd omdat de eerste succesvolle afwachting het coroutine-object al had uitgeput. Het team overwoog drie architectonische oplossingen.
Een benadering bestond uit het handmatig reconstrueren van het coroutine-object binnen de except-blok nadat de RuntimeError was opgevangen. Hoewel technisch haalbaar, introduceerde dit kwetsbaarheidsbeheer van de staat en maakte de code afhankelijk van het detecteren van uitputting via uitzondering behandeling, wat semantisch ambigu is en legitieme runtimefouten binnen de verbindingslogica zelf kan maskeren.
Een andere oplossing stelde voor om establish_connection om te zetten in een klasse die __await__ implementeert om een resetbare awaitable te creëren. Dit bood een fabrieks patroon maar voegde onnodige boilerplate en complexiteit toe, waardoor de eenvoudige intentie om een verbinding tot stand te brengen werd verduisterd en handmatige statusregistratie vereiste die duplicate wat de Python runtime al biedt via functie aanroepen.
De gekozen oplossing was om de async functie als een fabriek te beschouwen door de oproepplaats binnen de lus te verplaatsen, zodat elke iteratie een ongerept coroutine-object ontving. Door om te bouwen naar ws = await establish_connection(), werd elke poging een verse staatmachine met onafhankelijke resourcebeheer. Dit kwam overeen met de ontwerpfilosofie van Python waarbij async functies constructeurs zijn voor eenmalige computationele toekomsten, wat resulteerde in schone, exception-vrije retry-logica die mislukte verbinding pogingen op de juiste wijze isolement van volgende retries.
Wat kandidaten vaak missen
Waarom creëert het opslaan van een coroutine in een variabele en vergeten om deze af te wachten een resource-lek, en hoe verzacht close() dit?
Kandidaten nemen vaak aan dat niet-afgewachte coroutines gewoon garbage collected worden zonder bijwerkingen. Echter, als een coroutine zijn lichaam is binnengetreden en is gesuspend op een await expressie (bijvoorbeeld, het vasthouden van een databaseverbinding of slot), behoudt het frame referenties naar deze middelen. Het aanroepen van close() op het coroutine-object dwingt een GeneratorExit uitzondering door het frame, wat contextmanagers (async with) en try/finally blokken activeert om middelen onmiddellijk vrij te geven. Zonder expliciete close() blijven deze middelen vastgehouden totdat cyclische garbage collecting draait, wat te laat kan zijn voor verbinding pool uitputtingsscenario's.
Hoe verschilt inspect.iscoroutine() van inspect.isawaitable(), en waarom is deze onderscheiding belangrijk bij het schrijven van generieke asyncio utilities?
inspect.iscoroutine() retourneert True alleen voor native coroutine-objecten die zijn gemaakt door async def functies, terwijl inspect.isawaitable() True retourneert voor elk object dat __await__ implementeert, waaronder coroutines, taken, futures, en aangepaste awaitables. Kandidaten missen dat asyncio-functies zoals ensure_future() elk awaitable accepteren, niet alleen coroutines. Het schrijven van bibliotheken die strikt iscoroutine() controleren, verwerpt geldige awaitables zoals asyncio.Queue().get() of aangepaste toekomstobjecten, waardoor polymorfisme wordt gebroken in generieke hulpfuncties die zijn ontworpen om willekeurige asynchrone operaties te plannen.
Wat is het verschil tussen async for en await bij het consumeren van een asynchrone generator, en waarom vereist de eerste dat __aiter__ de generator zelf retourneert in plaats van een coroutine?
await consumeert een coroutine of toekomst tot voltooiing en retourneert een enkele waarde, terwijl async for iteraties maakt over een asynchrone iterator, en pauzeert bij elke yield binnen een async def generatorfunctie. Kandidaten verwarren async for met het afwachten van een lijst met coroutines. Cruciaal is dat __aiter__ het asynchrone iterator-object direct moet retourneren (niet een awaitable), omdat de Python runtime __aiter__ synchroon aanroept om de iterator te verkrijgen voordat de iteratieprotocol begint. Een coroutine retourneren vanuit __aiter__ veroorzaakt een TypeError, omdat het protocol onmiddellijke toegang verwacht tot de __anext__ methode van de iterator om de asynchrone iteratiestatusmachine aan te drijven.