Iteratory w Pythonie

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__()
która jest wykonywana przy każdym obiegu pętli. Jeśli chcemy by nasz obiekt mógł działać z pętlą for to musi mieć on swój iterator. Oczywiście nie zawsze on musi być napisany od podstaw, szczególnie jeśli np. mamy obiekt reprezentujący firmę, która jako pole przechowuje listę pracowników i chcielibyśmy móc, iterując po obiekcie firmy, dostawać kolejnych pracowników:
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__()
. Mogłoby to wyglądać tak:
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__()
W powyższym przypadku zwraca ona kolejny element z kolekcji.

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:

Aby potwierdzić swoją subskrypcję, odbierz pocztę i kliknij w link potwierdzający:) jeśli maila nie ma to poczekaj chwile i/lub sprawdź folder spam/inne/oferty itp :)

Aby potwierdzić swoją subskrypcję, odbierz pocztę i kliknij w link potwierdzający:) jeśli maila nie ma to poczekaj chwile i/lub sprawdź folder spam/inne/oferty itp :)
a w ramach prezentu otrzymasz całkowicie za darmo, dwa dokumenty PDF „6 (nie zawsze oczywistych) błędów popełnianych podczas nauki programowania” który jest jednym z efektów ponad siedmioletniej pracy i obserwacji rozwoju niejednego programisty oraz „Wstęp do testowania w Pythonie”, będący wprowadzeniem do biblioteki PyTest.
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__()
jaki będzie następny element, to implementacja pętli for mogłaby wyglądać tak:
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.

Dzięki za wizytę,
Mateusz Mazurek
Mateusz M.

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

  • 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

Ostatnie wpisy

Podsumowanie: maj, czerwiec, lipiec i sierpień 2024

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

2 miesiące ago

Podsumowanie: kwiecień 2024

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

6 miesięcy ago

Podsumowanie: luty i marzec 2024

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

7 miesięcy ago

Podsumowanie: styczeń 2024

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

9 miesięcy ago

Podsumowanie roku 2023

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

10 miesięcy ago

Podsumowanie: grudzień 2023

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

11 miesięcy ago