8.10. Klasy awaitable

8.10.1. Czym jest obiekt awaitable

Gdy korutyna zapisze await x, język pyta x, jak na niego czekać. Każdy obiekt, który potrafi na to odpowiedzieć, jest awaitable. Istnieją dwa rodzaje:

  • Obiekty korutyn zwracane przez funkcje async def. Wywołanie send_request() za każdym razem produkuje jeden z nich – obiekt korutyny, a nie wynik funkcji. Zapis await send_request() to to, co faktycznie uruchamia ciało funkcji.

  • Obiekty klasy definiującej metodę __await__. async def jest skrótem dla pierwszego rodzaju; __await__ to ręczny sposób tworzenia drugiego rodzaju.

Kod aplikacji domyślnie korzysta z pierwszego rodzaju. Drugi rodzaj istnieje dla rzadkiego przypadku, gdy to sam obiekt ma być tym, na co wywołujący wykonuje await, a nie wynik wywołania metody na nim.

8.10.2. Obie formy obok siebie

Ta sama logika zapisana jako korutyna i jako klasa awaitable:

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

Na obie można wykonać await w ten sam sposób:

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

Forma async def jest krótsza, czyta się jak zwykły Python i pozwala językowi zarządzać całą buchalterią wstrzymywania i wznawiania. Forma klasowa w tym przykładzie nie daje nic dodatkowego.

8.10.3. Kiedy forma klasowa zasługuje na swoje miejsce

Gdy obiekt musi być czymś, co aplikacja przekazuje dalej – a nie funkcją, którą wywołujący wywołuje, by otrzymać korutynę – jedyną opcją jest forma klasowa:

  • Wartość future-like, którą aplikacja tworzy, przekazuje producentowi i na którą później wykonuje await, aby pobrać wytworzony wynik.

  • Niestandardowy prymityw zbudowany na bazie istniejącego, w którym naturalnym API jest await my_thing zamiast await my_thing.wait(). Sama klasa asyncio.Task jest wbudowanym przykładem – zapis await task działa, ponieważ Task definiuje __await__.

Dla wszystkiego, co pasuje do schematu wywołaj funkcję, otrzymaj korutynę, wykonaj na niej await, wygrywa async def.

8.10.4. Szczegóły protokołu

__await__ to zwykła metoda (nie async def), która zwraca iterator. Zapisanie jej jako generatora – z co najmniej jednym yield w ciele – automatycznie czyni ją takim. Każdy yield wstrzymuje korutynę wykonującą await na obiekcie i oddaje sterowanie z powrotem do pętli zdarzeń; generator wznawia działanie przy następnym zaplanowaniu zadania przez pętlę. Zwykłe return (lub dotarcie do końca) kończy oczekiwanie; cokolwiek return wytworzy, staje się wartością wyrażenia await.

W praktyce rzadka klasa, która to robi, oddaje yieldowanie istniejącemu obiektowi awaitable, zamiast samodzielnie napędzać pętlę:

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

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

yield from ponownie wykorzystuje __await__ bazowego zdarzenia do faktycznego wstrzymywania. Większość aplikacji asyncio nigdy nie musi zapisywać żadnej z tych form.