23. Python – Generatory


Generator to specjalna funkcja która zwraca iterator. Zamiast zwracać wszystkie wartości na raz, generuje je po kolei, „leniwie” (lazy evaluation).

Dlaczego generatory?

  • Oszczędność pamięci (nie trzymają wszystkich wartości)
  • Efektywność dla dużych zbiorów danych
  • Nieskończone sekwencje
  • Pipeline’y przetwarzania danych

Kluczowe słowo: yield (zamiast return)

Zwykła funkcja vs Generator

Zwykła funkcja:

def liczby_zwykla():
    """Zwraca listę - wszystko naraz w pamięci"""
    wynik = []
    for i in range(5):
        wynik.append(i)
    return wynik

print(liczby_zwykla())  # [0, 1, 2, 3, 4]

Generator:

def liczby_generator():
    """Generator - zwraca po kolei"""
    for i in range(5):
        yield i  # yield zamiast append/return

# Generator nie wykonuje się od razu
gen = liczby_generator()
print(gen)  # <generator object liczby_generator at 0x...>

# Pobierz wartości po kolei
print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 2

# Lub w pętli
for liczba in liczby_generator():
    print(liczba)
# 0, 1, 2, 3, 4

Jak działa yield

def prosty_generator():
    print("Start")
    yield 1
    print("Między 1 a 2")
    yield 2
    print("Między 2 a 3")
    yield 3
    print("Koniec")

gen = prosty_generator()

print("Pierwsza wartość:")
print(next(gen))
# Start
# 1

print("Druga wartość:")
print(next(gen))
# Między 1 a 2
# 2

print("Trzecia wartość:")
print(next(gen))
# Między 2 a 3
# 3

print("Próba czwartej:")
# next(gen)  # StopIteration - generator się wyczerpał

Kluczowe:

  • Generator „zatrzymuje się” po każdym yield
  • Przy następnym next() kontynuuje od miejsca zatrzymania
  • Zapamiętuje stan (zmienne lokalne)

Pierwszy praktyczny generator

def liczby_parzyste(n):
    """Generuje liczby parzyste od 0 do n"""
    for i in range(n + 1):
        if i % 2 == 0:
            yield i

# Użycie
for liczba in liczby_parzyste(10):
    print(liczba)
# 0, 2, 4, 6, 8, 10

Generator vs Lista – porównanie pamięci

import sys

# Lista - wszystko w pamięci
lista = [x for x in range(1000000)]
print(f"Lista zajmuje: {sys.getsizeof(lista)} bajtów")
# Lista zajmuje: 8000056 bajtów

# Generator - tylko instrukcje
generator = (x for x in range(1000000))
print(f"Generator zajmuje: {sys.getsizeof(generator)} bajtów")
# Generator zajmuje: 104 bajty

# Generator jest ~77000 razy mniejszy!

Generator expressions (wyrażenia generatorowe)

Podobne do list comprehensions, ale z () zamiast []:

# List comprehension - lista w pamięci
lista = [x**2 for x in range(10)]
print(lista)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# Generator expression - generator
generator = (x**2 for x in range(10))
print(generator)  # <generator object ...>

# Użycie
for wartosc in generator:
    print(wartosc)

Nieskończone generatory

def liczby_naturalne():
    """Nieskończony generator liczb naturalnych"""
    n = 0
    while True:  # Nieskończona pętla!
        yield n
        n += 1

# Użycie - pobierz pierwsze 10
gen = liczby_naturalne()
for i in range(10):
    print(next(gen))
# 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

Generator Fibonacciego

def fibonacci():
    """Nieskończony generator liczb Fibonacciego"""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Pierwsze 10 liczb
gen = fibonacci()
for i in range(10):
    print(next(gen))
# 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

Generator z limitem

def fibonacci_limit(limit):
    """Generator Fibonacciego do limitu"""
    a, b = 0, 1
    while a <= limit:
        yield a
        a, b = b, a + b

# Wszystkie liczby Fibonacciego <= 100
for liczba in fibonacci_limit(100):
    print(liczba)
# 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89

Generator odczytujący plik

def czytaj_linie(nazwa_pliku):
    """Generator czytający plik linia po linii"""
    with open(nazwa_pliku, 'r', encoding='utf-8') as plik:
        for linia in plik:
            yield linia.strip()

# Użycie - oszczędność pamięci dla dużych plików
for linia in czytaj_linie('duzy_plik.txt'):
    print(linia)
    # Przetwarza jedną linię na raz, nie cały plik!

Generator z filtrowaniem

def wiersze_z_slowem(nazwa_pliku, slowo):
    """Generator zwracający wiersze zawierające słowo"""
    with open(nazwa_pliku, 'r', encoding='utf-8') as plik:
        for linia in plik:
            if slowo in linia:
                yield linia.strip()

# Znajdź wszystkie linie z "Python"
for linia in wiersze_z_slowem('log.txt', 'Python'):
    print(linia)

Generator liczb pierwszych

def liczby_pierwsze(limit):
    """Generator liczb pierwszych do limitu"""
    def czy_pierwsza(n):
        if n < 2:
            return False
        for i in range(2, int(n**0.5) + 1):
            if n % i == 0:
                return False
        return True
    
    for n in range(2, limit + 1):
        if czy_pierwsza(n):
            yield n

# Wszystkie liczby pierwsze <= 50
for liczba in liczby_pierwsze(50):
    print(liczba)
# 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47

Generator z yield from

yield from deleguje do innego generatora:

def generator1():
    yield 1
    yield 2
    yield 3

def generator2():
    yield 4
    yield 5
    yield 6

def polaczony():
    yield from generator1()
    yield from generator2()

for wartosc in polaczony():
    print(wartosc)
# 1, 2, 3, 4, 5, 6

Generator zagnieżdżonych list

def splaszcz(lista):
    """Spłaszcza zagnieżdżoną listę"""
    for element in lista:
        if isinstance(element, list):
            yield from splaszcz(element)  # Rekurencja!
        else:
            yield element

zagniezdzona = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]
for element in splaszcz(zagniezdzona):
    print(element)
# 1, 2, 3, 4, 5, 6, 7, 8, 9

Praktyczne przykłady

Przykład 1: Przetwarzanie CSV

def czytaj_csv(nazwa_pliku):
    """Generator wierszy CSV"""
    with open(nazwa_pliku, 'r', encoding='utf-8') as plik:
        naglowki = next(plik).strip().split(',')
        
        for linia in plik:
            wartosci = linia.strip().split(',')
            yield dict(zip(naglowki, wartosci))

# Użycie
for rekord in czytaj_csv('dane.csv'):
    print(rekord)
    # {'Imie': 'Jan', 'Wiek': '25', 'Miasto': 'Warszawa'}

Przykład 2: Batching (grupowanie)

def batches(iterable, rozmiar_batcha):
    """Dzieli iterable na batche o określonym rozmiarze"""
    batch = []
    for element in iterable:
        batch.append(element)
        if len(batch) == rozmiar_batcha:
            yield batch
            batch = []
    
    # Ostatni batch (może być mniejszy)
    if batch:
        yield batch

# Użycie
dane = range(1, 11)  # 1 do 10
for batch in batches(dane, 3):
    print(batch)
# [1, 2, 3]
# [4, 5, 6]
# [7, 8, 9]
# [10]

Przykład 3: Sliding window

def okno_przesuwne(iterable, rozmiar):
    """Generator okna przesuwnego"""
    from collections import deque
    okno = deque(maxlen=rozmiar)
    
    for element in iterable:
        okno.append(element)
        if len(okno) == rozmiar:
            yield list(okno)

# Użycie
dane = [1, 2, 3, 4, 5, 6, 7]
for okno in okno_przesuwne(dane, 3):
    print(okno)
# [1, 2, 3]
# [2, 3, 4]
# [3, 4, 5]
# [4, 5, 6]
# [5, 6, 7]

Przykład 4: Pipeline przetwarzania

def czytaj_liczby(nazwa_pliku):
    """Krok 1: Czytaj liczby z pliku"""
    with open(nazwa_pliku, 'r') as plik:
        for linia in plik:
            yield int(linia.strip())

def filtruj_parzyste(liczby):
    """Krok 2: Filtruj parzyste"""
    for liczba in liczby:
        if liczba % 2 == 0:
            yield liczba

def pomnoz_przez_2(liczby):
    """Krok 3: Pomnóż przez 2"""
    for liczba in liczby:
        yield liczba * 2

# Pipeline
liczby = czytaj_liczby('liczby.txt')
parzyste = filtruj_parzyste(liczby)
podwojone = pomnoz_przez_2(parzyste)

# Wszystko działa leniwie - przetwarza po jednej liczbie!
for wynik in podwojone:
    print(wynik)

Przykład 5: Generator losowych danych

import random

def losowe_liczby(ile, min_val, max_val):
    """Generator losowych liczb"""
    for _ in range(ile):
        yield random.randint(min_val, max_val)

def losowe_imiona(ile):
    """Generator losowych imion"""
    imiona = ['Jan', 'Anna', 'Piotr', 'Ewa', 'Tomasz', 'Kasia']
    for _ in range(ile):
        yield random.choice(imiona)

# Użycie
for liczba in losowe_liczby(5, 1, 100):
    print(liczba)
# 42, 17, 89, 5, 63

for imie in losowe_imiona(3):
    print(imie)
# Anna, Piotr, Jan

Send i Close

Generatory mogą również przyjmować wartości!

send()

def kalkulator():
    """Generator jako kalkulator"""
    wynik = 0
    while True:
        operacja = yield wynik
        if operacja is None:
            break
        
        akcja, liczba = operacja
        if akcja == 'dodaj':
            wynik += liczba
        elif akcja == 'odejmij':
            wynik -= liczba

# Użycie
kalk = kalkulator()
next(kalk)  # Uruchom generator

print(kalk.send(('dodaj', 10)))     # 10
print(kalk.send(('dodaj', 5)))      # 15
print(kalk.send(('odejmij', 3)))    # 12

close()

def nieskonczone_liczby():
    n = 0
    try:
        while True:
            yield n
            n += 1
    finally:
        print("Generator zamknięty - sprzątam")

gen = nieskonczone_liczby()
print(next(gen))  # 0
print(next(gen))  # 1
gen.close()       # Generator zamknięty - sprzątam
# print(next(gen))  # StopIteration

Itertools – moduł z generatorami

Python ma wbudowany moduł itertools z potężnymi generatorami:

import itertools

# count - nieskończone liczenie
for i in itertools.count(10, 2):
    if i > 20:
        break
    print(i)
# 10, 12, 14, 16, 18, 20

# cycle - cykliczne powtarzanie
kolory = itertools.cycle(['czerwony', 'zielony', 'niebieski'])
for i, kolor in enumerate(kolory):
    if i >= 6:
        break
    print(kolor)
# czerwony, zielony, niebieski, czerwony, zielony, niebieski

# repeat - powtarzanie n razy
for x in itertools.repeat('Python', 3):
    print(x)
# Python, Python, Python

# chain - łączenie iteratorów
for x in itertools.chain([1, 2, 3], ['a', 'b', 'c']):
    print(x)
# 1, 2, 3, a, b, c

# islice - wycinki
for x in itertools.islice(range(100), 5, 15, 2):
    print(x)
# 5, 7, 9, 11, 13

# combinations - kombinacje
for combo in itertools.combinations([1, 2, 3, 4], 2):
    print(combo)
# (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)

# permutations - permutacje
for perm in itertools.permutations([1, 2, 3], 2):
    print(perm)
# (1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)

Generator vs Iterator

Iterator to obiekt implementujący metody __iter__() i __next__():

class LicznikIterator:
    def __init__(self, limit):
        self.limit = limit
        self.current = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current >= self.limit:
            raise StopIteration
        self.current += 1
        return self.current

# Użycie
for i in LicznikIterator(5):
    print(i)
# 1, 2, 3, 4, 5

Generator to prostsza składnia dla iteratora:

def licznik_generator(limit):
    for i in range(1, limit + 1):
        yield i

# To samo, krócej!
for i in licznik_generator(5):
    print(i)
# 1, 2, 3, 4, 5

Podsumowanie Generatorów

Definicja:

  • Funkcja z yield zamiast return
  • Zwraca iterator
  • Leniwa ewaluacja

Kluczowe słowa:

  • yield – zwróć wartość i zatrzymaj
  • yield from – deleguj do innego generatora
  • next() – pobierz następną wartość
  • send() – wyślij wartość do generatora
  • close() – zamknij generator

Zalety:

  • Oszczędność pamięci
  • Efektywność dla dużych danych
  • Nieskończone sekwencje
  • Pipeline’y przetwarzania

Zastosowania:

  • Przetwarzanie dużych plików
  • Strumienie danych
  • Pipeline’y transformacji
  • Nieskończone sekwencje

Generator expressions:

  • (x for x in range(10)) – jak list comprehension ale z ()

Ćwiczenia

Generatory

  1. Napisz generator liczb od n do m z krokiem k.
  2. Napisz generator liczb parzystych.
  3. Napisz generator liczb nieparzystych.
  4. Napisz generator Fibonacciego do limitu.
  5. Napisz generator liczb pierwszych.
  6. Napisz generator kwadratów liczb.
  7. Napisz generator odwracający string znak po znaku.
  8. Napisz generator czytający plik linia po linii.
  9. Napisz generator filtrujący puste linie z pliku.
  10. Napisz generator countdown (odliczanie).

Generatory – zaawansowane

  1. Napisz generator spłaszczający zagnieżdżoną listę.
  2. Napisz generator grupujący elementy w batche.
  3. Napisz generator okna przesuwnego (sliding window).
  4. Napisz generator permutacji listy.
  5. Napisz generator kombinacji elementów.
  6. Napisz generator kartezjańskiego iloczynu.
  7. Napisz generator nieskończonych liczb Fibonacciego.
  8. Napisz generator losowych liczb w zakresie.
  9. Napisz generator dat między dwoma datami.
  10. Napisz generator tokenów z tekstu.

Generator expressions

  1. Stwórz generator expression kwadratów liczb 1-10.
  2. Stwórz generator expression parzystych z listy.
  3. Filtruj listę stringów (długość > 5) przez generator.
  4. Stwórz generator sumujący elementy dwóch list.
  5. Generator expression dla wielkich liter z tekstu.

Pipeline’y

  1. Stwórz pipeline: czytaj liczby → filtruj parzyste → pomnóż × 2.
  2. Pipeline: czytaj plik → filtruj linie ze słowem → zlicz.
  3. Pipeline: generator liczb → filtruj pierwsze → sumuj.
  4. Pipeline: czytaj CSV → przekształć dane → filtruj.
  5. Pipeline: generuj losowe → filtruj zakres → agreguj.

Praktyczne zastosowania

  1. Generator odczytujący duży plik CSV.
  2. Generator przetwarzający logi (filtruj ERROR).
  3. Generator paginacji (strony wyników).
  4. Generator krokowy (skip n elementów).
  5. Generator unikalnych elementów.
  6. Generator zip dla wielu iteratorów.
  7. Generator grupujący po kluczu.
  8. Generator akumulujący (cumsum).
  9. Generator take/drop (weź n, pomiń n).
  10. Generator iterujący po drzewie katalogów.