Cześć.
Iteratory w Pythonie to byt który jest wszechobecny. Praktycznie każdy program w jakiś sposób z nich korzysta. Dziś pokażę czym one są, jak działają i jak się mają do typu Iterable.
Iterowanie po kolekcjach w Pythonie jest bardzo proste, wystarczy mieć konkretną kolekcję np. listę i używając pętli for dostać się do każdego elementu po kolei, np.:
1 2 3 4 | numbers = [1, 2, 3, 4] for number in numbers: print(number) |
Jest to proste, łatwe i przyjemne. Zadajmy sobie jednak pytanie: jak to się dzieje, że jesteśmy w stanie iterować po liście, ale po swoim własnym obiekcie już nie? Dojdźmy do odpowiedzi razem. Najpierw spróbujmy iterować po naszym obiekcie:
1 2 3 4 5 6 7 8 9 10 11 12 13 | from dataclasses import dataclass @dataclass class Dummy: field1: str field2: int dummy_obj = Dummy('aaa', 123) for x in dummy_obj: print(x) |
Uruchomienie takiego kodu skończy się błędem:
Traceback (most recent call last): File "/home/mmazurek/PycharmProjects/iterators/m.py", line 12, in for x in dummy_obj: TypeError: 'Dummy' object is not iterable
Komunikat błędu jest już jakąś wskazówką. Skoro nie możemy iterować po obiektach które nie są typu Iterable, to prawdopodobnie te obiekty, po których iterować możemy, np. lista, są Iterable. Upewnijmy się:
1 2 3 4 5 6 7 8 9 10 11 12 13 | from dataclasses import dataclass from collections.abc import Iterable @dataclass class Dummy: field1: str field2: int dummy_obj = Dummy('aaa', 123) print(isinstance(dummy_obj, Iterable)) print(isinstance([1, 2, 3], Iterable)) |
Co zgodnie z przypuszczeniem wyświetli nam:
False True
Wejdźmy więc głębiej w to czym jest ten magiczny typ Iterable.
W poprzednim akapicie doszliśmy do wniosku, że klasa która działa poprawnie z pętlą for jest typu Iterable. A po prawdzie to odwrotnie – klasy implementując interfejs Iterable zaczynają być zrozumiałe dla pętli for.
Ta idea nie jest niczym nowym, podobne podejście jest w Javie czy PHPie. To co jednak wyróżnia Pythona to sposób sprawienia, żeby klasa była typu Iterable. I Java i PHP zmuszą nas, by po prostu nasz typ implementował odpowiedni, jawnie wskazany interfejs. Python podchodzi do tego trochę inaczej – wprowadza on bogaty zbiór metod magicznych i pozostawia programiście decyzję które z nich zaimplementować. Czyli nie musimy jawnie wskazywać interfejsu, po prostu implementujemy konkretne metody i nasz obiekt staje się Iterable.
W tym przypadku musimy zaimplementować metodę
1 | __iter__() |
Zróbmy to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | from dataclasses import dataclass from collections.abc import Iterable @dataclass class Dummy: field1: str field2: int def __iter__(self): pass dummy_obj = Dummy('aaa', 123) print(isinstance(dummy_obj, Iterable)) print(isinstance([1, 2, 3], Iterable)) for d in dummy_obj: print(d) |
Po uruchomieniu takiego kodu, dostajemy wprawdzie dwa razy True, ale próba iteracja rzuca nam wyjątek:
Traceback (most recent call last):
File "/home/mmazurek/PycharmProjects/iterators/m.py", line 18, in
for d in dummy_obj:
TypeError: iter() returned non-iterator of type 'NoneType'
Informujący nas, że zdefiniowana przez nas funkcja zwraca None’a który nie jest Iteratorem. Pomówmy więc o tym czym jest Iterator.
Iterator to obiekt który odpowiada za obsługę iteracji. Aby klasa stała się iteratorem musi implementować metodę (znów magiczną) o nazwie
1 | __next__() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | from dataclasses import dataclass from typing import List @dataclass class Employee: name: str surname: str @dataclass class Company: name: str def __post_init__(self): self._employees: List[Employee] = [] def hire_employee(self, employee: Employee): self._employees.append(employee) def fire_employee(self, empolyee: Employee): self._employees.remove(empolyee) def __iter__(self): return iter(self._employees) first_employee = Employee('Karol', 'Matczak') second_employee = Employee('Aleksander', 'Tolland') company = Company('Google') company.hire_employee(first_employee) company.hire_employee(second_employee) for employee in company: print(employee) |
Co spowoduje wypisanie:
Employee(name='Karol', surname='Matczak') Employee(name='Aleksander', surname='Tolland')
Aby wyłuskać iterator z już istniejącej kolekcji użyliśmy funkcji iter() i po prostu go zwróciliśmy.
Co natomiast, jeśli nasz obiekt pracownika zyska pole o nazwie gdpr_accepted i tylko pracowników z tym polem ustawionym na wartość True możemy podglądać? To nadal możemy to zrobić tak samo, wyłuskując iterator, jednakże wcześniej musimy odpowiednio przygotować kolejkcję:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | from dataclasses import dataclass, field from typing import List @dataclass class Employee: name: str surname: str gdpr_accepted: bool = False @dataclass class Company: name: str def __post_init__(self): self._employees: List[Employee] = [] def hire_employee(self, employee: Employee): self._employees.append(employee) def fire_employee(self, empolyee: Employee): self._employees.remove(empolyee) def __iter__(self): return iter(filter(lambda x: x.gdpr_accepted, self._employees)) first_employee = Employee('Karol', 'Matczak', True) second_employee = Employee('Aleksander', 'Tolland') company = Company('Google') company.hire_employee(first_employee) company.hire_employee(second_employee) for employee in company: print(employee) |
co wypisze nam oczywiście tylko:
Employee(name='Karol', surname='Matczak', gdpr_accepted=True)
Oczywiście ostatni przykład możemy zaimplementować z użyciem własnoręcznie stworzonego iteratora, czyli tworząc nową klasę implementującą wspomnianą wcześniej metodę
1 | __next__() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | from dataclasses import dataclass from typing import List @dataclass class Employee: name: str surname: str accepted_gdpr: bool = False class CompanyIterator: def __init__(self, employees: List[Employee]): self.__employees_iterator = iter(employees) def __next__(self): while True: employee = next(self.__employees_iterator) if employee.accepted_gdpr: return employee else: continue @dataclass class Company: name: str def __post_init__(self): self._employees: List[Employee] = [] def hire_employee(self, employee: Employee): self._employees.append(employee) def fire_employee(self, empolyee: Employee): self._employees.remove(empolyee) def __iter__(self): return CompanyIterator(self._employees) first_employee = Employee('Karol', 'Matczak', True) second_employee = Employee('Aleksander', 'Tolland') company = Company('Google') company.hire_employee(first_employee) company.hire_employee(second_employee) for employee in company: print(employee) |
Analogicznie do funkcji iter() istnieje funkcja next() która po prostu wykonuje metodę magiczną
1 | __next__() |
Ale znów w przykładzie wyżej korzystamy z czegoś gotowego… Stwórzmy coś całkowicie własnego:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import random import string class RandomLetterIterator: def __init__(self, limit: int): self.limit = limit self.__position = 0 self.__letters = string.ascii_letters def __get_random_letter(self): return random.choice(self.__letters) def __iter__(self): return self def __next__(self): if self.__position < self.limit: self.__position += 1 return self.__get_random_letter() else: raise StopIteration() for x in RandomLetterIterator(5): print(x) |
Zauważyć trzeba trzy rzeczy:
a sam kod po prostu wygeneruje 5 losowych liter z zadanego stringa.
Zbierając całą wiedzę z poprzednich akapitów można dojść do pewnych ciekawych wniosków. Skoro wyjątek StopIteration mówi pętli for kiedy przestać iterować a funkcja metoda
1 | __next__() |
1 2 3 4 5 6 7 8 | iter_obj = iter(iterable) while True: try: element = next(iter_obj) # ciało petli except StopIteration: break |
ot taka ciekawostka.
Jak widzicie, iteratory potrafią naprawdę wiele. Twórcy Pythona również to wiedzą i z tego powodu dostarczają nam w bibliotece standardowej kilkanaście przydatnych iteratorów zgrupowanych w moduł itertools. Zerknijmy na kilka z nich.
1 2 3 4 5 6 7 8 9 10 11 | from itertools import cycle numbers = [0, 2, 4, 6, 8] numbers_iterator = cycle(numbers) max_nums = 10 for _ in range(9): print(next(numbers_iterator)) |
Funkcja cycle tworzy iterator który będzie w nieskończoność powtarzał elementy konkretnego iterabla. To może być przydatniejsze częściej niż się wydaje. Wynik tego kodu to:
0
2
4
6
8
0
2
4
6
1 2 3 4 5 6 7 8 9 10 11 12 | from itertools import chain numbers = [0, 2, 4] name = 'mmazurek.dev' and_on_the_end = 'END' for x in chain(numbers, name, and_on_the_end): print(x) |
Kolejny ciekawy iterator pozwalający na iterację po wielu iterablach zgodnie z kolejnością przekazania w argumentach. Wynik tego kodu to:
0
2
4
m
m
a
z
u
r
e
k
.
d
e
v
E
N
D
1 2 3 4 5 6 7 8 9 | from itertools import compress numbers = [1, 2, 3, 4, 5, 6, 7, 8] selectors = [0, 0, 1, 1, 1, 0, 1] for x in compress(numbers, selectors): print(x) |
Bardzo ciekawy iterator który przeiteruje po tych elementach pierwszego argumentu dla których odpowiadający indeks w iterablu przekazanym jako drugi argument będzie True (lub rzecz jasna Truly). Wynik tego kodu to:
3
4
5
7
Funkcja zwraca iterator który zgrupuje elementy przekazanego iterable’a, np. taki kod:
1 2 3 4 5 6 7 | from itertools import groupby numbers = '5555AAAsssWWWWaaa' for key, value in groupby(numbers): print(key, list(value)) |
wyprodukuje:
5 [’5′, '5′, '5′, '5′]
A [’A’, 'A’, 'A’]
s [’s’, 's’, 's’]
W [’W’, 'W’, 'W’, 'W’]
a [’a’, 'a’, 'a’]
To co jest tu ważne i nie do końca intuicyjne (bo SQL nauczył nas inaczej), to dla tej funkcji kolejność jest ważna. Wręcz kluczowa. Każdy element który przerwie grupę zacznie nową grupę, a więc jeśli przekażemy do groupby napis „aaaAAA555aaaA” to rezultat będzie taki:
a [’a’, 'a’, 'a’]
A [’A’, 'A’, 'A’]
5 [’5′, '5′, '5′]
a [’a’, 'a’, 'a’]
A [’A’]
Iteratory nie są trudnym tematem ale z jakiegoś powodu rzadko spotykam osoby które faktycznie rozumieją ich działanie. Mam nadzieję, że ten wpis poszerzy zacne grono osób posiadających tę wiedzę. A i już teraz zapowiadam, że pojawi się kolejny artykuł, będący bezpośrednią kontynuacją tego, w którym skupimy się na generatorach.
Oj daaawnoo mnie tu nie było. Ale wakacje to był czas dużej liczby intensywnych wyjazdów i tak naprawdę, dopiero jakoś… Read More
Cześć! Zapraszam na krótkie podsumowanie kwietnia. Wyjazd do Niemiec A dokładniej pod granicę z Francją. Chrześnica miała pierwszą komunię. Po… Read More
Ostatnio tygodnie były tak bardzo wypełnione, że nie udało mi się napisać nawet krótkiego podsumowanie. Więc dziś zbiorczo podsumuję luty… Read More
Zapraszam na krótkie podsumowanie miesiąca. Książki W styczniu przeczytałem "Homo Deus: Historia jutra". Książka łudząco podoba do wcześniejszej książki tego… Read More
Cześć! Zapraszam na podsumowanie roku 2023. Książki Zacznijmy od książek. W tym roku cel 35 książek nie został osiągnięty. Niemniej… Read More
Zapraszam na krótkie podsumowanie miesiąca. Książki W grudniu skończyłem czytać Mein Kampf. Nudna książka. Ciekawsze fragmenty można by było streścić… Read More
Pokaż komentarze
Hej Mateusz,
Fajnie przedstawiłeś temat interatorów w Pythonie.
Czy mógłbyś napisać do czego służą te dwa moduły które załączasz do kodu:
from dataclasses import dataclass
from typing import List
Oraz do czego służy ten dekorator?
@dataclass
Iteratory to chyba jeden z łatwiejszych tematów w programowaniu, tak mi się wydaje. Jednak sama implementacja własnego iteratora mogła by zmusić kogoś do dłuższego zastanowienia :-).
Pozdrawiam.
Cześć!
Hej Mateusz,
Fajnie przedstawiłeś temat interatorów w Pythonie.
from dataclasses import dataclass - importuje dataclass, czyli moduł pozwalający na szybsze tworzenie klas
from typing import List - importuje typy, żeby móc określić typy obiektów:D
@dataclass - ten dekorator sprawia, że nie musisz definiowac własnego __init__ tylko zostanie od wygenerowany :D
Ok. Dziki Mateusz za odpowiedz.
Czytam 3 raz i części nie kumam. Wkurza mnie to, że jestem taki tępy...
Bez przesady z tym tępy :D napisz z czym masz problem, może ułożę zdanie inaczej i wtedy zrozumiesz? :)
Cześć, nie ogarnąłem jeszcze dataclass i w tym kłopot, a z OP jeszcze nie jestem dostatecznie obyty bo nie programuję zawodowo. Po wstępnym przeczytaniu Twojego artykułu pozyskuję wiedzę na temat dataclass i powtarzam OP. Jak już to ogarnę to wrócę do tego wpisu i miejmy nadzieję ze więcej zrozumiem. Jak widać nawet iteratory mogą być trudne ;)
Pozdrawiam
Trzymam kciuki i pozdrawiam! :)
W jakich przypadkach użyłeś tego w realnym projekcie w web dev?
Nie pracuje w webdevie:D