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.
Komentarze (0)
Brak komentarzy...