Iterator jest to nic innego jak obiekt pozwalający na sekwencyjny dostęp do kolejnych elementów zawartych w innym obiekcie. warto się z nim zapoznać chociażby po to, żeby podczas rozmowy rekrutacyjnej nie zostać zaskoczonym
Iterator w Python
Python udostępnia bardzo prosty sposób tworzenia iteratorów. Wystarczy, że w klasie zdefiniujemy dwie metody. Są to __iter__() oraz __ next__() . Najczęściej nadpisuje się również __init__() chociażby do przekazania warunku zakończenia.
Metoda __iter__() zwraca obiekt naszego iteratora. To właśnie ta metoda pozwala na wykorzystanie naszego kontenera danych np. w pętli for.
Metoda __ next__() natomiast zwraca kolejny element w kontenerze. Jeśli nie ma już więcej obiektów wywoływany jest wyjątek StopIteration .
Pierwszy iterator
Zatem bez zbędnego przedłużania zbudujmy nasz pierwszy prosty iterator. Niech będzie to po prostu iterator, który zwróci kolejne liczby naturalne. Coś na wzór range ().
class IncrementIterator:
def __init__(self, n):
self.n = n
self.i = 0
def __iter__(self):
return self
def __next__(self):
if self.n == self.i:
raise StopIteration
self.i += 1
return self.i
Krótkie wyjaśnienie. Mamy tu prostą klasę, która ma 3 metody. Pierwsza to
__init__
, w ktorej ustawiamy warunek stop oraz inicjalizujemy zmienną i, która będzie stanowić indeks. Następnie mamy metodę
__iter__
, która zwraca obiekt oraz
__next__
w której znajduje się cała logika. Tu bowiem zwiększamy nasz indeks i go zwracamy.
No dobra, mamy już nasz iterator więc pora go jakoś użyć. Jest to równie proste, co widać niżej:
print([a for a in IncrementIterator(10)]) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Suma składana
Zróbmy coś bardziej skomplikowanego. Spróbujmy napisać iterator, który zwróci sumę liczb od 1 do n, gdzie n to nasza wartość końcowa.
class SumIterator:
def __init__(self, n):
self.n = n
self.i = 0
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.n == self.i:
raise StopIteration
self.i += 1
self.current += self.i
return self.current
print([a for a in SumIterator(10)]) # [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
Tu również nie ma nic skomplikowanego. Różnica polega na tym, że mamy jeszcze jedną zmienną, do której za każdym razem dodajemy wartość naszego indeksu.
Potęgowanie
Przejdźmy do jeszcze bardziej przydatnych zastosowań. Na pewno przynajmniej raz zetknąłeś się z potrzebą potęgowania danej liczby. Dla uproszczenia powiedzmy tu o potęgowaniu do kwadratu. Jak powinien wyglądać taki iterator?
class PowIterator:
def __init__(self, n):
self.n = n
self.i = 0
def __iter__(self):
return self
def __next__(self):
if self.n == self.i:
raise StopIteration
self.i += 1
return self.i * self.i
print([a for a in PowIterator(10)]) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Fibonacci
Jest to bardzo ciekawy ciąg liczb. Rozpoczyna się od dwóch jedynek, a każdy kolejny rozdział jest sumą dwóch poprzednich. Do tej pory najczęściej spotykałem się z rekurencyjnym sposobem rozwiązania tego ciągu. Można to jednak bardzo fajnie zrobić za pomocą iteratora.
class FibIterator:
def __init__(self, n):
self.n = n
self.i = 0
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
if self.n == self.i:
raise StopIteration
self.i += 1
self.a, self.b = self.b, self.a + self.b
return self.a
print([a for a in FibIterator(10)]) # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Przechowujemy dwa elementy i w każdej iteracji jako poprzedni przypisujemy następny, a jako następny sumę poprzednich. Bardzo proste i ciekawe rozwiązanie.
FizzBuzz
Kto przynajmniej raz nie pisał FizzBuzz niech pierwszy rzuci reject ;)
Idea jest stosunkowo prosta. Jeśli podana liczba jest podzielna przez 3 wyświetlamy
Fizz
, a jeśli przez 5 wyświetlamy
Buzz
. W zależności od wersji, przy liczbie podzielnej przez 3 oraz 5 wyświetlany jest
Fizz
,
Buzz
lub
FizzBuzz
. Pamiętam, że podczas swojej pierwszej rekrutacji jako zadanie dostałem właśnie to zadanie w wersji Jack Daniel's, a w obecnej pracy na tym zadaniu uczyliśmy się TDD. Proste zadanie, które po czasie nam zbrzydło. Do tej pory realizowałem to tylko jako funkcję. Dziś postanowiłem spróbować zrealizować FizzBuzz jako iterator i przyznam, że to była dobra decyzja. Sami się przekonajcie:
class SimpleFizzBuzzIterator:
def __init__(self, n):
self.n = n
self.i = 0
def __iter__(self):
return self
def __next__(self):
if self.n == self.i:
raise StopIteration
self.i += 1
if not self.i % 3:
return 'Fizz'
elif not self.i % 5:
return 'Buzz'
else:
return self.i
print([a for a in SimpleFizzBuzzIterator(10)]) # [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz']
Jak wspomniałem, właśnie na tym przykładzie uczyliśmy się TDD i bawiliśmy się różnymi wersjami tego zadania, na przykład dodając więcej niż 2 liczby. Wtedy zauważyłem, że warto to uprościć, bo dodawanie kolejnych elif było mało eleganckie. Tak powstał kod bardzo zbliżony do tego niżej:
class ExtendedFizzBuzzIterator:
def __init__(self, n):
self.n = n
self.i = 0
self.data = {3: 'Ala', 5: 'ma', 7: 'kota'}
def __iter__(self):
return self
def __next__(self):
if self.n == self.i:
raise StopIteration
self.i += 1
for k, v in self.data.items():
if not self.i % k:
return v
return self.i
print([a for a in ExtendedFizzBuzzIterator(10)]) # [1, 2, 'Ala', 4, 'ma', 'Ala', 'kota', 8, 'Ala', 'ma']
Podsumowanie
Jak widać nie taki diabeł straszny. O iteratorach słyszałem kiedyś na studiach, ale nigdy nie zwracałem na to uwagi, zwłaszcza że wtedy uczyłem się C, C++ oraz Javy. Od kiedy pracuję w Python, nie musiałem ich używać i bardzo zdziwiłem się kiedy na ostatniej rozmowie zostałem o to zapytany. Nie potrafiłem napisać nawet prostego iteratora i musiałem zerknąć do dokumentacji. Dla początkujących umiejętność korzystania z iteratorów nie jest niezbędna, ale ich zrozumienie daje ciekawe perspektywy. Omawiając iteratory warto wspomnieć o generatorach, jednak te stosowałem znacznie częściej i uważam, że zasługują na oddzielny wpis. Może nawet powstanie jakiś wpis, który porówna je ze sobą?