🧩 Iteratory
Iteratory
Iterator to obiekt, który zwraca kolejne elementy na żądanie.
W Pythonie większość konstrukcji iteracyjnych (for, list(...), sum(...), any(...)) działa właśnie na iteratorach.
Najważniejsza idea: przetwarzamy elementy jeden po drugim, zamiast budować całą kolekcję naraz.
Iterable vs iterator
To dwa różne pojęcia:
- Iterable: obiekt, po którym można iterować (np.
list,tuple,str), czyli ma__iter__(). - Iterator: obiekt, który pamięta aktualny stan iteracji i ma
__next__().
W praktyce:
iterable --> iter() --> iteratornext(iterator)zwraca kolejne elementy- po wyczerpaniu danych
next(...)zgłaszaStopIteration
liczby = [10, 20, 30] # iterable
it = iter(liczby) # iterator
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
Jak działa pętla for "pod maską"?
Pętla:
for x in [1, 2, 3]:
print(x)
jest koncepcyjnie równoważna:
it = iter([1, 2, 3])
while True:
try:
x = next(it)
print(x)
except StopIteration:
break
Własny iterator (klasa)
Poniżej prosty iterator odliczający w dół:
class CountdownIterator:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for number in CountdownIterator(3):
print(number) # 3, 2, 1, 0
Kiedy iterator klasowy ma sens?
- gdy potrzebujesz pełnej kontroli nad stanem iteracji,
- gdy logika jest bardziej złożona niż jedno
yield, - gdy iterator ma mieć dodatkowe metody pomocnicze.
Iterator vs generator
Generator to wygodny sposób tworzenia iteratora. Oba podejścia robią podobną rzecz (zwracają kolejne elementy na żądanie), ale implementuje się je inaczej:
- Iterator (klasa) - samodzielnie implementujesz
__iter__()i__next__(), więc masz pełną kontrolę nad stanem i logiką. - Generator (
yield) - Python automatycznie tworzy obiekt iteratora, a stan jest zapamiętywany między kolejnymiyield.
# Generator:
def countdown_generator(start):
while start >= 0:
yield start
start -= 1
Dlaczego robimy oba podejścia?
Wcześniej (w zaj02/generatory.md) ciąg Fibonacciego był realizowany przez generator.
Teraz implementujemy ten sam problem jako własny iterator klasowy FibonacciIterator(max_elements), żeby przećwiczyć protokół iteratora (__iter__, __next__, StopIteration) i zobaczyć różnicę w podejściu.
📝 Zadania
W ramach nowego modułu python1course.zaj04.fibonacci:
-
Stwórz własny iterator (klasę)
FibonacciIterator(max_elements), który generuje ciąg liczb Fibonacciego. Ciąg Fibonacciego zaczyna się od0, 1, a każda kolejna liczba to suma dwóch poprzednich. -
Obsłuż przypadki brzegowe:
max_elements = 0(brak elementów),max_elements = 1(tylko0),- niepoprawna wartość (np. liczba ujemna) - rzuć
ValueError.
-
Dodaj krótki kod porównawczy:
- uruchomienie wersji iteratorowej (to zadanie),
- uruchomienie wersji generatorowej z wcześniejszych zajęć (
zaj02), - porównanie czy obie dają ten sam wynik dla np. 10 elementów.
Kod wykonawczy umieść w głównym folderze projektu, np. main_zajecia04.py.
📝 Zadanie integracyjne - iterator w projekcie z zaj03
W zaj03 klasa IncidentQueue ma już zaimplementowane __iter__() i __next__() - działa, ale ma pewną wadę projektową.
Zmodyfikuj IncidentQueue tak, żeby zamiast tego implementowała własny, klasowy iterator:
- Stwórz klasę
IncidentQueueIteratorz metodami__iter__()i__next__(), która iteruje po incydentach z kolejki. - Zmień
IncidentQueue.__iter__()tak, żeby zwracała instancjęIncidentQueueIterator. - Upewnij się, że zewnętrzny kod korzystający z
for incident in queue:działa identycznie jak przed zmianą.
Jaki problem ma obecna implementacja?
Wróć do rozróżnienia z początku tego tematu: iterable to obiekt po którym można iterować, iterator to obiekt który pamięta stan iteracji. To dwa różne byty.
Aktualna IncidentQueue jest jednocześnie iterable i iteratorem - ma __iter__ zwracający self oraz __next__:
def __iter__(self):
self._index = 0 # stan na obiekcie kolejki!
return self # kolejka zwraca samą siebie jako iterator
def __next__(self):
...
Innymi słowy: IncidentQueue jest iterable i iteratorem w jednym. Taki obiekt może istnieć tylko w jednej iteracji naraz - bo ma tylko jeden _index.
Przy jednej pętli działa poprawnie. Problem pojawia się przy zagnieżdżonych pętlach:
for outer in queue:
for inner in queue: # to wywołuje queue.__iter__() i resetuje _index = 0
print(inner) # inner iteruje od początku przy każdym obrocie outer
# outer nigdy nie dojdzie do kolejnych elementów - _index jest cały czas zerowany
Zagnieżdżona iteracja jest rzadka, ale realna - np. gdy porównujesz każdy incydent z każdym innym.
Po refaktoryzacji na IncidentQueueIterator każda pętla dostaje własną instancję iteratora z własnym _index, więc działają niezależnie:
def __iter__(self):
return IncidentQueueIterator(self.__queue) # nowy obiekt z własnym stanem
for outer in queue:
for inner in queue: # tworzy nowy IncidentQueueIterator, nie resetuje outer
print(inner) # działa poprawnie