Dziś kolejna koncepcja, po dekoratorach, trochę bardziej zaawansowana. Mianowicie generatory, czyli mechanizm pozwalający zatrzymać wykonywanie funkcji w dowolnym momencie i wznowić je później.. Możemy wyobrazić sobie funkcję wykonującą 1000 operacji lub przetwarzającą ogromne ilości danych – zamiast czekać na zakończenie całości operacji moglibyśmy 'zapauzować’ wykonywanie.
Innymi slowy, moglibyśmy wykonwać 10 operacji z 1000 i się zatrzymać, po czym wykonać jakiś inny fragment kodu. Następnie możemy powrócić do wykonywania operacji i wykonac 11 operację. Jest to niejako rozbrat z ideą funkcji, która po wywołaniu wykonuje się do końca. Można powiedzieć, że jest to koncepcja funkcji przerywanej, pauzowalnej, którą można zatrzymywać, wznawiać i znowu zatrzymywać.
Zacznijmy od pętli
Zacznijmy opowieść o generatorach od pętli while, która będzie w ciele funkcji gen.
Powyższy kod zwróci nam kolejne wartości od 0 do 4, co nie jest zaskoczeniem. Jednak jest pewien problem. Generatory, jako część koncepcji programowania funkcyjnego, nie powinny używać print()
w swoim ciele (generalnie jest to mało eleganckie rozwiązanie). W programowaniu funkcyjnym jedną z zasad jest to by funkcje tylko coś zwracały, a nie wpływały na inne dane, na wyświetlanie danych itp. Tym funkcja nie powinna się zajmować. Ma ona tylko zwrócić wartość. Oczywiście, zwracanie wartości w funkcjach odbywa się zwykle przy użyciu słowa kluczowego return
, jak poniżej.
Generatory i ich słówko kluczowe
Pojawia się tutaj jednak kolejny problem. Słówko return zwraca wartość i od razu kończy funkcję, dlatego jedyne co otrzymamy z powyższego kodu to wartość zero. I tutaj wkracza słówko kluczowe charakterystyczne dla generatorów. Do zwracania wartości bez kończenia funkcji wprowadzono słowo kluczowe yield
. Z pomocą tego słówka kluczowego możemy zwracać każdą kolejną wartość, nie przerywając jednocześnie działania pętli, ani funkcji. Zobaczmy przykład.
Mamy postęp, funkcja nie została przerwana, a jej wywołanie i wyświetlenie zwróciło po prostu obiekt tej funkcji. Nie do końca o to chodziło, ale jakiś postęp mamy. W tym przypadku, jest to funkcja generatora, czyli swego rodzaju iterator, coś po czym można przechodzić krok po kroku – jak rage w pętli.
Spróbujmy zatem użyć wywołania takiej funkcji po prostu zamiast funkcji range w pętli for i zobaczmy co się stanie.
Jak widzimy, uzyskaliśmy poprawny wynik — wszystkie wartości zostały wygenerowane i zwrócone kolejno. Czyli po prostu każde kolejne przejście pętli zaowocowało kolejną wartością jaka się kryła w generatorze. Innymi słowy generator w momencie wywołania trzymał wszystkie kolejne wartości jakie wygenerowano w pętli while, a pętla for je po prostu odczytała z niego i wyświetliła po kolei.
Generatory a funkcja range()
No dobrze, ale czym różni się to od funkcji range()
? Przecież zachowuje się tak samo. Do tej pory rzeczywiście wygląda to, jakbyśmy po prostu zamienili range()
w pętli for
na gen()
. Mówiliśmy, że generator może wykonywać kolejne operacje, ale nie musimy czekać, aż wszystkie zostaną zakończone. I tutaj wchodzi na scenę funkcja next(). Funkcja ta pozwoli nam zrealizować to, by generator uruchomić i uzyskać od niego kolejną wartość, a następnie zatrzymać go gdy nie jest potrzebny. Następnie możemy do niego wrócić i otrzymać kolejną wartość. Zobaczmy jak to działa.
Jak widzimy wyżej, otrzymaliśmy cyfrę zero. Zadziało się tak, poniważ wyświetliliśmy w konsoli, za pomocą funkci print() , efekt działania funkcji next() na generatorze. Otrzymaliśmy zatem pierwszą z wygenerowanych wartości. Inaczej mówiąc, generator został uruchomiony, dał nam pierwszą wartość i został zatrzymany. Teraz możemy wykonywać sobie jakąś inną część kodu. Przy normalnej pętli i programie, który domyślnie jest wykonywany jednowątkowo – musielibyśmy czekać na zakończenie pęli.
Wykonywanie innych instrukcji dzięki next()
Co gdybyśmy mieli wygenerować 10000 liczb, albo milion. Musielibyśmy poczekać, aż wszystkie wartości zostaną wygenerowane. Co w przypadku gdy musielibyśmy działać na ogromny bazach, albo ogromnych ilościach plików – róznież musielibyśmy czekać na wykonanie wszystkich tych operacji.
Generatory pozwalają nam odejść od tradycyjnego podejścia, w którym funkcja musi wykonać się od początku do końca bez przerw. Możemy wykonać jakąś część operacji, a do reszty ewentualnie wrócić później. Bo każde użycie funkcji next() zwróci nam kolejną wygenerowaną wartość, ale nie zobliguje nas do generowania ich wszystkich na raz.
Jak można zauważyć, każde użycie funkcji next() z referencją do generatora pozwoliło na uzyskiwanie kolejnych liczb wygenerowanych przez generator, a pomiędzy tymi instrukcjami mieliśmy swobodę wykonywania innych instrukcji. Na tym polega właśnie siła generatora, nie jesteśmy zmuszeni czekać na zakończenie wszystkich operacji realizowanych w funkcji. Możemy wykonać ich część.
Jest to teoretycznie od zera do nieskończoności, ale możemy od generatora zażądać tylko by dał, nam liczbę 1, a później 2, a później 3, ale tylko wtedy gdy zajdzie taka potrzeba.