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.