Blog JSystems - z miłości do programowania

Generatory



Generator jest funkcją która może zostać wstrzymana i wznowiona od miejsca w którym została wstrzymana. Generatory cechują się leniwą ewaluacją. Tworzą kolejne elementy dopiero w momencie odwołania się do generatora. Pozwala nam to wydajniej wykorzystywać pamięć operacyjną i zwracać z generatora nieskończoną liczbę elementów


Generatory nie tworzą całej zwracanej kolekcji od razu, tylko każda kolejna wartość jest generowana w momencie jej pobrania. To duża oszczędność dla pamięci, ponieważ w dowolnym momencie możemy przerwać pobieranie kolejnych wartości. Nie moglibysmy tego zrobić gdyby funkcja zwracała listę, choć sposób iteracji byłby taki sam. W przypadku generatorów w pamięci przechowywany jest tylko jeden aktualnie pobierany element a nie cała lista, co pozwala nam przetwarzać zbiory w zasadzie nieskończenie długie, czego nie moglibysmy zrobić w przypadku zwracania listy. Lista mogłaby przepełnić dostępną pamięć.


Z zagadnień podstawowych pamiętamy taką konstrukcję:


for x in range(10):
    print(x)


Konstrukcja ta pozwalała na iterowanie po kolejnych elementach zwracanych przez range. Istnieje możliwość tworzenia własnych mechanizmów tego typu. Przyjrzyjmy się poniższemu kodowi:


def elementy():
    yield 'element numer 1'
    yield 'element numer 2'
    yield 'element numer 3'
    yield 'element numer 4'

for e in elementy():
    print(e)


Powyżej widzimy bardzo prosty generator. Wynik jego działania przedstawia się następująco:


element numer 1


element numer 2


element numer 3


element numer 4


Moja funkcja "elementy" za pomocą słowa kluczowego "yield" podaje nam kolejne elementy.  Słowo kluczowe „yield” odpowiada za przerwanie wykonania funkcji, zapisanie jej aktualnego stanu i zwrócenie kolejnej wartości. Jak widzimy w powyższym przykładzie, stan wykonania funkcji elementy był zapamiętywany i dlatego iterując po wyniku tej funkcji dostajemy kolejne podawane przez „yield” elementy.


W podobny sposób możemy utworzyć własną adaptację range podającą nam wartości co 10:


def myrange(n):
    for x in range(n):
        yield x*10

for x in myrange(10):
    print(x)


Idąc tym tropem możemy budować bardziej złożone konstrukcje. Poniżej przykład generatora który podaje tyle potęg kolejnych liczb ile otrzyma przez argument:


def potegi2(n):
    for x in range(1, n + 1):
        yield pow(2, x)

for p in potegi2(5):
    print(p)


Wynik działania na konsoli:


2


4


8


16


32


Nie zawsze chcemy przetworzyć cały zbiór jaki generator może nam zwrócić. Weźmy pod uwagę funkcję generującą która będzie nam zwracała kolejne wartości bez końca. W poniższym przypadku będą to kolejne dziesięci:


def dziesieci():
    i=1
    while True:
       yield i*10
       i+=1


Mogę teraz pojedynczo pobierać kolejne wartości korzystając z poniższej konstrukcji:


dz=dziesieci()
print( dz.__next__() )
print( dz.__next__() )
print( dz.__next__() )


Na konsoli dostaję dane:


10


20


30


Ten sam skutek mogę osiągnąć taką konstrukcją:


dz=dziesieci()
print(next(dz))
print(next(dz))
print(next(dz))


Generator może oddawać wartości różnych typów i generować je na różne sposoby, ważne by oddawać kolejne wartości za pomocą yield:


def poryRoku():
    pory = ['styczeń', 'luty', 'marzec', 'kwiecień', 'maj', 'czerwiec', 'lipiec', 'sierpień', 'wrzesień', 'październik',
            'listopad', 'grudzień']
    for e in pory:
        yield e


for p in poryRoku():
    print(p)


Powyzej generator zwracający nam nazwy kolejnych miesięcy. Kolejny przykład to generator kolejnych liczb parzystych:


def parzyste(n):
    for x in range(n + 1):
        yield 2 * x

for p in parzyste(10):
    print(p)


Generator kolejnych liter alfabetu:


def literki():
    for x in range(97,123):
        yield(chr(x))

for l in literki():
    print(l)


Teraz nieco bardziej praktyczny przykład. Generator który czyta plik csv, rozbija każdą z linii csv na listę wg podanego przez argument rozdzielacza. Na potrzeby tego przykładu stworzyłem plik o nazwie "plik.csv" i umieściłem w nim następujące dane:


1;Artur
2;Krzysztof
3;Zenon
4;Marcin
5;Andrzej


Dodajemy generator i wywołujemy go:


def rozbijacz_csv(np,r):
    plik=open(np,encoding='utf-8')
    while True:
        linia=plik.readline()
        if not linia:
            break
        yield linia.strip().split(r)

rc=rozbijacz_csv('plik.csv',';')
print(next(rc))
print(next(rc))


Wynik na konsoli:


['1', 'Artur']


['2', 'Krzysztof']


Zwróć uwagę że nie użyłem konstrukcji typu "for linia in plik.readlines(): " tylko wczytuję kolejne linie jedna po drugiej. Robię tak dlatego, że "readlines()" wczytuje od razu całą zawartość pliku, co mogłoby skończyć się przepełnieniem pamięci w przypadku bardzo dużego pliku.

Przyjdź do nas na szkolenie z języka Python! Mamy szereg szkoleń w ofercie, od podstawowych po aplikacje webowe z użyciem Django, analizę danych, tesowanie, machine learning i wiele innych.
Sprawdź dostępne szkolenia Python
Zapisz się do newslettera aby otrzymywać najnowsze świeżynki pojawiające się na blogu!