Python - Generatori


Generatori u Pythonu

Puno je posla na izgradnji iteratora u Pythonu. Moramo implementovati klasu s __iter__() i __next__() metodom, pratiti untrašnja stanja i podići StopIteration kada nema vrijednosti koje treba vratiti. Ovo je i dugotrajno i kontraintuitivno. Generator dolazi u pomoć u takvim situacijama. Python generatori su jednostavan način stvaranja iteratora. Sav posao koji smo gore spomenuli automatski obrađuju generatori u Pythonu. Jednostavno rečeno, generator je funkcija koja vraća objekt (iterator) koji možemo iterirati (jednu po jednu vrijednost).



Kreirajte generatore u Pythonu

Stvoriti generator u Pythonu prilično je jednostavno. Jednostavno je kao definisati normalnu funkciju, ali s naredbom yield umjesto izjave return. Ako funkcija sadrži najmanje jedan izraz yield (može sadržavati i druge statement ili return), ona postaje funkcija generatora. I yield i return vratit će neku vrijednost iz funkcije. Razlika je u tome što dok naredba return u potpunosti završava funkciju, naredba yield zaustavlja funkciju spremajući sva njena stanja i kasnije nastavlja odatle na uzastopne pozive.



Razlike između funkcije generatora i normalne funkcije

Evo kako se funkcija generatora razlikuje od normalne funkcije.

  • Funkcija generatora sadrži jedan ili više izraza prenosa.
  • Kada se pozove, vraća objekt (iterator), ali ne započinje izvršenje odmah.
  • Metode poput __iter__() i __next__() implementuje se automatski. Tako možemo pregledavati stavke koristeći next().
  • Jednom kada funkcija popusti, funkcija se pauzira i kontrola se prenosi na pozivatelja.
  • Lokalne varijable i njihova stanja pamte se između uzastopnih poziva.
  • Konačno, kada se funkcija završi, StopIteration se automatski podiže na dalje pozive.

Evo primjera koji ilustrira sve gore navedene tačke. Imamo funkciju generatora nazvanu my_gen() s nekoliko izraza prenosa.

# Jednostavna funkcija generatora
def my_gen():
    n = 1
    print('Ovo se ispisuje prvo')
    # Funkcija generatora sadrži izjave o prenosu
    yield n

    n += 1
    print('Ovo će se ispisati drugo')
    yield n

    n += 1
    print('Ovo će se ispisati posljednje')
    yield n

Interaktivni rad u interpretatoru dat je u nastavku. Pokrenite ih u Python shell da biste vidjeli izlaz.

>>> # Vraća objekt, ali ne započinje izvršenje odmah.
>>> a = my_gen()

>>> # Možemo se kretati kroz stavke pomoću next().
>>> next(a)
Ovo se ispisuje prvo
1
>>> # Jednom kada funkcija popusti, funkcija se pauzira i kontrola se prenosi na pozivatelja.

>>> # Lokalne varijable i njihova stanja pamte se između uzastopnih poziva.
>>> next(a)
Ovo će se ispisati drugo
2

>>> next(a)
Ovo će se ispisati posljednje
3

>>> # Konačno, kada se funkcija završi, StopIteration se automatski podiže na dalje pozive.
>>> next(a)
Traceback (most recent call last):
...
StopIteration
>>> next(a)
Traceback (most recent call last):
...
StopIteration

Jedna zanimljiva stvar koju treba primijetiti u gornjem primjeru je da se vrijednost varijable n pamti između svakog poziva. Za razliku od normalnih funkcija, lokalne varijable se ne uništavaju kada funkcija popusti. Nadalje, objekt generatora može se ponoviti samo jednom. Da bismo ponovno pokrenuli postupak, moramo stvoriti još jedan generator objekta koristeći nešto poput a = my_gen(). Zadnja stvar koju treba primetiti je da generatore for petlje možemo koristiti direktno.

To je zato što for petlja uzima iterator i prevlači se preko njega pomoću funkcije next(). Automatski se završava kad se podigne StopIteration. Pogledajte lekciju kako se petlja for zapravo implementuje u Python, da biste saznali više.

# Jednostavna generator funkcija
def my_gen():
    n = 1
    print('Ovo se ispisuje prvo')
    # Funkcija generatora koja koristi yield izjavu
    yield n

    n += 1
    print('Ovo se ispisuje drugo')
    yield n

    n += 1
    print('Ovo se ispisuje zadnje')
    yield n


# Korištenje for petlje
for item in my_gen():
    print(item)


Python generatori s petljom

Gornji primjer je od manje koristi i mi smo ga proučavali samo da bismo imali ideju o tome što se događalo u pozadini. Obično se funkcije generatora implementuje s petljom koja ima odgovarajuće završno stanje. Uzmimo primjer generatora koji obrće string.

def rev_str(my_str):
    length = len(my_str)
    for i in range(length - 1, -1, -1):
        yield my_str[i]


# For petlja koja obrće string
for char in rev_str("hello"):
    print(char)

U ovom smo primjeru koristili funkciju range() da bismo indeks dobili obrnutim redoslijedom pomoću for petlje.



Izraz Python generatora

Jednostavni generatori mogu se lako stvoriti u hodu pomoću izraza generator. To olakšava izgradnju generatora. Slično lambda funkcijama koje stvaraju anonimne funkcije, izrazi generatora stvaraju anonimne funkcije generatora. Sintaksa izraza generatora slična je sintaksi za razumijevanje liste u Pythonu. Ali uglate zagrade, zamijenjene su okruglim zagradama.

Glavna razlika između razumijevanja popisa i izraza generatora je u tome što razumijevanje popisa stvara cijelu listu, dok izraz daje jednu po jednu stavku. Imaju lijeno izvršavanje (proizvode predmete samo kada se to traži od njih). Iz tog razloga, izraz generatora mnogo je učinkovitiji od memorije od ekvivalentnog razumijevanja liste.

# Inicijalizacija liste
my_list = [1, 3, 6, 10]

# kvadrat svaki pojam koristeći razumijevanje liste
list_ = [x**2 for x in my_list]

# ista stvar se može učiniti pomoću izraza generatora
# izrazi generatora okruženi su zagradama ()
generator = (x**2 for x in my_list)

print(list_)
print(generator)

Možemo vidjeti da izraz generatora nije odmah dao traženi rezultat. Umjesto toga, vratio je generator objekt koji proizvodi predmete samo na zahtjev. Evo kako možemo početi dobijati stavke iz generatora:

# Inicijalizacija liste
my_list = [1, 3, 6, 10]

a = (x**2 for x in my_list)
print(next(a))

print(next(a))

print(next(a))

print(next(a))

next(a)

Izrazi generatora mogu se koristiti kao argumenti funkcije. Kada se koriste na takav način, okrugle zagrade mogu se ispustiti.

>>> sum(x**2 for x in my_list)
146

>>> max(x**2 for x in my_list)
100


Upotreba Python generatora

Postoji nekoliko razloga koji generatore čine moćnom implementacijom.


1. Jednostavni su za implementaciju

Generatori se mogu implementovati na jasan i sažet način u odnosu na iteratore. Slijedi primjer za implementaciju sekvence snage 2 pomoću klase iteratora.

class PowTwo:
    def __init__(self, max=0):
        self.n = 0
        self.max = max

    def __iter__(self):
        return self

    def __next__(self):
        if self.n > self.max:
            raise StopIteration

        result = 2 ** self.n
        self.n += 1
        return result

Gornji program bio je dugotrajan i zbunjujući. Sada, učinimo isto koristeći funkciju generator.

def PowTwoGen(max=0):
    n = 0
    while n < max:
        yield 2 ** n
        n += 1

Budući da generatori automatski prate detalje, implementacija je bila sažeta i mnogo čišća.


2. Učinkovito pamćenje

Normalna funkcija za vraćanje sekvence stvoriće čitavu sekvencu u memoriji prije vraćanja rezultata. Ovo je pretjerano ako je broj stavki u sekvenci velik. Implementacija generatora takvih sekvenci prilagođena je memoriji i poželjna je, jer istovremeno proizvodi samo jednu stavku.


3. Predstavljanje beskonačnog toka

Generatori su izvrsni mediji koji predstavljaju beskonačan tok podataka. Beskonačni tokovi se ne mogu pohraniti u memoriju, a budući da generatori proizvode samo po jednu stavku odjednom, mogu predstavljati beskonačan tok podataka. Sljedeća funkcija generatora može generisati sve parne brojeve (barem u teoriji).

def all_even():
    n = 0
    while True:
        yield n
        n += 2

4. Pipelining generator

Za generisanje niza operacija može se koristiti više generatora. To ćemo najbolje ilustrovati na primjeru. Pretpostavimo da imamo generator koji proizvodi brojeve u Fibonaccijevoj seriji. I imamo još jedan generator za kvadriranje brojeva.

Ako želimo saznati zbir kvadrata brojeva u Fibonaccijevoj seriji, to možemo učiniti na sljedeći način cjevovođenjem izlaza funkcija generatora zajedno.

def fibonacci_numbers(nums):
    x, y = 0, 1
    for _ in range(nums):
        x, y = y, x+y
        yield x

def square(nums):
    for num in nums:
        yield num**2

print(sum(square(fibonacci_numbers(10))))

Ovaj pipelining efekat je učinkovit i lak za čitanje.