En cronometrar tancament_de_caixa() a la lliçó anterior va quedar una pregunta a l'aire: per sumar les vendes d'avui, la funció llegeix vendes.csv sencer i el converteix en una llista. Amb les quatre vendes d'aquesta setmana tant és; amb cinc anys d'història de Papyrus, aquesta llista ocupa memòria per calcular... un sol número. Els generadors són la resposta de Python a aquest malbaratament: produeixen els valors d'un en un, sota demanda, sense materialitzar mai la col·lecció completa. Aquesta lliçó desmunta el yield, el protocol d'iteració que hi ha sota cada for que has escrit des del mòdul 2, i hi munta un pipeline de processament de vendes.

Contingut

  1. El problema: carregar-ho tot vs processar sota demanda
  2. yield: funcions que es pausen
  3. El protocol d'iteració: next() i StopIteration
  4. Generadors vs llistes: la taula de la veritat
  5. Expressions generadores
  6. Fitxers grans línia a línia: el generador natural
  7. Encadenar generadors: el pipeline de vendes
  8. yield from, breument
  9. itertools: islice i chain
  10. Quan llista i quan generador

El problema: carregar-ho tot vs sota demanda

Així podria sumar el dia una versió ingènua del tancament (amb el csv i el try mínim de M6/M7):

import csv

def quanties_del_dia(ruta, data):
    files = list(csv.DictReader(ruta.open(encoding="utf-8")))  # TOT en memoria!
    quanties = []
    for fila in files:
        if fila["data"] == data:
            quanties.append(float(fila["quantia"]))
    return quanties

Dues llistes completes (files i quanties) per acabar fent sum(). Si vendes.csv té un milió de files, hi ha un milió de diccionaris vius alhora. L'alternativa: produir cada quantia just quan es necessita i oblidar-la just després. Memòria constant, tant se val la mida del fitxer.

yield: funcions que es pausen

Un generador s'escriu com una funció normal, però amb yield on una funció normal posaria return... amb una diferència radical de comportament:

def primeres_vendes():
    print("preparant la primera")
    yield ("2026-07-13", "Hamlet", 20.70)
    print("preparant la segona")
    yield ("2026-07-13", "L'Odissea", 13.00)
    print("no n'hi ha mes")

g = primeres_vendes()
print(type(g))        # <class 'generator'> — encara no s'ha imprès RES!

Cridar primeres_vendes() no n'executa el cos: retorna un objecte generador, pausat a la primera línia. El cos avança només quan algú demana un valor:

print(next(g))
# preparant la primera
# ('2026-07-13', 'Hamlet', 20.7)

print(next(g))
# preparant la segona
# ('2026-07-13', "L'Odissea", 13.0)

next(g)
# no n'hi ha mes
# Traceback ... StopIteration

Cada next() reprèn el cos exactament on s'havia quedat — amb les seves variables locals intactes, com un punt de llibre en un llibre de la botiga — executa fins al següent yield, lliura el valor i torna a pausar-se. Quan el cos acaba, es llança StopIteration.

sequenceDiagram
    participant C as Consumidor (for / next)
    participant G as Generador
    C->>G: next(g)
    activate G
    Note over G: executa fins al 1r yield
    G-->>C: lliura el valor i ES PAUSA
    deactivate G
    C->>G: next(g)
    activate G
    Note over G: reprèn després del yield,<br/>locals intactes
    G-->>C: valor següent, pausa
    deactivate G
    C->>G: next(g)
    activate G
    Note over G: el cos acaba
    G-->>C: StopIteration
    deactivate G

El protocol d'iteració

Això no és un mecanisme a part: és el que el for fa per dins des del mòdul 2. Un bucle for x in cosa: crida iter(cosa), després next() repetidament, i captura StopIteration per acabar netament. Per això un generador es recorre amb for sense veure mai l'excepció:

for venda in primeres_vendes():
    print(venda)      # el for gestiona next() i StopIteration per tu

Llistes, diccionaris, fitxers, range: tots parlen aquest mateix protocol. Els generadors són simplement la manera més còmoda de crear objectes que el parlin.

Generadors vs llistes

def quadrats_llista(n):
    return [i * i for i in range(n)]     # materialitza n valors

def quadrats_gen(n):
    for i in range(n):
        yield i * i                      # produeix d'un en un
Aspecte Llista Generador
Memòria Tots els elements alhora Un element cada vegada (constant)
Quan es calcula? En crear-la (ansiós) En demanar cada valor (mandrós)
Recorreguts Tants com vulguis Un de sol — després queda esgotat
len(), indexar [i], in repetit No
Infinits possibles No (memòria finita) Sí (produeix mentre es demani)
Depurar amb print() Fàcil: es veu el contingut Només es veu <generator object ...>

La fila més traïdora és la del recorregut únic:

g = quadrats_gen(4)
print(sum(g))    # 14
print(sum(g))    # 0  ← esgotat! No llanca cap error: simplement ja no produeix res

Un generador és com el rotlle de paper del tiquet: es consumeix en fer-lo servir. Si necessites dues passades, crea el generador dues vegades o materialitza una llista.

Expressions generadores

Les comprensions de llistes de M2 tenen una germana mandrosa: la mateixa sintaxi amb parèntesis en comptes de claudàtors:

quanties = [float(f["quantia"]) for f in files]    # llista: materialitza
quanties = (float(f["quantia"]) for f in files)    # generador: sota demanda

I quan l'expressió és l'únic argument d'una funció, els parèntesis de la funció ja basten. Així queda el cor d'un tancament de caixa eficient:

total = sum(float(f["quantia"]) for f in files if f["data"] == avui)

Cap llista intermèdia: sum() va demanant valors pel protocol d'iteració i el generador els produeix d'un en un. És la forma idiomàtica d'agregar (sum, max, min, any, all) sense cost de memòria.

Fitxers línia a línia: el generador natural

Aquí tot connecta amb M6: un fitxer obert ja és un generador de línies. Quan escrivies for linia in f: no carregaves el fitxer — demanaves línia a línia. Embolicar-lo en un generador propi permet afegir-hi lògica:

from pathlib import Path

BASE = Path(__file__).parent

def llegir_vendes(ruta: Path):
    """Produeix les files de vendes.csv d'una en una, com a dict."""
    import csv
    with ruta.open(encoding="utf-8", newline="") as f:
        for fila in csv.DictReader(f):
            yield fila                    # pausa aqui; el fitxer segueix obert

for fila in llegir_vendes(BASE / "dades" / "vendes.csv"):
    print(fila["titol"], fila["quantia"])

Detall important: el with de dins manté el fitxer obert mentre el generador sigui viu, i el tanca quan el cos acaba (o el generador es destrueix). El consumidor no gestiona el fitxer: només demana files.

Encadenar generadors: el pipeline de vendes

La veritable potència apareix en encadenar generadors: cada etapa consumeix de l'anterior i produeix per a la següent, com una cadena de muntatge on cada fila travessa tota la cadena abans que hi entri la següent:

def parsejar(files):
    """dict → (data, titol, quantia float). Salta files corruptes, com tancament_de_caixa."""
    import logging
    logger = logging.getLogger(__name__)
    for fila in files:
        try:
            yield fila["data"], fila["titol"], float(fila["quantia"])
        except (KeyError, ValueError):
            logger.warning("fila corrupta saltada: %r", fila)   # el WARNING de 07-05

def nomes_del_dia(vendes, data):
    for d, titol, quantia in vendes:
        if d == data:
            yield d, titol, quantia

# Muntatge del pipeline: llegir → parsejar → filtrar → sumar
files   = llegir_vendes(BASE / "dades" / "vendes.csv")
vendes  = parsejar(files)
del_dia = nomes_del_dia(vendes, "2026-07-13")
total   = sum(quantia for _, _, quantia in del_dia)
print(f"Caixa del dia: {total:.2f} €")

Fins a l'última línia no s'ha llegit ni una fila: muntar el pipeline surt gratis. És sum() qui estira la cadena: demana un valor a del_dia, que el demana a vendes, que el demana a files, que llegeix una línia del disc. Memòria: una fila. I cada etapa és petita, amb nom, testable per separat — cosa que apreciarem a M9.

yield from, breument

Quan un generador només vol delegar en un altre iterable, yield from estalvia el bucle:

def vendes_de_tots_dos_fitxers(ruta_actual, ruta_antiga):
    yield from llegir_vendes(ruta_antiga)   # produeix tot el del primer...
    yield from llegir_vendes(ruta_actual)   # ...i despres tot el del segon

yield from iterable equival a for x in iterable: yield x. Suficient per ara; té usos més profunds que no necessitem.

itertools: dues eines útils

La biblioteca estàndard (M3) porta itertools, una caixa de peces per a iteradors. Dues que encaixen avui:

from itertools import islice, chain

# islice: "llescar" un generador (no es pot fer g[:5])
primeres_cinc = islice(llegir_vendes(ruta), 5)     # les 5 primeres files, sense llegir res mes

# chain: encadenar iterables — el mateix que vendes_de_tots_dos_fitxers, en una linia
totes = chain(llegir_vendes(ruta_antiga), llegir_vendes(ruta_actual))

Totes dues retornen, és clar, iteradors mandrosos. Quan t'enxampis escrivint lògica d'iteració "de fontaneria", mira primer si itertools ja la porta.

Quan llista i quan generador

  • Generador quan: la dada es recorre un cop i s'agrega (sum, informes del tancament), l'origen és gran o de mida desconeguda (vendes.csv, papyrus.log), o construeixes un pipeline per etapes.
  • Llista quan: necessites len(), indexar, ordenar (sorted materialitza de totes maneres), recórrer diverses vegades, o la dada és petita i la vols veure en depurar. El catàleg de Papyrus (4 llibres) en dict[str, Llibre] està perfecte tal com està: la mandra no aporta res a 4 elements.

Errors Comuns i Consells

  • Reutilitzar un generador esgotat: la segona passada retorna "res" sense error. Si un sum() et dona un 0 inexplicable, comprova si has consumit el generador abans.
  • Esperar que el cos s'executi en cridar: llegir_vendes(ruta) encara no obre el fitxer; ni tan sols valida la ruta. El FileNotFoundError saltarà al primer next(), potser lluny de la crida — tingues-ho en compte en llegir traces (M7).
  • return amb valor dins d'un generador: no "retorna" al consumidor; acaba el generador (el valor viatja amagat dins de StopIteration). Per produir valors, sempre yield.
  • Materialitzar sense voler: list(generador), sorted(generador) o ", ".join(generador) ho carreguen tot en memòria. De vegades és el correcte — però que sigui una decisió, no un accident.
  • Consell: anota els generadors amb els hints de 08-01 — la forma senzilla és Iterator[FilaVenda] (de typing o collections.abc): documenta que es consumeix un cop.
  • Consell: si en depurar necessites veure el contingut, list(islice(g, 10)) t'ensenya una mostra sense empassar-te el fitxer sencer.

Exercicis

  1. Escriu el generador quanties_del_titol(ruta, titol) que produeixi (yield) les quanties (float) de totes les vendes d'un títol donat llegint vendes.csv fila a fila, i fes-lo servir amb sum() per saber quant ha facturat Hamlet en total. Reutilitza llegir_vendes.
  2. L'Anna vol les 3 primeres vendes que superin 15.00 € per a una promoció. Resol-ho amb una expressió generadora més islice, sense cap llista intermèdia.
  3. Afegeix al pipeline de la lliçó una etapa amb_iva_desglossat(vendes) que rebi tuples (data, titol, quantia) i produeixi (data, titol, quantia, iva) on iva = round(quantia - quantia / 1.04, 2) (la part d'IVA_LLIBRES continguda en la quantia). Munta-la entre parsejar i nomes_del_dia i explica per què l'ordre de les etapes no canvia la memòria consumida.

Solucions

  1. def quanties_del_titol(ruta, titol):
        for fila in llegir_vendes(ruta):
            if fila["titol"] == titol:
                yield float(fila["quantia"])
    
    total_hamlet = sum(quanties_del_titol(BASE / "dades" / "vendes.csv", "Hamlet"))
    

    Dos generadors encadenats (llegir_vendesquanties_del_titol) i una agregació: memòria constant. Si alguna fila té "gratis" com a quantia, el float() llançarà — pots protegir-ho amb el try mínim de 07-02, com fa parsejar.

  2. from itertools import islice
    
    grans = (float(f["quantia"]) for f in llegir_vendes(ruta) if float(f["quantia"]) > 15.00)
    tres_primeres = list(islice(grans, 3))
    

    La part elegant: tan bon punt islice lliura la tercera, deixa de demanar — la resta del fitxer ni es llegeix. El list() final materialitza només 3 elements, per poder mirar-los.

  3. def amb_iva_desglossat(vendes):
        for data, titol, quantia in vendes:
            iva = round(quantia - quantia / 1.04, 2)
            yield data, titol, quantia, iva
    
    files   = llegir_vendes(BASE / "dades" / "vendes.csv")
    vendes  = amb_iva_desglossat(parsejar(files))
    del_dia = nomes_del_dia_4(vendes, "2026-07-13")   # adaptada a tuples de 4
    

    La memòria no depèn del nombre d'etapes sinó de quantes files estan "en trànsit" alhora: exactament una, perquè cada next() del consumidor arrossega una única fila per tota la cadena. Deu etapes continuarien fent servir la memòria d'una fila.

Conclusió

Els generadors compleixen la segona promesa del tancament de M7: tancament_de_caixa ja pot processar les vendes sense carregar-les senceres en memòria, amb yield pausant i reprenent la funció, StopIteration tancant el cicle que el for gestiona des de sempre, i un pipeline llegir → parsejar → filtrar → sumar on cada etapa fa una sola cosa. I a llegir_vendes ha quedat a la vista un vell conegut treballant en silenci: el with, que manté el fitxer obert just mentre cal i el tanca en acabar. Portem des del mòdul 6 fent-lo servir a cegues i prometent explicar-lo. La propera lliçó salda aquest deute: què fa el with per dins — el protocol __enter__/__exit__ — i com escriure gestors de context propis, inclosa una versió amb yield que et resultarà sospitosament familiar.

Curs de Programació en Python

Mòdul 1: Introducció a Python

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats