A la lliçó anterior les signatures de magatzem.py van quedar anotades, i en escriure-les va aparèixer una necessitat nova: l'Anna vol saber quant triga tancament_de_caixa() amb un any de vendes, i vol que cada crida a vendre() quedi auditada a papyrus.log. Podries obrir cada funció i afegir-hi dins el cronòmetre i el log... però això barreja la lògica de negoci amb la fontaneria, i caldria repetir-ho a cada funció. Els decoradors resolen això: emboliquen una funció amb comportament extra sense tocar-ne el codi. Ja els fas servir des del mòdul 5 (@property, @dataclass) sense saber com funcionen per dins; avui els desmuntem peça a peça.
Contingut
- Funcions com a objectes de primera classe
- Closures: funcions que recorden
- Un decorador des de zero, sense sucre sintàctic
- La sintaxi
@ *argsi**kwargs: embolicar qualsevol signaturafunctools.wraps: no perdre la identitat- Decoradors per a Papyrus:
@cronometrari@registrar_crida - Decoradors amb arguments:
@reintentar(cops=3) - Els decoradors que ja coneixies, amb una altra llum
- No sobredecorar
Funcions com a objectes de primera classe
Tot el que ve a continuació es recolza en un fet que ja vas fregar a M3 amb sorted(key=...) i les lambdes: en Python, una funció és un objecte com qualsevol altre. Es pot assignar, passar com a argument i retornar:
def saludar(nom: str) -> str:
return f"Benvingut a Papyrus, {nom}"
atendre = saludar # assignar: SENSE parentesis — la funcio, no el seu resultat
print(atendre("Omar")) # Benvingut a Papyrus, Omar
def aplicar(funcio, valor):
return funcio(valor) # passar com a argument (com key= a sorted)
print(aplicar(saludar, "Julia"))La distinció crucial és saludar (l'objecte funció) davant de saludar() (el resultat de cridar-la). Tot el mecanisme dels decoradors gira sobre aquesta diferència.
I també es poden definir i retornar funcions dins de funcions:
def crear_salutacio(botiga: str):
def salutacio(nom: str) -> str: # funcio definida a dins
return f"Benvingut a {botiga}, {nom}"
return salutacio # es retorna l'objecte funcio
saluda_papyrus = crear_salutacio("Papyrus")
print(saluda_papyrus("Julia")) # Benvingut a Papyrus, JuliaClosures: funcions que recorden
Fixa't en una cosa sorprenent de l'exemple anterior: quan crear_salutacio("Papyrus") acaba, la seva variable local botiga hauria de desaparèixer... i tanmateix saluda_papyrus la continua fent servir. Una funció interna que captura variables de la funció que la va crear s'anomena closure (clausura). Pas a pas:
- Crides
crear_salutacio("Papyrus")→ es creabotiga = "Papyrus". - Es defineix
salutacio, que esmentabotiga— Python pren nota que la necessita. crear_salutacioretornasalutacioi acaba, peròbotigano es destrueix: viatja de "motxilla" dins de la funció retornada.- Cada crida posterior a
saluda_papyrus(...)trobabotigaintacta.
Els closures són la memòria dels decoradors: així és com la funció embolcall "recorda" quina funció embolica.
Un decorador des de zero, sense sucre
Un decorador és simplement una funció que rep una funció i en retorna una altra que l'embolica. Sense @, a mà:
def anunciar(funcio):
def embolcall():
print(f"→ Cridant {funcio.__name__}...")
resultat = funcio() # la funcio original, recordada pel closure
print(f"← {funcio.__name__} ha acabat.")
return resultat
return embolcall
def obrir_botiga():
print("Papyrus obre les portes.")
obrir_botiga = anunciar(obrir_botiga) # decorar = REASSIGNAR!
obrir_botiga()La línia clau és obrir_botiga = anunciar(obrir_botiga): el nom obrir_botiga deixa d'apuntar a la funció original i passa a apuntar a l'embolcall, que desa l'original al seu closure. Ningú no ha tocat el cos d'obrir_botiga; només se li ha posat una capa al voltant.
La sintaxi @
Com que aquest patró de reassignació és tan comú, Python li dona sucre sintàctic:
@anunciar just a sobre del def significa exactament obrir_botiga = anunciar(obrir_botiga) executat després de definir la funció. No hi ha més màgia: si entens la reassignació, entens el @.
*args i **kwargs: embolicar qualsevol signatura
L'embolcall() de dalt no accepta arguments, així que no pot embolicar vendre(cataleg, titol, unitats). La solució és el duo de M3 *args/**kwargs, que aquí troba el seu ús estrella — recollir el que sigui i passar-ho tal qual:
def anunciar(funcio):
def embolcall(*args, **kwargs):
print(f"→ {funcio.__name__}")
return funcio(*args, **kwargs) # reenvia els arguments intactes
return embolcallAmb això el decorador és universal: serveix per a funcions amb zero, tres o vint paràmetres, posicionals o amb nom. És la signatura estàndard de tot embolcall.
functools.wraps: no perdre la identitat
Hi ha un efecte secundari subtil. Un cop decorada, vendre.__name__ ja no és "vendre" sinó "embolcall", i la seva docstring — el contracte que tant hem cuidat a 08-01 — desapareix. Per al logger.exception() de M7, que registra noms de funcions, això és un problema real. functools.wraps copia la identitat de l'original a l'embolcall:
import functools
def anunciar(funcio):
@functools.wraps(funcio) # ← un decorador... dins del teu decorador
def embolcall(*args, **kwargs):
return funcio(*args, **kwargs)
return embolcallSense @functools.wraps |
Amb @functools.wraps |
|---|---|
vendre.__name__ → "embolcall" |
vendre.__name__ → "vendre" |
help(vendre) → docstring de l'embolcall (o res) |
help(vendre) → docstring original |
| Traces i logs confusos | Traces i logs correctes |
Regla sense excepcions: tot decorador porta @functools.wraps.
Decoradors per a Papyrus
@cronometrar: mesurar tancament_de_caixa
import functools
import logging
import time
logger = logging.getLogger(__name__) # el patro per modul de 07-05
def cronometrar(funcio):
"""Registra al log quant triga cada crida."""
@functools.wraps(funcio)
def embolcall(*args, **kwargs):
inici = time.perf_counter()
try:
return funcio(*args, **kwargs)
finally: # es mesura fins i tot si la funcio llanca (07-02)
durada = time.perf_counter() - inici
logger.info("%s ha trigat %.3f s", funcio.__name__, durada)
return embolcall
@cronometrar
def tancament_de_caixa(ruta_vendes):
...Després d'executar el tancament, a dades/papyrus.log apareix amb el format de M7:
El try/finally garanteix la mesura encara que tancament_de_caixa llanci: la mateixa disciplina de neteja de 07-02.
@registrar_crida: auditoria de vendes
L'Anna vol poder reconstruir què s'ha venut i què ha fallat. En comptes de ficar logs dins de vendre(), s'embolica:
def registrar_crida(funcio):
"""Deixa al log cada crida, el seu resultat o la seva excepcio."""
@functools.wraps(funcio)
def embolcall(*args, **kwargs):
logger.info("crida: %s args=%r kwargs=%r", funcio.__name__, args, kwargs)
try:
resultat = funcio(*args, **kwargs)
logger.info("ok: %s → %r", funcio.__name__, resultat)
return resultat
except Exception:
logger.exception("fallada a %s", funcio.__name__) # amb traca, com a 07-05
raise # rellançar: el decorador no decideix
return embolcall
@registrar_crida
def vendre(cataleg, titol, unitats):
...Una venda de Hamlet x2 deixa ok: vendre → 20.7; un intent de vendre 9 exemplars de L'Odissea amb 4 en estoc deixa la traça completa d'EstocInsuficientError. I atenció al raise nu: el decorador observa i rellança. Atrapar i silenciar aquí violaria la regla de les tres anelles de 07-04 — la decisió sobre l'excepció pertany al taulell o a la frontera, no a l'embolcall.
Decoradors amb arguments: @reintentar(cops=3)
I si desar el catàleg falla esporàdicament (disc de xarxa, antivirus) i vols reintentar? Voldríem escriure @reintentar(cops=3). Però això vol dir que reintentar(cops=3) es crida primer i ha de retornar un decorador: és una fàbrica de decoradors. Tres nivells:
def reintentar(cops: int = 3, pausa: float = 0.5):
"""Fabrica: retorna un decorador que reintenta fins a `cops` vegades."""
def decorador(funcio): # nivell 2: el decorador real
@functools.wraps(funcio)
def embolcall(*args, **kwargs): # nivell 3: l'embolcall
for intent in range(1, cops + 1):
try:
return funcio(*args, **kwargs)
except OSError: # nomes errors transitoris, no bugs
logger.warning("intent %d/%d de %s ha fallat",
intent, cops, funcio.__name__)
if intent == cops:
raise # esgotats els intents, propaga
time.sleep(pausa)
return embolcall
return decorador
@reintentar(cops=3)
def desar_cataleg(cataleg, ruta):
...Lectura en cadena: @reintentar(cops=3) → s'executa reintentar(cops=3) → retorna decorador → decorador(desar_cataleg) → retorna embolcall → desar_cataleg = embolcall. Cada nivell és un closure: embolcall recorda alhora funcio, cops i pausa. Fixa't en la tria d'except OSError: es reintenta allò transitori; un TypeError és un bug i ha de petar a la primera (07-04).
Els decoradors que ja coneixies, amb una altra llum
| Decorador | On el vas veure | Què fa, ara que saps com |
|---|---|---|
@property |
M5 (encapsulació) | Embolica el mètode perquè s'hi accedeixi com a atribut |
@dataclass |
05-06 | Decora la classe: en llegeix les anotacions (08-01) i li genera __init__, __repr__, __eq__ |
@staticmethod / @classmethod |
M5 | Canvien com es passa (o no) self/cls al mètode |
@functools.wraps |
Aquesta lliçó | Un decorador que arregla decoradors |
Cap no era màgia: tots són "funció (o classe) entra, versió embolicada o transformada surt".
No sobredecorar
Un advertiment abans que decoris tota la botiga: cada decorador afegeix una capa a travessar quan depures. Si vendre porta @cronometrar, @registrar_crida i @reintentar, una traça d'error mostra tres embolcalls abans d'arribar al codi real, i l'ordre d'apilament comença a importar (s'apliquen de baix a dalt). Criteri pràctic: decora quan el comportament sigui transversal i repetit (logging, mesura, reintents); no decoris per a lògica de negoci d'una sola funció — això va dins de la funció, on es veu.
Errors Comuns i Consells
- Decorar amb parèntesis de més o de menys:
@cronometrar()falla sicronometrarés un decorador simple, i@reintentar(sense parèntesis) falla si és una fàbrica. Regla: fàbrica → sempre amb parèntesis, encara que facis servir els valors per defecte (@reintentar()). - Oblidar
return funcio(...)a l'embolcall: la funció decorada passa a retornarNonesilenciosament. Sivendredecorada deixa de retornar la quantia, revisa l'embolcall abans que la funció. - Oblidar
@functools.wraps: els logs de 07-05 comencen a dirembolcallen comptes devendrei la depuració es torna un laberint. - Empassar-se excepcions al decorador: un
except Exception: passen un embolcall és el "silenci" que 07-04 va prohibir, ara camuflat. Observa, registra amblogger.exception, i rellança. - Consell: escriu primer el decorador sense
@, amb la reassignació explícita, fins que el flux et resulti obvi. El@arriba sol. - Consell: els embolcalls es beneficien dels type hints de 08-01 —
def embolcall(*args, **kwargs) -> ...es pot anotar amb precisió fent servirtyping.Callable; guarda-t'ho com a lectura opcional, encara no ho necessites.
Exercicis
- Escriu
@nomes_socis, un decorador per a funcions el primer argument de les quals és un codi de soci ("LLUIS-001","MARTA-002","PAU-003"). Si el codi no és al conjunt de socis vàlids, ha de llançarSociInvalidError(codi)sense cridar la funció; si és vàlid, la crida normalment. No oblidisfunctools.wraps. - Sense fer servir la sintaxi
@, decoratancament_de_caixaamb elcronometrarde la lliçó mitjançant reassignació explícita, i explica en un comentari per quètancament_de_caixa.__name__continua sent"tancament_de_caixa". - Converteix
@cronometraren una fàbrica@cronometrar(llindar=1.0)que només escrigui al log si la funció ha trigat més dellindarsegons (per no omplirpapyrus.logamb tancaments ràpids).
Solucions
-
import functools from errors import SociInvalidError SOCIS_VALIDS: set[str] = {"LLUIS-001", "MARTA-002", "PAU-003"} def nomes_socis(funcio): @functools.wraps(funcio) def embolcall(codi, *args, **kwargs): if codi not in SOCIS_VALIDS: raise SociInvalidError(codi) # guard clause de 07-03, ara reutilitzable return funcio(codi, *args, **kwargs) return embolcallL'embolcall anomena
codiexplícitament (el necessita per validar) i reenvia la resta amb*args, **kwargs. -
tancament_de_caixa = cronometrar(tancament_de_caixa) # __name__ segueix sent "tancament_de_caixa" perque l'embolcall porta # @functools.wraps(funcio), que copia __name__ i la docstring de la # funcio original a l'embolcall abans de retornar-lo. -
def cronometrar(llindar: float = 0.0): def decorador(funcio): @functools.wraps(funcio) def embolcall(*args, **kwargs): inici = time.perf_counter() try: return funcio(*args, **kwargs) finally: durada = time.perf_counter() - inici if durada > llindar: logger.info("%s ha trigat %.3f s", funcio.__name__, durada) return embolcall return decorador @cronometrar(llindar=1.0) def tancament_de_caixa(ruta_vendes): ...llindarviatja al closure a través dels tres nivells. Error comú aquí: deixar@cronometrarsense parèntesis — ara és fàbrica i cal cridar-la sempre.
Conclusió
Un decorador no té res de màgic: funcions que són objectes, closures que recorden, i una reassignació que el @ abreuja. Amb @cronometrar, @registrar_crida i @reintentar(cops=3), Papyrus afegeix mesura, auditoria i resiliència sense embrutar vendre() ni tancament_de_caixa() — i de passada @property i @dataclass han deixat de ser encanteris. Precisament en cronometrar el tancament apareix la pregunta següent: tancament_de_caixa() llegeix vendes.csv sencer per sumar-lo, i el fitxer creix cada dia. Cal carregar en memòria un any de vendes per sumar les d'avui? No: es poden produir i consumir les files sota demanda, d'una en una. Aquest és el terreny dels generadors i del yield, la propera lliçó.
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
