8.10. Awaitable klassen

8.10.1. Wat een awaitable is

Wanneer een coroutine await x schrijft, vraagt de taal aan x hoe erop gewacht moet worden. Elk object dat dat weet te beantwoorden is awaitable. Er zijn twee soorten:

  • De coroutine-objecten die async def-functies teruggeven. Het aanroepen van send_request() produceert er telkens zo een – het coroutine-object, niet het resultaat van de functie. Door await send_request() te schrijven wordt de body daadwerkelijk uitgevoerd.

  • Objecten van een klasse die een __await__-methode definieert. async def is de verkorte schrijfwijze voor de eerste soort; __await__ is de handmatige manier om de tweede soort te maken.

Applicatiecode gebruikt standaard de eerste soort. De tweede soort bestaat voor het zeldzame geval waarin het object zelf datgene moet zijn waarop de aanroeper awaitt, en niet het resultaat van het aanroepen van een methode erop.

8.10.2. De twee vormen naast elkaar

Dezelfde logica, geschreven als coroutine en als awaitable klasse:

import asyncio

# async def -- almost always the right choice
async def yield_then_return(value):
    await asyncio.sleep_ms(0)
    return value

# awaitable class -- equivalent, rarely written
class YieldThenReturn:
    def __init__(self, value):
        self._value = value

    def __await__(self):
        yield                       # let the loop run once
        return self._value          # value of the ``await`` expression

Op beide kan op dezelfde manier awaited worden:

async def main():
    a = await yield_then_return(1)
    b = await YieldThenReturn(2)
    print(a, b)                     # prints: 1 2

De async def-vorm is korter, leest als normale Python en laat de taal alle administratie van pauzeren en hervatten verzorgen. De klassevorm levert in dit voorbeeld niets extra’s op.

8.10.3. Wanneer de klassevorm zijn plaats verdient

Wanneer het object datgene moet zijn dat de applicatie doorgeeft – en niet een functie die de aanroeper aanroept om een coroutine te krijgen – is de klassevorm de enige optie:

  • Een future-achtige waarde die de applicatie aanmaakt, aan een producent overhandigt en later awaitt om het geproduceerde resultaat op te halen.

  • Een eigen primitief gebouwd bovenop een bestaande, waarbij de natuurlijke API await my_thing is in plaats van await my_thing.wait(). De klasse asyncio.Task is zelf een ingebouwd voorbeeld – await task schrijven werkt omdat Task __await__ definieert.

Voor alles dat in de vorm roep een functie aan, krijg een coroutine, await die past, wint async def.

8.10.4. De protocoldetails

__await__ is een gewone methode (geen async def) die een iterator teruggeeft. Door het als een generator te schrijven – met ten minste één yield in de body – wordt het er automatisch een. Elke yield pauzeert de coroutine die op het object awaitt en geeft de controle terug aan de event loop; de generator wordt hervat zodra de loop de taak weer inplant. Een kale return (of het bereiken van het einde) beëindigt het wachten; wat de return oplevert wordt de waarde van de await-expressie.

In de praktijk delegeert de zeldzame klasse die dit doet het yielden aan een bestaande awaitable, in plaats van de loop zelf aan te sturen:

class WaitForEvent:
    def __init__(self, event):
        self._event = event

    def __await__(self):
        yield from self._event.wait().__await__()
        return self._event

De yield from hergebruikt de __await__ van het onderliggende event voor het daadwerkelijke pauzeren. De meeste asyncio-applicaties hoeven nooit een van beide vormen te schrijven.