Mateusz Mazurek – programista z pasją

Blog o Pythonie i kilku innych technologiach. Od developera dla wszystkich.

Programowanie Programowanie webowe

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:
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.

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

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

9 komentarzy
Inline Feedbacks
View all comments

[…] 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 […]

Mateusz H.

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.

Mateusz H.

Ok. Dziki Mateusz za odpowiedz.

Arek

Czytam 3 raz i części nie kumam. Wkurza mnie to, że jestem taki tępy…

Arek

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