Twisted - wprowadzenie: generatory

We wpisie "reactor i deferred" przedstawiłem koncepcje programowania asynchronicznego z wykorzystaniem obiektu Deferred w którym kolejkowane były zadania. Taki sposób pisania może być jednak bardzo niewygodny, jeśli okaże się, że w swoim kodzie mamy wiele miejsc oczekujących na dane. Każdorazowe pojawienie się nowych informacji wymaga napisania nowej funkcji która je obsłuży. Drugie tyle, jeśli chcemy oprogramować możliwe wystąpienia błędów.

Generatory

Jaki jest generator, każdy widzi. Najprostszym przykładem może być generowanie kolejnych liczb:

>>> def gen_number(start):
...     while True:
...         start += 1
...         yield start
...         
>>> g = gen_number(2)
>>> g.next()
3
>>> g.next()
4
>>> g.next()
5

Jeśli chcesz dowiedzieć się o nich więcej, polecam prezentację "Generator Tricks for Systems Programmers".

Generatory, aż do Pythona 2.5 miały bardzo poważną wadę - były hermetyczne. Utworzony obiekt nie przyjmował żadnych danych z zewnątrz i mógł jedynie zwracać kolejne wartości. Od wersji 2.5, generatory posiadają metodę send(), dzięki któremu komunikacja odbywać się może w obu kierunkach:

>>> def push_filter(power):
...     #initialize for future usage
...     data = 0
...     while True:
...         data = yield (data ** power)
...         
>>> pf = push_filter(3)
>>> # initialize generator
>>> pf.next()
0
>>> pf.send(3)
27
>>> pf.send(4)
64
>>> pf.send(2)
8

Nieocenioną pomocą w zrozumieniu nowych możliwości generatorów był dla mnie wykład "A Curious Course on Coroutines and Concurrency". Kolejno omawiane w nim przykłady to implementacja "systemu operacyjnego" ze wsparciem dla zielonych wątków. Końcowy kod rozbudowałem o dodatkowe sygnały i epoll. W efekcie otrzymałem asynchroniczny prawie-framework. Jego główna zaletą jest wykorzystanie yield do oczekiwania na blokujące operacje, dzięki czemu nie muszę pisać callbacków. Pierwszy z trzech przykładowych programów to serwer echo:

# importy...

def handle_request(sock, addr):
    data = ''
    while True:
        data += yield sock.recv(1024)
        if '\r\n' in data:
            break
    response = 'echo: ' + data
    yield sock.send(response)
    yield sock.close()

def echo(raw_socket, host, port):
    sock = Socket(raw_socket)
    sock.bind((host, port))
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.listen(1)
    while True:
        print '>> waiting for new connection'
        conn, addr = yield sock.accept()
        # spawn new light-thread task
        yield calls.NewTask(handle_request(conn, addr))

scheduler = Scheduler()
raw_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
scheduler.add(echo(raw_socket, '', 8080)
scheduler.run()

Każda blokująca operacja opakowana jest przez sygnał systemowy , przez co możliwe jest użycie yield i uśpienie podprogramu aż do nadejścia danych. W kolejnych przykładach pokazałem, jak proste jest używanie wyjątków.

Twisted już to ma..

Ostatnim przykładem jaki napisałem w twisted był serwer pobierający strony. W metodzie lineReceived tworzony był obiekt Deferred w którym kolejkowane były send_sucess oraz send_errback. Przykład wyglądał nie najgorzej, bo wymagał jedynie dwóch callbacków. Co jeśli każde żądanie klienta wymagałoby X blokujących wywołań? Musiałbym napisać co najmniej 2X funkcji?

W module twisted.internet.defer znajduje się dekorator inlineCallbacks, który pozwala na wykorzystanie możliwości generatorów Pythona >= 2.5. Ten sam przykład może więc wyglądać o wiele lepiej:

def lineReceived(self, line):
    self._lineReceived(line)

@defer.inlineCallbacks
def _lineReceived(self, line):
    try:
        page = yield getPage(line)
    except Exception as e:
        self.sendLine('%s:%s' % \
                (type(e).__name__, ', '.join(e.args)))
        self.transport.loseConnection()
    else:
        data = page.replace('\r\n', '')
        self.sendLine(data)