Fantastyczne iteratory i jak je tworzyć

Zdjęcie John Matychuk na Unsplash

Problem

Podczas nauki w Make School widziałem, jak moi rówieśnicy piszą funkcje, które tworzą listy przedmiotów.

s = „baacabcaab”
p = „a”
def find_char (ciąg, znak):
  indices = list ()
  dla indeksu str_char in enumerate (string):
    jeśli str_char == znak:
      indices.append (indeks)
  wskaźniki zwrotu
print (find_char (s, p)) # [1, 2, 4, 7, 8]

Ta implementacja działa, ale stwarza kilka problemów:

  • Co jeśli chcemy tylko pierwszego wyniku; czy będziemy musieli stworzyć zupełnie nową funkcję?
  • Co jeśli wszystko, co robimy, to zapętlenie wyniku raz, czy musimy przechowywać każdy element w pamięci?

Iteratory są idealnym rozwiązaniem tych problemów. Działają one jak „leniwe listy” w tym, że nie zwracają listy z każdą wygenerowaną wartością i zwracają każdy element pojedynczo.

Iteratory leniwie zwracają wartości; oszczędzanie pamięci.

Zajmijmy się nimi!

Wbudowane Iteratory

Najczęściej stosowanymi iteratorami są enumerate () i zip (). Obie te leniwie zwracają z nimi wartości przez next ().

range () nie jest jednak iteratorem, ale „leniwym iterowalnym”. - Objaśnienie

Możemy przekonwertować range () w iterator za pomocą iter (), więc zrobimy to dla naszych przykładów w celu nauki.

my_iter = iter (zakres (10))
print (next (my_iter)) # 0
print (next (my_iter)) # 1

Przy każdym wywołaniu next () otrzymujemy kolejną wartość z naszego zakresu; ma sens, prawda? Jeśli chcesz przekonwertować iterator na listę, po prostu daj mu konstruktor listy.

my_iter = iter (zakres (10))
print (lista (mój_iter)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Jeśli naśladujemy to zachowanie, zaczniemy rozumieć, jak działają iteratory.

my_iter = iter (zakres (10))
moja_lista = lista ()
próbować:
  podczas gdy prawda:
    my_list.append (next (my_iter))
oprócz StopIteration:
  przechodzić
print (moja_lista) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Widać, że musieliśmy zawinąć go w instrukcję try catch. To dlatego, że iteratory podnoszą StopIteration, gdy są wyczerpane.

Więc jeśli zadzwonimy dalej na naszym iteratorze z wyczerpanym zasięgiem, otrzymamy ten błąd.

next (my_iter) # Raises: StopIteration

Robienie iteratora

Spróbujmy utworzyć iterator, który będzie zachowywał się jak zakres z samym argumentem stop, używając trzech popularnych typów iteratorów: klas, funkcji generatora (Yield) i wyrażeń generatora

Klasa

Stary sposób tworzenia iteratora polegał na użyciu wyraźnie określonej klasy. Aby obiekt był iteratorem, musi implementować __iter __ (), który zwraca siebie, i __next __ (), która zwraca następną wartość.

klasa my_range:
  _current = -1
  def __init __ (self, stop):
    self._stop = stop
  def __iter __ (self):
    wróć do siebie
  def __next __ (self):
    self._current + = 1
    if self._current> = self._stop:
      podnieść StopIteration
    zwraca self._current
r = mój_zakres (10)
drukuj (lista (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Nie było to zbyt trudne, ale niestety musimy śledzić zmienne między wywołaniami next (). Osobiście nie podoba mi się płyta ani zmiana sposobu myślenia o pętlach, ponieważ nie jest to rozwiązanie typu drop-in, więc wolę generatory

Główną zaletą jest to, że możemy dodawać dodatkowe funkcje, które modyfikują jego wewnętrzne zmienne, takie jak _stop lub tworzyć nowe iteratory.

Iteratory klas mają tę wadę, że wymagają płyty kotłowej, mogą jednak mieć dodatkowe funkcje modyfikujące stan.

Generatory

PEP 255 wprowadził „proste generatory” za pomocą słowa kluczowego fed.

Dzisiaj generatory są iteratorami, które są po prostu łatwiejsze do wykonania niż ich odpowiedniki klasowe.

Funkcja generatora

Funkcje generatora są ostatecznie omawiane w tym PEP i są moim ulubionym typem iteratora, więc zacznijmy od tego.

def my_range (stop):
  indeks = 0
  podczas gdy indeks 
r = mój_zakres (10)
drukuj (lista (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Czy widzisz, jak piękne są te 4 linie kodu? Jest nieco krótszy niż wdrożenie naszej listy, aby go uzupełnić!

Generator działa iteratorami z mniejszą liczbą kotłów niż klasy o normalnym przepływie logicznym.

Generator działa automatycznie „wstrzymuje” wykonywanie i zwraca określoną wartość przy każdym wywołaniu funkcji next (). Oznacza to, że do pierwszego następnego () wywołania nie jest uruchamiany żaden kod.

Oznacza to, że przepływ jest taki:

  1. wywoływana jest funkcja next (),
  2. Kod jest wykonywany do następnej instrukcji dochodu.
  3. Zwracana jest wartość po prawej stronie dochodu.
  4. Wykonanie jest wstrzymane.
  5. Powtarza się 1–5 dla każdego następnego () wywołania, aż zostanie trafiony ostatni wiersz kodu.
  6. StopIteracja została podniesiona.

Funkcje generatora pozwalają również na wykorzystanie wydajności ze słowa kluczowego, które Future next () wywołuje do innej iteracji, dopóki iterable nie zostanie wyczerpane.

def feded_range ():
  zysk z my_range (10)
print (lista (feded_range ())) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

To nie był szczególnie skomplikowany przykład. Ale możesz to zrobić nawet rekurencyjnie!

def my_range_recursive (stop, current = 0):
  jeśli prąd> = stop:
    powrót
  wydajność prądu
  zysk z my_range_recursive (stop, current + 1)
r = my_range_recursive (10)
drukuj (lista (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Wyrażenie generatora

Wyrażenia generatora pozwalają nam tworzyć iteratory jako jednowierszowe i są dobre, gdy nie musimy nadawać mu funkcji zewnętrznych. Niestety nie możemy utworzyć kolejnego my_range za pomocą wyrażenia, ale możemy pracować nad iterowalnymi elementami, takimi jak nasza ostatnia funkcja my_range.

my_doubled_range_10 = (x * 2 dla x w my_range (10))
print (lista (my_doubled_range_10)) # 0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Fajne jest to, że wykonuje następujące czynności:

  1. Lista prosi my_doubled_range_10 o następną wartość.
  2. my_doubled_range_10 pyta my_range o następną wartość.
  3. my_doubled_range_10 zwraca wartość my_range pomnożoną przez 2.
  4. Lista dołącza wartość do siebie.
  5. 1–5 powtarzaj, aż my_doubled_range_10 podniesie StopIteration, co dzieje się, gdy my_range robi.
  6. Zwracana jest lista zawierająca każdą wartość zwróconą przez my_doubled_range.

Możemy nawet filtrować za pomocą wyrażeń generatora!

my_even_range_10 = (x dla x w my_range (10) jeśli x% 2 == 0)
print (lista (my_even_range_10)) # [0, 2, 4, 6, 8]

Jest to bardzo podobne do poprzedniego, z wyjątkiem tego, że my_even_range_10 zwraca tylko wartości, które pasują do danego warunku, więc tylko wartości z zakresu [0, 10).

Przez cały czas tworzymy listę tylko dlatego, że tak nam kazano.

Korzyść

Źródło

Ponieważ generatory są iteratorami, iteratory są iterowalnymi, a iteratory leniwie zwracają wartości. Oznacza to, że korzystając z tej wiedzy, możemy tworzyć obiekty, które dadzą nam przedmioty tylko wtedy, gdy o nie poprosimy i ile tylko zechcemy.

Oznacza to, że możemy przekazywać generatory do funkcji, które się redukują.

print (suma (my_range (10))) # 45

Obliczanie sumy w ten sposób pozwala uniknąć tworzenia listy, gdy wszystko, co robimy, to dodawanie ich razem, a następnie odrzucanie.

Możemy przepisać pierwszy przykład, aby był znacznie lepszy, używając funkcji generatora!

s = „baacabcaab”
p = „a”
def find_char (ciąg, znak):
  dla indeksu str_char in enumerate (string):
    jeśli str_char == znak:
      wskaźnik wydajności
print (lista (find_char (s, p))) # [1, 2, 4, 7, 8]

Od razu może nie być oczywistych korzyści, ale przejdźmy do mojego pierwszego pytania: „co, jeśli chcemy tylko pierwszego rezultatu; czy będziemy musieli stworzyć zupełnie nową funkcję? ”

Dzięki funkcji generatora nie musimy przepisywać tyle logiki.
print (next (find_char (s, p))) # 1

Teraz możemy pobrać pierwszą wartość listy podaną przez nasze oryginalne rozwiązanie, ale w ten sposób otrzymujemy tylko pierwsze dopasowanie i przestajemy iterować listę. Generator zostanie następnie odrzucony i nic więcej nie zostanie utworzone; ogromnie oszczędzająca pamięć.

Wniosek

Jeśli kiedykolwiek tworzysz funkcję, gromadzi wartości na takiej liście.

def foo (bar):
  wartości = []
  dla x w barach:
    # trochę logiki
    wartości.append (x)
  zwracane wartości

Zastanów się nad tym, aby zwracał iterator z klasą, funkcją generatora lub wyrażeniem generatora w następujący sposób:

def foo (bar):
  dla x w barach:
    # trochę logiki
    wydajność x

Zasoby i źródła

PEP

  • Generatory
  • Wyrażenia generatora PEP
  • Wydajność z PEP

Artykuły i wątki

  • Iteratory
  • Iterable vs Iterator
  • Dokumentacja generatora
  • Iteratory kontra generatory
  • Wyrażenie generatora a funkcja
  • Generatory Recrusive

Definicje

  • Iterowalny
  • Iterator
  • Generator
  • Generator Iterator
  • Wyrażenie generatora

Pierwotnie opublikowany na https://blog.dacio.dev/2019/05/03/python-iterators-and-generators/.