Zanurkuj w Pythonie/plural.py, etap 6

Z Wikibooks, biblioteki wolnych podręczników.

Teraz jesteście już gotowi, aby porozmawiać o generatorach.

Przykład 17.17. plural6.py

import re

def rules(language):                                                                 
    for line in file('rules.%s' % language):                                         
        pattern, search, replace = line.split()                                      
        yield lambda word: re.search(pattern, word) and re.sub(search, replace, word)

def plural(noun, language='en'):      
    for applyRule in rules(language): 
        result = applyRule(noun)      
        if result: return result

Powyższy kod używa generatora. Nie zacznę nawet tłumaczyć, na czym polega ta technika, dopóki nie przyjrzycie się najpierw prostszemu przykładowi.

Przykład 17.18. Wprowadzenie do generatorów

>>> def make_counter(x):
...     print 'entering make_counter'
...     while 1:
...         yield x               #(1)
...         print 'incrementing x'
...         x = x + 1
...     
>>> counter = make_counter(2) #(2)
>>> counter                   #(3)
<generator object at 0x001C9C10>
>>> counter.next()            #(4)
entering make_counter
2
>>> counter.next()            #(5)
incrementing x
3
>>> counter.next()            #(6)
incrementing x
4
  1. Obecność słowa kluczowego yield w definicji make_counter oznacza, że nie jest to zwykła funkcja. To specjalny rodzaj funkcji, która generuje wartości przy każdym wywołaniu. Możecie myśleć o niej jak o funkcji kontynuującej swoje działanie: wywołanie jej zwraca obiekt generatora, który może zostać użyty do generowania kolejnych wartości x.
  2. Aby uzyskać instancję generatora make_counter, wystarczy wywołać funkcję make_counter. Zauważcie, że nie spowoduje to jeszcze wykonania kodu tej funkcji. Można to stwierdzić na podstawie faktu, że pierwszą instrukcją w tej funkcji jest instrukcja print, a nic jeszcze nie zostało wypisane.
  3. Funkcja make_counter zwraca obiekt będący generatorem.
  4. Kiedy po raz pierwszy wywołamy next() na obiekcie generatora, zostanie wykonany kod funkcji make_counter aż do pierwszej instrukcji yield, która spowoduje zwrócenie pewnej wartości. W tym przypadku będzie to wartość 2, ponieważ utworzyliśmy generator wywołując make_counter(2).
  5. Kolejne wywołania next() na obiekcie generatora spowodują kontynuowanie wykonywania kodu funkcji od miejsca, w którym wykonywanie funkcji zostało przerwane aż do kolejnego napotkania instrukcji yield. W tym przypadku następną linią kodu oczekującą na wykonanie jest instrukcja print, która wypisuje tekst "incrementing x", a kolejną - instrukcja przypisania x = x + 1, która zwiększa wartość x. Następnie wchodzimy w kolejny cykl pętli while, w którym wykonywana jest instrukcja yield, zwracająca bieżącą wartość x (obecnie jest to 3).
  6. Kolejne wywołanie counter.next spowoduje wykonanie tych samych instrukcji, przy czym tym razem x osiągnie wartość 4. I tak dalej. Ponieważ make_counter zawiera nieskończoną pętlę, więc teoretycznie moglibyśmy robić to w nieskończoność: generator zwiększałby wartość x i wypluwał jego bieżącą wartość. Spójrzmy jednak na bardziej produktywne przypadki użycia generatorów.

Przykład 17.19. Użycie generatorów w miejsce rekurencji

def fibonacci(max):
    a, b = 0, 1       #(1)
    while a < max:
        yield a       #(2)
        a, b = b, a+b #(3)
  1. Ciąg Fibonacciego składa się z liczb, z których każda jest sumą dwóch poprzednich (za wyjątkiem dwóch pierwszych liczb tego ciągu). Ciąg rozpoczynają liczby 0 i 1, kolejne wartości rosną powoli, następnie różnice między nimi coraz szybciej się zwiększają. Aby rozpocząć generowanie tego ciągu, potrzebujemy dwóch zmiennych: a ma wartość 0, b ma wartość 1.
  2. a to bieżąca wartość w ciągu, więc zwracamy ją
  3. b jest kolejną wartością, więc przypisujemy ją do a, jednocześnie obliczając kolejną wartość (a+b) i przypisując ją do b do późniejszego użycia. Zauważcie, że dzieje się to równolegle; jeśli a ma wartość 3 a b ma wartość 5, wówczas w wyniku wartościowania wyrażenia a, b = b, a+b do zmiennej a zostanie przypisana wartość 5 (wcześniejsza wartość b), a do b wartość 8 (suma poprzednich wartości a i b).

Mamy więc funkcję, która wyrzuca z siebie kolejne wartości ciągu Fibonacciego. Oczywiście moglibyśmy to samo osiągnąć przy pomocy rekurencji, jednak ten sposób jest znacznie prostszy w zapisie. No i świetnie się sprawdza w przypadku pętli:

Przykład 17.20. Generatory w pętlach

>>> for n in fibonacci(1000): #(1)
...     print n,              #(2)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
  1. Generatory (takie jak fibonacci) mogą być używane bezpośrednio w pętlach. Pętla for utworzy obiekt generatora i będzie wywoływać na nim metodę next(), przypisując ją do zmiennej sterującej pętli (n).
  2. W każdym przebiegu pętli for n będzie miało nową wartość, zwróconą przez generator w instrukcji yield funkcji fibonacci, i ta wartość zostanie wypisana. Jeśli generatorowi fibonacci skończą się generowane wartości (zmienna a przekroczy max, które w tym przypadku jest równe 1000), pętla for zakończy działanie.

OK, wróćmy teraz do funkcji plural i sprawdźmy, jak tam został użyty generator.

Przykład 17.21. Generator tworzący dynamicznie funkcje

def rules(language):                                                                 
    for line in file('rules.%s' % language):                                          #(1)
        pattern, search, replace = line.split()                                       #(2)
        yield lambda word: re.search(pattern, word) and re.sub(search, replace, word) #(3)

def plural(noun, language='en'):      
    for applyRule in rules(language):  #(4)
        result = applyRule(noun)      
        if result: return result
  1. for line in file(...) to często spotykany w języku Python idiom służący wczytaniu po jednej wszystkich linii z pliku. Działa on w ten sposób, że file zwraca generator, którego metoda next() zwraca kolejną linię z pliku. To szalenie fajna sprawa, robię się cały mokry, kiedy tylko o tym pomyślę[1].
  2. Tu nie ma żadnej magii. Pamiętajcie, że każda linia w pliku z regułami zawiera trzy wartości oddzielone białym znakiem, a więc line.split zwróci trzyelementową krotkę[2], a jej trzy wartości są tu przypisywane do trzech zmiennych lokalnych.
  3. A teraz następuje instrukcja yield. Co zwraca yield? Zwraca ona funkcję zbudowaną dynamicznie przy użyciu notacji lambda, będącą w rzeczywistości domknięciem (używa bowiem zmiennych lokalnych pattern, search oraz replace jako stałych). Innymi słowy, rules jest generatorem który generuje funkcje reprezentujące reguły.
  4. Jeśli rules jest generatorem, to znaczy, że możemy użyć go bezpośrednio w pętli for. W pierwszym przebiegu pętli wywołanie funkcji rules spowoduje otworzenie pliku z regułami, przeczytanie pierwszej linijki oraz dynamiczne zbudowanie funkcji, która dopasowuje i modyfikuje na podstawie pierwszej odczytanej reguły z pliku, po czym nastąpi zwrócenie zbudowanej w ten sposób funkcji w instrukcji yield. W drugim przebiegu pętli for wykonywanie funkcji rules rozpocznie się tam, gdzie się ostatnio zakończyło (a więc w środku pętli for line in file(...)), zostanie więc odczytana druga linia z pliku z regułami, zostanie dynamicznie zbudowana inna funkcja dopasowująca i modyfikująca na podstawie zapisanej w pliku reguły, po czym ta funkcja zostanie zwrócona w instrukcji yield. I tak dalej.

Co takiego osiągnęliśmy w porównaniu z etapem 5? W etapie 5 wczytywaliśmy cały plik z regułami i budowaliśmy listę wszystkich możliwych reguł zanim nawet wypróbowaliśmy pierwszą z nich. Teraz, przy użyciu generatora, podchodzimy do sprawy w sposób leniwy: otwieramy plik i wczytujemy pierwszą linię w celu zbudowania funkcji, jednak jeśli ta funkcja zadziała (reguła zostanie dopasowana, a rzeczownik zmodyfikowany), nie będziemy niepotrzebnie czytać dalej linii z pliku ani tworzyć jakichkolwiek innych funkcji.

Przypisy

  1. Poprawnym tłumaczeniem oryginalnego: "That is so insanely cool, I wet myself just thinking about it." powinno chyba być coś w rodzaju: "To szalenie fajna sprawa, sikam po gaciach tylko o tym myśląc" :-)
  2. Patrz przypis w poprzednim rozdziale