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.
Iteracja
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.
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
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)
Własny iterator
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:
- Iterator może być Iterable,
- pojawił się wyjątek StopIteration – mówi on pętli for kiedy ma skończyć iterować,
- trzeba samodzielnie zarządzać pozycją konkretnego elementu,
- raz przeiterowany iterator nie może zostać „cofnięty”. Po prostu próba iteracji na już przeiterowanym iteratorze od razu rzuci nam StopIteration.
a sam kod po prostu wygeneruje 5 losowych liter z zadanego stringa.
Czekaj, stop!
Podoba Ci się to co tworzę? Jeśli tak to zapraszam Cię do zapisania się na newsletter:Jeśli to Cię interesuje to zapraszam również na swoje social media.
Jak i do ewentualnego postawienia mi kawy :)
Pętla for
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.
Itertools
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.
cycle
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
chain
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
compress
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
groupby
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’]
Zmierzając do końca
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.
Mateusz Mazurek
[…] Ale zacznijmy od początku. A dokładniej od przeczytania i zrozumienia mojego artykułu o iteratorach w Pythonie – jest tam sporo wiedzy, która pozwoli nam płynnie wejść z temat […]
[…] Iteratory w Pythonie […]
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