Iteratory i iterable

Zastanawialiście się kiedyś, dlaczego poniższy program działa:

1lista = [ (i * 101) % 23 for i in range(20) ]
2for l in lista: print(l)

Obiekt klasy list jest iterable, ponieważ potrafi zwrócić iterator do swojej zawartości. W Pythonie osiąga się to przez dodanie do klasy magicznej metody __iter__(). Czym zatem jest iterator? Iterator to obiekt, który potrafi podać następny obiekt w sekwencji. W Pythonie osiąga się to przez dodanie do klasy magicznej metody __next__().

 1class EvenNumbers:
 2
 3    def __init__(self,stop = 1000, start = 0) : 
 4        self.__i = start - 2
 5        self.__n = stop
 6
 7    def __iter__(self) :
 8        return self
 9
10    def __next__(self):
11        if self.__i < self.__n:
12            self.__i += 2
13            return self.__i
14        else : raise StopIteration()
15
16if __name__ == "__main__" :
17
18    for k in EvenNumbers() : print(k)

A oto już zupełnie praktyczny przykład iteratora po zakresie liczb rzeczywistych:

 1class EvenlySpacedFloats:
 2
 3    __slots__ = [ '__from','__to','__step','__x']
 4
 5    def __init__(self, begin, step, end):
 6        self.__from = begin
 7        self.__to = end
 8        self.__step = step
 9        self.__x = begin - self.__step 
10
11    def __iter__(self) :
12        return self
13
14    def __next__(self):
15        self.__x += self.__step 
16        if self.__x < self.__to:
17            return self.__x
18        else : raise StopIteration()
19
20
21if __name__ == "__main__" :
22    for x in EvenlySpacedFloats(0.0,0.1,1.0) : 
23        print(x)

Kolejny przykład iteratora podaje wybrane elementy źródłowej listy wg zadanej kolejności. Konstruktor tego obiektu przyjmuje źródłową listę oraz dodatkowo listę indeksów, definiującą kolejność iteracji:

 1class CustomOrderIterator:
 2    __slots__ = ['__source_list', '__elements', '__current']
 3
 4    def __init__(self, source_list, elements):
 5        self.__source_list = source_list
 6        self.__elements = elements
 7        self.__current = 0
 8
 9    def __iter__(self):
10        return self
11
12    def __next__(self):
13        self.__current += 1
14        if self.__current <= len(self.__elements):
15            return self.__source_list[self.__elements[self.__current-1]]
16        else:
17            raise StopIteration()
18
19
20if __name__ == "__main__":
21    letters = list("ABCDEFGHIJKLMNOP")
22    order = [2, 3, 4, 1, 8, 0, 2, 2]
23    for x in CustomOrderIterator(letters, order):
24        print(x)

W powyższych przykładach trzech przykładach klasy są zarówno iterable jak i iteratorami, co niestety często nie jest dobrym rozwiązaniem. Iterator bowiem jest obiektem, który musi pamiętać aktualny stan iteracji, tzn na czym stanęło ostatnie wywołanie metody __next__(). Wywołanie ponownie __iter__() z tego samego obiektu co poprzednio przez zakończeniem poprzedniej iteracji psuje już istniejący iterator. Powyższe przykłady działały, bowiem każda pętla tworzyła nowy obiekt, np tu:

    for x in EvenlySpacedFloats(0.0,0.1,1.0) : 
        print(x)

albo tu:

    for k in EvenNumbers() : print(k)

Instrukcje EvenNumbers() oraz EvenlySpacedFloats(0.0,0.1,1.0) są wywołaniami konstruktorów a zatem tworzą nowe obiekty. Kiedy zatem to nie będzie działać? Problem ten ilustruje poniższy przykład. Uruchom go i przekonaj się, że podwójna iteracja jest błędna, choć pojedyncza działa prawidłowo.

Przyjmimy, że obiekt VeryBigContainer zawiera dużo danych a jego konstrukcja jest kosztowna. Chcesz uniknąć tworzenia kilku jego kopii. Najpierw stworzysz obiekt tej klasy:

43    data = VeryBigContainer()

zapakujesz do niego odpowiednie dane i rozpoczniesz iterację:

50    for el in data: print(el)

Do tej pory wszystko działa zgodnie z założeniami. Problem pojawia się w podwójnie zagnieżdżonej iteracji:

54    for el_i in data:
55        for el_j in data:
56            print(el_i, el_j)

Fragmenty in data w linii 54 i w linii 55 odwołują się do tego samego obiektu data. Instrukcja for el_j in data: powoduje wywołanie metody __next__() co zmienia wewnętrzny stan iteratora a tym samym wpływa na iterację po el_i.

W takiej sytuacji należy rozdzielić funkcjonalność iteratora od iterable pisząc dwie oddzielne klasy: jedną na pojemnik (czyli tu VeryBigContainer) a drugą na iterator.