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
- El problema: carregar-ho tot vs processar sota demanda
yield: funcions que es pausen- El protocol d'iteració:
next()iStopIteration - Generadors vs llistes: la taula de la veritat
- Expressions generadores
- Fitxers grans línia a línia: el generador natural
- Encadenar generadors: el pipeline de vendes
yield from, breumentitertools:isliceichain- 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 quantiesDues 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 ... StopIterationCada 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ó:
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 |
Sí | 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 resUn 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 demandaI 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:
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 segonyield 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 (sortedmaterialitza 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) endict[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. ElFileNotFoundErrorsaltarà al primernext(), potser lluny de la crida — tingues-ho en compte en llegir traces (M7). returnamb valor dins d'un generador: no "retorna" al consumidor; acaba el generador (el valor viatja amagat dins deStopIteration). Per produir valors, sempreyield.- 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](detypingocollections.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
- Escriu el generador
quanties_del_titol(ruta, titol)que produeixi (yield) les quanties (float) de totes les vendes d'un títol donat llegintvendes.csvfila a fila, i fes-lo servir ambsum()per saber quant ha facturat Hamlet en total. Reutilitzallegir_vendes. - 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. - Afegeix al pipeline de la lliçó una etapa
amb_iva_desglossat(vendes)que rebi tuples(data, titol, quantia)i produeixi(data, titol, quantia, iva)oniva = round(quantia - quantia / 1.04, 2)(la part d'IVA_LLIBRES continguda en la quantia). Munta-la entreparsejarinomes_del_diai explica per què l'ordre de les etapes no canvia la memòria consumida.
Solucions
-
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_vendes→quanties_del_titol) i una agregació: memòria constant. Si alguna fila té"gratis"com a quantia, elfloat()llançarà — pots protegir-ho amb eltrymínim de 07-02, com faparsejar. -
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
islicelliura la tercera, deixa de demanar — la resta del fitxer ni es llegeix. Ellist()final materialitza només 3 elements, per poder mirar-los. -
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 4La 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
- Introducció a Python
- Configuració de l'Entorn de Desenvolupament
- Sintaxi de Python i Tipus de Dades Bàsics
- Variables i Constants
- Entrada i Sortida Bàsica
- Entorns Virtuals i Gestió de Paquets
Mòdul 2: Estructures de Control
Mòdul 3: Funcions i Mòduls
- Definició de Funcions
- Arguments de Funció
- Funcions Lambda
- Mòduls i Paquets
- Visió General de la Biblioteca Estàndard
Mòdul 4: Estructures de Dades
Mòdul 5: Programació Orientada a Objectes
Mòdul 6: Gestió de Fitxers
- Lectura i Escriptura de Fitxers
- Treball amb Fitxers CSV
- Gestió de Dades JSON
- Operacions amb Fitxers i Directoris
Mòdul 7: Gestió d'Errors i Excepcions
- Introducció a les Excepcions
- Gestió d'Excepcions
- Llançament d'Excepcions
- Excepcions Personalitzades
- Bones Pràctiques i Registre d'Errors amb logging
Mòdul 8: Temes Avançats
- Anotacions de Tipus (type hints)
- Decoradors
- Generadors
- Gestors de Context
- Concurrència: Fils i Processos
- Asyncio per a la Programació Asíncrona
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb unittest
- Proves amb pytest
- Desenvolupament Guiat per Proves
- Tècniques de Depuració
- Ús de pdb per a la Depuració
Mòdul 10: Desenvolupament Web amb Python
- Introducció al Desenvolupament Web
- Fonaments del Framework Flask
- Construcció d'APIs REST amb Flask
- Introducció a Django
- Construcció d'Aplicacions Web amb Django
Mòdul 11: Ciència de Dades amb Python
- Introducció a la Ciència de Dades
- NumPy per a la Computació Numèrica
- Pandas per a la Manipulació de Dades
- Matplotlib per a la Visualització de Dades
- Introducció a l'Aprenentatge Automàtic amb scikit-learn
