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)