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

  1. Què fa realment el with
  2. El protocol: __enter__ i __exit__
  3. Reimplementar (conceptualment) el with de fitxers
  4. Un gestor propi com a classe: TransaccioCataleg
  5. __exit__ i les excepcions: propagar o suprimir
  6. contextlib.@contextmanager: la versió generador
  7. Altres usos: cronometrar un bloc, canviar de directori
  8. 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 excepcio

Qualsevol 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 : tots tres són None.
  • Si el bloc ha llançat: exc_type és la classe de l'excepció (p. ex. EstocInsuficientError), exc_val la instància (amb els seus atributs titol, demanat, disponible de 07-04) i exc_tb el 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 propaga

deepcopy (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 (o None, que és el que retorna un mètode sense return): 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 classe

La 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 del with.
  • Si el bloc llança, l'excepció apareix al punt del yield — per això s'embolica amb try/except (o try/finally si només cal netejar).
  • Reprendre i acabar sense més equival a propagar normalitat; raise propaga l'excepció; atrapar-la sense rellançar equival al return True.
with transaccio_cataleg(cataleg) as cat:
    vendre(cat, "Hamlet", 2)

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 peti

Fixa'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 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 try al voltant del yield en un @contextmanager: si el bloc llança, el teu codi de neteja no s'executa — el generador mor al yield. El try/finally (o try/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) sense with encara 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 els Llibre; la reversió deixaria estocs corruptes. Per a transaccions, copy.deepcopy.
  • Consell: si escrius try/finally al 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

  1. Escriu com a classe el gestor RegistreBloc(nom) que en entrar escrigui al log INICI <nom> i en sortir FI <nom> (ok) o FI <nom> (error: <TipusExc>) segons hi hagi hagut excepció o no, propagant-la sempre. Prova'l embolicant una venda que llanci EstocInsuficientError.
  2. Reescriu RegistreBloc amb @contextmanager (anomena'l registre_bloc). Quina construcció substitueix la comprovació if exc_type is not None?
  3. Escriu copia_de_seguretat(ruta) amb @contextmanager: abans del bloc copia el fitxer a copies/<nom>.bak (fes servir shutil.copy2, de M6); si el bloc falla, restaura el .bak sobre l'original i rellança; si va bé, no fa res més. Fes-lo servir al voltant de desar_cataleg.

Solucions

  1. 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 EstocInsuficientError
    

    A papyrus.log hi queda INICI + FI ... (error: EstocInsuficientError), i l'excepció segueix cap al taulell.

  2. @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__)
            raise
    

    La comprovació if exc_type is not None es converteix en l'estructura try/except: el camí sense excepció continua després del yield; el camí amb excepció entra per l'except — i el raise final preserva la propagació.

  3. 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 TransaccioCataleg portada 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

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