Iteratory w Python


Poznaj zasady działania iteratorów i przekonaj się jak za ich pomocą łatwo urozmaicić twoją aplikację.


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, 3, 6, 10, 15, 21, 28, 36, 45, 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 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ą?

Maj 21, 2019

Najnowsze wpisy

Zobacz wszystkie