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.