Aquesta és la lliçó de saldar el deute més antic del curs. Des del mòdul 6 escrius with ruta.open() as f: acceptant dues promeses: "el fitxer es tanca sol, passi el que passi" i "algun dia veurem com funciona per dins". El mòdul 7 hi va afegir la pista clau — el try/finally garanteix la neteja fins i tot amb excepcions — i la lliçó anterior et va ensenyar un with dins d'un generador. Avui es tanca el cercle: el protocol __enter__/__exit__ que hi ha darrere de cada with, i amb ell construirem gestors de context propis per a Papyrus, inclosa una TransaccioCataleg que deixa el catàleg fora de perill si una venda es trenca a mitges.
Contingut
- Què fa realment el
with - El protocol:
__enter__i__exit__ - Reimplementar (conceptualment) el
withde fitxers - Un gestor propi com a classe:
TransaccioCataleg __exit__i les excepcions: propagar o suprimircontextlib.@contextmanager: la versió generador- Altres usos: cronometrar un bloc, canviar de directori
- Classe vs
@contextmanager: taula de decisió
Què fa realment el with
Comencem pel que el with no és: no és un bloc màgic "per a fitxers". És sucre sintàctic sobre un patró try/finally que ja sabries escriure a mà amb el que vas aprendre a 07-02:
# El que escrius:
with (BASE / "dades" / "cataleg.json").open(encoding="utf-8") as f:
dades = f.read()
# El que Python executa (equivalent essencial):
f = (BASE / "dades" / "cataleg.json").open(encoding="utf-8")
f_ctx = f.__enter__() # preparar el context; el seu retorn va a l'"as"
try:
dades = f_ctx.read()
finally:
f.__exit__(None, None, None) # netejar SEMPRE: amb exit o amb excepcioQualsevol objecte que implementi aquests dos mètodes dunder — i ja saps de dunders pel mòdul 5 — es pot fer servir amb with. Se n'diu gestor de context (context manager): gestiona l'entrada i la sortida d'un context, garantint la sortida.
El protocol: __enter__ i __exit__
| Mètode | Quan es crida | Què fa | Què retorna |
|---|---|---|---|
__enter__(self) |
En entrar al with |
Adquirir/preparar el recurs | El que vulguis lligar a l'as (sovint self) |
__exit__(self, exc_type, exc_val, exc_tb) |
En sortir, sempre | Alliberar/netejar | True → suprimeix l'excepció; False/None → la propaga |
Els tres arguments d'__exit__ descriuen què ha passat dins del bloc:
- Si el bloc ha acabat bé: tots tres són
None. - Si el bloc ha llançat:
exc_typeés la classe de l'excepció (p. ex.EstocInsuficientError),exc_valla instància (amb els seus atributstitol,demanat,disponiblede 07-04) iexc_tbel traceback.
És a dir: __exit__ és un finally amb superpoders — no només s'executa sempre, sinó que a més sap si hi ha hagut excepció i quina.
Reimplementar el with de fitxers
Per fixar el concepte, escrivim el nostre propi "open" conceptual (el de debò és en C, però el protocol és idèntic):
class Fitxer:
"""Reimplementacio didactica del context manager d'open()."""
def __init__(self, ruta, mode="r", encoding="utf-8"):
self.ruta = ruta
self.mode = mode
self.encoding = encoding
self.f = None
def __enter__(self):
print(f"[enter] obrint {self.ruta}")
self.f = open(self.ruta, self.mode, encoding=self.encoding)
return self.f # aixo es el que rep l'"as f"
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"[exit] tancant {self.ruta} (excepcio: {exc_type})")
self.f.close() # passi el que passi
return False # no suprimim res: que es propagui
with Fitxer(BASE / "dades" / "registre_altes.txt") as f:
print(f.readline())Si dins del bloc alguna cosa llança, veuràs [exit] tancant ... (excepcio: <class '...'>) abans que la traça arribi a la teva pantalla: la neteja ha passat, i l'excepció ha seguit el seu camí cap a les anelles de 07-04. Exactament les dues promeses del with, ara sense misteri.
TransaccioCataleg: el cas real de Papyrus
Ara un gestor que resol un problema de debò. vendre() descompta estoc i després es registra la venda i es desa el catàleg. Què passa si l'Anna ven un lot de tres títols a la Marta i el segon llança EstocInsuficientError? El primer ja s'ha descomptat: el catàleg queda a mitges, en un estat que no correspon a cap venda real. Volem semàntica de transacció: o tot, o res.
import copy
import logging
logger = logging.getLogger(__name__)
class TransaccioCataleg:
"""Si el bloc falla, restaura el cataleg a l'estat inicial i propaga l'error."""
def __init__(self, cataleg: dict):
self.cataleg = cataleg
self.copia = None
def __enter__(self):
self.copia = copy.deepcopy(self.cataleg) # foto de l'estat inicial
return self.cataleg # es treballa sobre l'original
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None: # alguna cosa s'ha trencat a dins
self.cataleg.clear()
self.cataleg.update(self.copia) # restaurar la foto
logger.warning("transaccio revertida per %s: %s",
exc_type.__name__, exc_val)
return False # False → l'excepcio es propagadeepcopy (cosí profund del copy que vam fregar a M4) fotografia catàleg i llibres; restaurar mutant amb clear()/update() — en comptes de reassignar — manté vàlida qualsevol altra referència al mateix diccionari. El seu ús al taulell:
from magatzem import vendre
from errors import ErrorPapyrus
lot = [("Hamlet", 2), ("L'Odissea", 9), ("Faust", 1)] # L'Odissea: nomes n'hi ha 4
try:
with TransaccioCataleg(cataleg) as cat:
for titol, unitats in lot:
vendre(cat, titol, unitats)
except ErrorPapyrus as e:
print(f"Venda cancelada, el cataleg queda intacte: {e}")Seqüència: es venen 2 Hamlet (estoc 6 → 4)... i L'Odissea amb 9 unitats demanades i 4 de disponibles llança EstocInsuficientError. __exit__ la veu arribar, restaura la foto — Hamlet torna a 6 — en deixa constància a WARNING a papyrus.log, i retorna False: l'excepció continua pujant fins a l'except ErrorPapyrus del taulell, que informa amb la calma de 07-04. Ningú no ha perdut dades; ningú no ha silenciat res.
Propagar o suprimir: el retorn d'__exit__
El valor retornat per __exit__ és un interruptor delicat:
False(oNone, que és el que retorna un mètode sensereturn): l'excepció es propaga. És el correcte en el 95 % dels casos — el gestor neteja, no decideix.True: l'excepció es suprimeix, com si el bloc hagués anat bé. Només té sentit quan suprimir és el servei que ofereixes (p. ex.contextlib.suppress(FileNotFoundError), que la biblioteca estàndard ja porta fet).
Un return True descuidat és l'except: pass de 07-04 disfressat d'elegància. Si dubtes, False.
contextlib.@contextmanager: la versió generador
Escriure una classe sencera per a "fes una cosa abans, fes-ne una altra després" pot ser molta cerimònia. contextlib ofereix una drecera que reuneix les dues lliçons anteriors — un decorador aplicat a un generador:
from contextlib import contextmanager
@contextmanager
def transaccio_cataleg(cataleg: dict):
copia = copy.deepcopy(cataleg) # ── aixo fa d'__enter__
try:
yield cataleg # ── aqui s'executa el bloc del with
except Exception:
cataleg.clear()
cataleg.update(copia) # ── aixo fa d'__exit__ amb excepcio
raise # rellancar = "return False" de la classeLa correspondència exacta:
- Tot el que hi ha abans del
yieldés l'__enter__. - El valor del
yieldés el que rep l'as. - El generador queda pausat al
yield(tal com ho vas veure a 08-03) mentre s'executa el cos delwith. - Si el bloc llança, l'excepció apareix al punt del
yield— per això s'embolica ambtry/except(otry/finallysi només cal netejar). - Reprendre i acabar sense més equival a propagar normalitat;
raisepropaga l'excepció; atrapar-la sense rellançar equival alreturn True.
El mateix comportament que la classe, en un terç de línies.
Altres usos: no només recursos
Qualsevol patró "preparar / desfer" hi encaixa. Dos exemples útils a Papyrus:
import time
from contextlib import contextmanager
@contextmanager
def cronometre(etiqueta: str):
"""El cosi de bloc del @cronometrar de 08-02: mesura un TROS de codi."""
inici = time.perf_counter()
try:
yield
finally:
logger.info("%s: %.3f s", etiqueta, time.perf_counter() - inici)
with cronometre("tancament + informe"):
total = tancament_de_caixa(BASE / "dades" / "vendes.csv")
generar_informe(total)import os
from pathlib import Path
@contextmanager
def en_directori(ruta: Path):
"""Canvia el cwd temporalment i SEMPRE hi torna (util a informes/)."""
anterior = Path.cwd()
os.chdir(ruta)
try:
yield ruta
finally:
os.chdir(anterior) # encara que generar l'informe petiFixa't que cronometre fa amb un bloc el que @cronometrar feia amb una funció: decoradors i context managers són les dues cares d'"embolicar codi".
Classe vs @contextmanager
| Criteri | Classe (__enter__/__exit__) |
@contextmanager |
|---|---|---|
| Línies de codi | Més (cerimònia de classe) | Menys (un generador) |
| Estat complex o mètodes extra | Natural (atributs, mètodes) | Incòmode |
| Reutilitzable/configurable amb herència | Sí | No aplica |
| Distingir tipus d'excepció a la sortida | exc_type explícit |
except TipusX: dins del generador |
| Llegibilitat per a casos simples | Correcta | Excel·lent |
| Exemple idoni | TransaccioCataleg amb opcions |
cronometre, en_directori |
Regla pràctica: comença amb @contextmanager; passa't a la classe quan necessitis estat, configuració o una jerarquia.
Errors Comuns i Consells
- Oblidar el
tryal voltant delyielden un@contextmanager: si el bloc llança, el teu codi de neteja no s'executa — el generador mor alyield. Eltry/finally(otry/except+raise) no és opcional: és l'equivalent de l'__exit__. return True(o atrapar sense rellançar) per accident: suprimeix excepcions que el taulell esperava. Símptoma típic: "la venda ha fallat però no ha saltat cap error". Revisa el retorn d'__exit__abans que res.- Fer feina pesada a
__init__en comptes d'__enter__:TransaccioCataleg(cataleg)sensewithencara no hauria de fotografiar res.__init__desa paràmetres;__enter__adquireix. - Copiar superficialment quan hi ha objectes imbricats:
dict(cataleg)copia el diccionari però no elsLlibre; la reversió deixaria estocs corruptes. Per a transaccions,copy.deepcopy. - Consell: si escrius
try/finallyal voltant d'un recurs més d'una vegada, és el senyal que allà hi viu un context manager amb nom propi. - Consell: es poden encadenar en un sol
with:with transaccio_cataleg(cat) as c, cronometre("venda"):— se'n surt en ordre invers a com s'hi ha entrat.
Exercicis
- Escriu com a classe el gestor
RegistreBloc(nom)que en entrar escrigui al logINICI <nom>i en sortirFI <nom> (ok)oFI <nom> (error: <TipusExc>)segons hi hagi hagut excepció o no, propagant-la sempre. Prova'l embolicant una venda que llanciEstocInsuficientError. - Reescriu
RegistreBlocamb@contextmanager(anomena'lregistre_bloc). Quina construcció substitueix la comprovacióif exc_type is not None? - Escriu
copia_de_seguretat(ruta)amb@contextmanager: abans del bloc copia el fitxer acopies/<nom>.bak(fes servirshutil.copy2, de M6); si el bloc falla, restaura el.baksobre l'original i rellança; si va bé, no fa res més. Fes-lo servir al voltant dedesar_cataleg.
Solucions
-
class RegistreBloc: def __init__(self, nom: str): self.nom = nom def __enter__(self): logger.info("INICI %s", self.nom) return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: logger.info("FI %s (ok)", self.nom) else: logger.error("FI %s (error: %s)", self.nom, exc_type.__name__) return False # propagar sempre with RegistreBloc("venda lot Marta"): vendre(cataleg, "L'Odissea", 9) # llanca EstocInsuficientErrorA
papyrus.loghi quedaINICI+FI ... (error: EstocInsuficientError), i l'excepció segueix cap al taulell. -
@contextmanager def registre_bloc(nom: str): logger.info("INICI %s", nom) try: yield logger.info("FI %s (ok)", nom) except Exception as e: logger.error("FI %s (error: %s)", nom, type(e).__name__) raiseLa comprovació
if exc_type is not Nonees converteix en l'estructuratry/except: el camí sense excepció continua després delyield; el camí amb excepció entra per l'except— i elraisefinal preserva la propagació. -
import shutil from contextlib import contextmanager @contextmanager def copia_de_seguretat(ruta: Path): resguard = BASE / "copies" / (ruta.name + ".bak") shutil.copy2(ruta, resguard) try: yield ruta except Exception: shutil.copy2(resguard, ruta) # restaurar l'original raise with copia_de_seguretat(BASE / "dades" / "cataleg.json") as ruta: desar_cataleg(cataleg, ruta)És la
TransaccioCatalegportada al disc: la mateixa idea (foto → intentar → restaurar si falla → rellançar) aplicada a un fitxer en comptes d'un diccionari.
Conclusió
Promesa saldada: el with era un protocol de dos dunders — __enter__ prepara, __exit__ neteja sempre i decideix amb el seu retorn si l'excepció es propaga (gairebé sempre False) — muntat sobre el mateix try/finally de 07-02. Papyrus ha guanyat TransaccioCataleg, que garanteix vendes de tot-o-res, i les versions @contextmanager han demostrat que decoradors (08-02) i generadors (08-03) no eren temes solts: aquí treballen junts. Amb això, el codi de Papyrus és expressiu i eficient... però continua fent una cosa cada vegada: si consultar el preu d'un llibre a una distribuïdora triga dos segons, consultar-ne tres en triga sis, amb el programa aturat esperant. Les dues últimes lliçons del mòdul ataquen aquesta espera: primer amb fils i processos — i una conversa honesta sobre el famós GIL — i després amb asyncio.
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
