Iteraatorid ja Generaatorid

Iteratsioon on samade või sarnaste sündmuste kordumine. Klassikalise iteratsiooni näidiseks on tsüklid, näiteks:

for _ in range(10):
    # Prints message 'How boring would life be without cycles?' 10 times to the console
    print("How boring would life be without cycles?")

Ilma tsüklita peaksime kirjutama sama print() meetodi manuaalselt 10 korda, mis on juba üsna vastik tegevus, rääkimata olukordadest kus korduste arvuks oleks näiteks 100, 1000 vms.

for-each tsükkel

Iteratsiooni on võimalik teostada mõnede kindlate objektide sisu järgi. Sel juhul on korduste arvuks objekti ühikute summa. Sõnade puhul on ühikuteks tähed, järjendi puhul elemendid. Sellist iteratsiooni nimetatakse for-each tsükliks:

String’ide puhul saame itereerida tähtede järgi:
hello = "Hello..."

for char in hello:
    print(char, end=" ")

# H e l l o . . .
Järjendite ja ennikute puhul nende elementide järgi:
some_list = ["We", "are", "called"]

for el in some_list:
    print(el, end=" ")

# We are called

some_tup = ("iterative", "objects")

for el in some_tup:
    print(el, end=" ")

# iterative objects
Sõnastike puhul saame itereerida üle võtmepaaride:
some_dict = {"iter": 1, "this": 2}
for key in some_dict:
    print(f"key: {key}")
Samuti on võimalik itereerida üle faili ridade:
with open("text.txt") as file:
    for line in file:
        print(line):

Python’is on palju sisseehitatud funktsioone, mis võtavad argumendiks iteratiivset objekti: näiteks list(), "".join() jm.

>>> ",".join(["a", "b", "c"])
'a,b,c'
>>> ",".join({"x": 1, "y": 2})
'y,x'
>>> list("python")
['p', 'y', 't', 'h', 'o', 'n']
>>> list({"x": 1, "y": 2})
['y', 'x']

Generaatorid

Generaatorid on funktsioon, mis tagastab igal väljakutsumisel ühe jada liikme. Erinevalt tavalistest funktsioonidest, mis tagastavad väärtusi võtmesõnaga return, tagastavad generaatorid järjest jada liikmeid võtmesõnaga yield.

Kui generaatoril pole enam elemente, mida tagastada siis tagastab ta StopIteration erindi.

Generaatori järgmist elementi saab küsida funktsiooniga next(generator)

See generaator tagastab järjest täisarvude ruute.
# Generator function
def square_range(n):
    """Generate first n square numbers"""
    i = 0
    while i < n:
        yield i ** 2
        i += 1

for i in square_range(5):
    print(i, end=" ")

# output: 0 1 4 9 16

generator = square_range(3)
print(next(generator))  # 0
print(next(generator))  # 1
print(next(generator))  # 4
print(next(generator))  # StopIteration

Generaatori eeliseks listi ees on see, et iga järgnev element arvutatakse välja alles siis, kui seda on vaja. Seetõttu vajab generaator vähem arvuti mälu kui suur list. Toodud näites vajab generaator alla 0.000 MiB ruumi, kuid list vajab 0.137 MiB ruumi.

(venv) kristjan@kristjan-ThinkPad-T460s:~/PycharmProjects/pydoc_root$ python3 -m memory_profiler test_memory.py
Filename: test_memory.py

Line #    Mem usage    Increment   Line Contents
================================================
     1   14.605 MiB   14.605 MiB   @profile
     2                             def f():
     3   14.605 MiB    0.000 MiB       num = 10_000_000
     4   14.605 MiB    0.000 MiB       range(num)  # range is a generator
     5   14.742 MiB    0.137 MiB       list(range(num))  # save this range of numbers in a list

Samuti võib generaator olla lõpmatu. Näiteks:

def infinite_stream_of_true_and_false():
    while True:
        yield True
        yield False


stream = infinite_stream_of_true_and_false()

print(next(stream)) # True
print(next(stream)) # False
print(next(stream)) # True
print(next(stream)) # False
print(next(stream)) # True
print(next(stream)) # False

Generaatori väljendid

Generaatori väljend näeb välja nagu list comprehension, lihtsalt ümarsulgudes.

# List comprehension
squared = [i*i for i in range(10)]
# We can read the stored data as many times as we like
print(squared)  # --> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(squared)  # --> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# Generator expression
squared = (i*i for i in range(10))
print(squared)  # --> <generator object <genexpr> at 0x01C13480>
print(list(squared))  # --> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(list(squared))  # --> []

Iteraator klassina

Keerukamat iteraatorit võib kirjelda ka klassina. Selleks tuleb defineerida __next__(self) funktsioon.

class NewRange:
    def __init__(self, start, end, step):
        self.current = start
        self.end = end
        self.step = step

    def __iter__(self):
        return self

    def __next__(self):
        # If the limit is not yet reached
        if self.current < self.end:
            old_current = self.current
            self.current += self.step
            return old_current
        else:
            raise StopIteration


iterator = NewRange(1, 2, 0.25)  # --> Creating an iterator object

print(next(iterator))  # --> 1
print(next(iterator))  # --> 1.25
print(next(iterator))  # --> 1.5
print(next(iterator))  # --> 1.75
# print(next(iterator))  # --> StopIteration