Wydajność Pythona, generatory i wyrażenia generatorów

W tym samouczku nauczysz się, jak łatwo tworzyć iteracje przy użyciu generatorów Pythona, czym różni się od iteratorów i zwykłych funkcji oraz dlaczego warto go używać.

Wideo: generatory języka Python

Generatory w Pythonie

Tworzenie iteratora w Pythonie wymaga dużo pracy. Musimy zaimplementować klasę z metodą __iter__()i __next__(), śledzić stany wewnętrzne i podnosić, StopIterationgdy nie ma żadnych wartości do zwrócenia.

Jest to długie i sprzeczne z intuicją. W takich sytuacjach na ratunek przychodzi generator.

Generatory Pythona to prosty sposób tworzenia iteratorów. Cała praca, o której wspomnieliśmy powyżej, jest automatycznie obsługiwana przez generatory w Pythonie.

Mówiąc najprościej, generator to funkcja zwracająca obiekt (iterator), po którym możemy iterować (jedna wartość na raz).

Twórz generatory w Pythonie

Utworzenie generatora w Pythonie jest dość proste. Jest to tak proste, jak zdefiniowanie normalnej funkcji, ale z yieldinstrukcją zamiast returninstrukcji.

Jeśli funkcja zawiera co najmniej jedną yieldinstrukcję (może zawierać inne yieldlub returninstrukcje), staje się funkcją generującą. Oba yieldi returnzwrócą pewną wartość z funkcji.

Różnica polega na tym, że podczas gdy returninstrukcja całkowicie kończy funkcję, yieldinstrukcja wstrzymuje funkcję zapisując wszystkie jej stany, a następnie kontynuuje od tego momentu kolejne wywołania.

Różnice między funkcją generatora a normalną funkcją

Oto jak funkcja generatora różni się od normalnej funkcji.

  • Funkcja generatora zawiera jedną lub więcej yieldinstrukcji.
  • Po wywołaniu zwraca obiekt (iterator), ale nie rozpoczyna wykonywania natychmiast.
  • Metody takie jak __iter__()i __next__()są wdrażane automatycznie. Możemy więc przeglądać elementy za pomocą next().
  • Gdy funkcja daje wynik, funkcja jest wstrzymywana, a kontrola jest przekazywana wywołującemu.
  • Zmienne lokalne i ich stany są zapamiętywane pomiędzy kolejnymi wywołaniami.
  • Wreszcie, gdy funkcja kończy działanie, StopIterationjest automatycznie wywoływana przy kolejnych wywołaniach.

Oto przykład ilustrujący wszystkie powyższe punkty. Mamy funkcję generatora o nazwie my_gen()zawierającej kilka yieldinstrukcji.

 # A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n

Poniżej przedstawiono interaktywny przebieg w tłumaczu. Uruchom je w powłoce Pythona, aby zobaczyć dane wyjściowe.

 >>> # It returns an object but does not start execution immediately. >>> a = my_gen() >>> # We can iterate through the items using next(). >>> next(a) This is printed first 1 >>> # Once the function yields, the function is paused and the control is transferred to the caller. >>> # Local variables and theirs states are remembered between successive calls. >>> next(a) This is printed second 2 >>> next(a) This is printed at last 3 >>> # Finally, when the function terminates, StopIteration is raised automatically on further calls. >>> next(a) Traceback (most recent call last):… StopIteration >>> next(a) Traceback (most recent call last):… StopIteration

Interesującą rzeczą do odnotowania w powyższym przykładzie jest to, że wartość zmiennej n jest zapamiętywana między każdym wywołaniem.

W przeciwieństwie do zwykłych funkcji, zmienne lokalne nie są niszczone, gdy funkcja daje wynik. Ponadto obiekt generatora można powtórzyć tylko raz.

Aby zrestartować proces, musimy utworzyć inny obiekt generatora za pomocą czegoś takiego jak a = my_gen().

Ostatnią rzeczą, na którą należy zwrócić uwagę, jest to, że możemy bezpośrednio używać generatorów z pętlami for.

Dzieje się tak, ponieważ forpętla przyjmuje iterator i iteruje po nim za pomocą next()funkcji. Automatycznie kończy się, gdy StopIterationzostanie podniesiony. Sprawdź tutaj, aby dowiedzieć się, w jaki sposób pętla for jest faktycznie zaimplementowana w Pythonie.

 # A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n # Using for loop for item in my_gen(): print(item)

Po uruchomieniu programu wynik będzie następujący:

 To jest drukowane jako pierwsze 1 To jest drukowane jako drugie 2 To jest drukowane jako ostatnie 3

Generatory Pythona z pętlą

Powyższy przykład jest mniej przydatny i przestudiowaliśmy go tylko po to, aby zorientować się, co dzieje się w tle.

Zwykle funkcje generatora są realizowane z pętlą mającą odpowiedni warunek zakończenia.

Weźmy przykład generatora, który odwraca ciąg.

 def rev_str(my_str): length = len(my_str) for i in range(length - 1, -1, -1): yield my_str(i) # For loop to reverse the string for char in rev_str("hello"): print(char)

Wynik

 olleh

W tym przykładzie użyliśmy range()funkcji, aby uzyskać indeks w odwrotnej kolejności za pomocą pętli for.

Uwaga : ta funkcja generatora działa nie tylko z ciągami, ale także z innymi rodzajami iterowalnych, takich jak lista, krotka itp.

Wyrażenie generatora Pythona

Proste generatory można łatwo tworzyć w locie za pomocą wyrażeń generatora. Ułatwia to budowanie generatorów.

Podobnie jak funkcje lambda, które tworzą funkcje anonimowe, wyrażenia generatora tworzą anonimowe funkcje generatora.

Składnia wyrażeń generatora jest podobna do składni list złożonych w Pythonie. Ale nawiasy kwadratowe są zastępowane okrągłymi nawiasami.

Główną różnicą między zrozumieniem listy a wyrażeniem generującym jest to, że zrozumienie listy tworzy całą listę, podczas gdy wyrażenie generujące generuje jeden element na raz.

They have lazy execution ( producing items only when asked for ). For this reason, a generator expression is much more memory efficient than an equivalent list comprehension.

 # Initialize the list my_list = (1, 3, 6, 10) # square each term using list comprehension list_ = (x**2 for x in my_list) # same thing can be done using a generator expression # generator expressions are surrounded by parenthesis () generator = (x**2 for x in my_list) print(list_) print(generator)

Output

 (1, 9, 36, 100) 

We can see above that the generator expression did not produce the required result immediately. Instead, it returned a generator object, which produces items only on demand.

Here is how we can start getting items from the generator:

 # Initialize the list my_list = (1, 3, 6, 10) a = (x**2 for x in my_list) print(next(a)) print(next(a)) print(next(a)) print(next(a)) next(a)

When we run the above program, we get the following output:

 1 9 36 100 Traceback (most recent call last): File "", line 15, in StopIteration

Generator expressions can be used as function arguments. When used in such a way, the round parentheses can be dropped.

 >>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100

Use of Python Generators

There are several reasons that make generators a powerful implementation.

1. Easy to Implement

Generators can be implemented in a clear and concise way as compared to their iterator class counterpart. Following is an example to implement a sequence of power of 2 using an iterator class.

 class PowTwo: def __init__(self, max=0): self.n = 0 self.max = max def __iter__(self): return self def __next__(self): if self.n> self.max: raise StopIteration result = 2 ** self.n self.n += 1 return result

The above program was lengthy and confusing. Now, let's do the same using a generator function.

 def PowTwoGen(max=0): n = 0 while n < max: yield 2 ** n n += 1

Since generators keep track of details automatically, the implementation was concise and much cleaner.

2. Memory Efficient

A normal function to return a sequence will create the entire sequence in memory before returning the result. This is an overkill, if the number of items in the sequence is very large.

Generator implementation of such sequences is memory friendly and is preferred since it only produces one item at a time.

3. Represent Infinite Stream

Generatory są doskonałymi nośnikami do reprezentowania nieskończonego strumienia danych. Nieskończone strumienie nie mogą być przechowywane w pamięci, a ponieważ generatory wytwarzają tylko jeden element naraz, mogą reprezentować nieskończony strumień danych.

Następująca funkcja generatora może wygenerować wszystkie liczby parzyste (przynajmniej w teorii).

 def all_even(): n = 0 while True: yield n n += 2

4. Generatory rurociągów

Do realizacji szeregu operacji można użyć wielu generatorów. Najlepiej zilustrować to na przykładzie.

Załóżmy, że mamy generator, który generuje liczby w szeregu Fibonacciego. Mamy też inny generator do kwadratu liczb.

Jeśli chcemy znaleźć sumę kwadratów liczb w szeregu Fibonacciego, możemy to zrobić w następujący sposób, potokując razem wyjście funkcji generatora.

 def fibonacci_numbers(nums): x, y = 0, 1 for _ in range(nums): x, y = y, x+y yield x def square(nums): for num in nums: yield num**2 print(sum(square(fibonacci_numbers(10))))

Wynik

 4895

Te potoki są wydajne i łatwe do odczytania (i tak, dużo fajniejsze!).

Interesujące artykuły...