Des de 05-01 arrosseguem un deute: print(odissea) mostra <__main__.Llibre object at 0x...>. I hi ha més mancances silencioses: dos objectes Llibre amb el mateix títol no són iguals per a ==, sorted() no els sap ordenar sense key=, i len() els rebutja. Els tipus integrats fan tot això amb naturalitat perquè implementen mètodes màgics (o dunder methods, per double underscore): mètodes amb nom __aixi__ que Python invoca automàticament quan uses operadors, print, len, in, sorted... En aquesta lliçó les teves classes aprendran aquest idioma, amb un vehicle nou del fil de Papyrus: la classe Cistell, la cistella de la compra de la botiga de l'Anna.
Contingut
- Què és un mètode màgic (i un que ja coneixes)
__str__vs__repr__: saldar elprintlleig__eq__: quan dos llibres són el mateix llibre__lt__: ordenar ambsorted()sensekey=- La classe
Cistellde Papyrus __len__,__contains__i__add__al cistell- Taula dels dunder més usats
- No abusar de la màgia
- Errors comuns i consells
- Exercicis amb solucions
Què és un mètode màgic (i un que ja coneixes)
Un mètode màgic és un mètode amb doble guió baix al principi i al final que tu defineixes però no crides: el crida Python en usar certa sintaxi. Ja en coneixes un: __init__, que s'executa sol en escriure Llibre(...). La taula mental és sempre la mateixa:
| Tu escrius... | Python executa... |
|---|---|
Llibre("Hamlet", 9.95) |
Llibre.__init__(objecte, "Hamlet", 9.95) |
print(llibre) / str(llibre) |
llibre.__str__() |
llibre_a == llibre_b |
llibre_a.__eq__(llibre_b) |
llibre_a < llibre_b |
llibre_a.__lt__(llibre_b) |
len(cistell) |
cistell.__len__() |
llibre in cistell |
cistell.__contains__(llibre) |
cistell_a + cistell_b |
cistell_a.__add__(cistell_b) |
Aquí hi ha el mecanisme que 05-03 va deixar anunciat: len() és polimòrfica perquè cada tipus porta el seu __len__. Els operadors de Python són, en el fons, missatges polimòrfics.
__str__ vs __repr__: saldar el print lleig
Python distingeix dues representacions en text d'un objecte:
__str__: per a persones. La usenprint(),str()i les f-strings. Ha de ser llegible i agradable.__repr__: per a programadors. La usen la consola interactiva, els missatges de depuració i les col·leccions (print(llista_de_llibres)usa el__repr__de cada element). La convenció d'or: que sembli el codi necessari per recrear l'objecte.
class Llibre:
IVA_LLIBRES = 0.04
DESCOMPTE_SOCI = 0.05
def __init__(self, titol, preu, estoc=0):
self.titol = titol
self.preu = preu
self.estoc = estoc
def preu_final(self, soci=False):
descompte = Llibre.DESCOMPTE_SOCI if soci else 0
return round(self.preu * (1 - descompte) * (1 + Llibre.IVA_LLIBRES), 2)
def __str__(self):
return f"{self.titol} ({self.preu:.2f} €, estoc: {self.estoc})"
def __repr__(self):
return f"Llibre({self.titol!r}, {self.preu}, {self.estoc})"odissea = Llibre("L'Odissea", 12.50, 4)
print(odissea) # L'Odissea (12.50 €, estoc: 4) ← __str__
print(f"Nou: {odissea}") # Nou: L'Odissea (12.50 €, estoc: 4) ← __str__
print([odissea]) # [Llibre("L'Odissea", 12.5, 4)] ← __repr__
odissea # Llibre("L'Odissea", 12.5, 4) (a la consola interactiva)Ganxo de 05-01 saldat. Dos apunts: el !r dins de la f-string demana el repr del valor (per això el títol surt amb cometes — i fixa't que, com que "L'Odissea" conté un apòstrof, Python el mostra amb cometes dobles), i si només defineixes __repr__, print() l'usarà com a reserva — per això, si només n'has d'escriure un, que sigui __repr__.
__eq__: quan dos llibres són el mateix llibre
Sense __eq__, == compara identitat (són el mateix objecte en memòria?), no contingut:
a = Llibre("Hamlet", 9.95, 6)
b = Llibre("Hamlet", 9.95, 6)
print(a == b) # False (!) — objectes diferents, encara que identicsPer a Papyrus decidim una regla de negoci: dos llibres són el mateix si el seu títol normalitzat coincideix — la mateixa normalització strip().casefold() que cercar_llibre() usa des de 04-05:
def __eq__(self, altre):
if not isinstance(altre, Llibre):
return NotImplemented
return self.titol.strip().casefold() == altre.titol.strip().casefold()NotImplemented(sense llançar cap error) li diu a Python "jo no sé comparar-me amb això": aixíodissea == 42retornaFalsenetament en lloc de trencar-se, i Python pot donar l'oportunitat a l'altre operand. És un dels usos legítims d'isinstance(05-03): comprovar abans de comparar, no decidir comportament.- Amb
__eq__definit,!=funciona sol (Python el deriva).
Un efecte col·lateral que has de conèixer: en definir __eq__, Python desactiva el hash per defecte i l'objecte deixa de poder-se guardar en sets o com a clau de diccionari (04-04). És deliberat: igualtat i hash han de ser coherents. La solució (definir __hash__) queda anotada a la taula final; la dataclass de la propera lliçó ho gestionarà per nosaltres.
__lt__: ordenar amb sorted() sense key=
sorted() només necessita saber si un element és "menor que" un altre: exactament el que expressa __lt__ (less than). Definim l'ordre natural dels llibres de Papyrus com l'alfabètic per títol normalitzat:
def __lt__(self, altre):
if not isinstance(altre, Llibre):
return NotImplemented
return self.titol.casefold() < altre.titol.casefold()prestatgeria = [Llibre("Hamlet", 9.95, 6), Llibre("Faust", 21.00, 0),
Llibre("L'Odissea", 12.50, 4), Llibre("El Quixot", 15.90, 8)]
for llibre in sorted(prestatgeria): # sense key=!
print(llibre)El Quixot (15.90 €, estoc: 8) Faust (21.00 €, estoc: 0) Hamlet (9.95 €, estoc: 6) L'Odissea (12.50 €, estoc: 4)
min(), max() i list.sort() també funcionen ja sense key=. Criteri de disseny: defineix __lt__ només si la teva classe té un ordre natural evident. Si de vegades ordenes per preu i d'altres per estoc, no hi ha ordre natural: continua usant key= amb lambdes (03-03), que per a això hi són.
La classe Cistell de Papyrus
Els clients de l'Anna omplen cistells, i un cistell demana a crits comportar-se com una col·lecció: tenir longitud, poder preguntar-li "hi és aquest llibre?", sumar-se amb un altre. És composició (05-02): el cistell té llibres.
class Cistell:
"""El cistell de la compra d'un client de Papyrus."""
def __init__(self, client, llibres=None):
self.client = client
self._llibres = list(llibres) if llibres else [] # copia defensiva (04-01)
def afegir(self, llibre):
self._llibres.append(llibre)
def total(self, soci=False):
return round(sum(llibre.preu_final(soci) for llibre in self._llibres), 2)
def __len__(self):
return len(self._llibres)
def __contains__(self, cercat):
return any(llibre == cercat for llibre in self._llibres)
def __add__(self, altre):
if not isinstance(altre, Cistell):
return NotImplemented
nom = f"{self.client} i {altre.client}"
return Cistell(nom, self._llibres + altre._llibres) # cistell NOU!
def __str__(self):
return f"Cistell de {self.client}: {len(self)} articles, {self.total():.2f} €"
def __repr__(self):
return f"Cistell({self.client!r}, {self._llibres!r})"__len__, __contains__ i __add__ al cistell
odissea = Llibre("L'Odissea", 12.50, 4)
hamlet = Llibre("Hamlet", 9.95, 6)
quixot = Llibre("El Quixot", 15.90, 8)
cistell_julia = Cistell("Julia", [odissea, hamlet])
cistell_omar = Cistell("Omar", [quixot])
print(len(cistell_julia)) # 2 → __len__
print(hamlet in cistell_julia) # True → __contains__
print(quixot in cistell_julia) # False
comanda_conjunta = cistell_julia + cistell_omar # __add__
print(comanda_conjunta) # Cistell de Julia i Omar: 3 articles, 39.89 €
print(len(cistell_julia)) # 2 → els originals queden intactesTres decisions de disseny que convé subratllar:
__contains__reutilitza__eq__: com queLlibrecompara per títol normalitzat,Llibre(" hamlet ", 0) in cistell_juliadonaTrue. Els dunder es recolzen els uns en els altres — coherència gratis.__add__retorna un cistell NOU i no modifica els operands, igual que[1, 2] + [3]crea una llista nova. Un+que mutés els seus operands seria una trampa per a qui l'usés.__len__regala truthiness (02-01): un cistell amb__len__a 0 és fals dins d'unif, així queif cistell:significa "si té alguna cosa" — exactament com les llistes i els diccionaris.- I sumar cistells tenia sentit? Aquí sí (la Júlia i l'Omar comparteixen comanda per estalviar enviament): el
+té un significat evident en el domini. Aquesta és la vara de mesurar, com veuràs de seguida.
Taula dels dunder més usats
| Mètode | L'activa | Ha de retornar | A Papyrus |
|---|---|---|---|
__init__ |
Classe(...) |
None |
Inicialitzar llibre/cistell |
__str__ |
print(), str(), f-strings |
str llegible |
"L'Odissea (12.50 €, estoc: 4)" |
__repr__ |
Consola, depuració, col·leccions | str recreable |
Llibre("L'Odissea", 12.5, 4) |
__eq__ |
==, !=, in (via contains per defecte) |
bool o NotImplemented |
Mateix títol normalitzat |
__lt__ |
<, sorted(), min(), max() |
bool o NotImplemented |
Ordre alfabètic |
__hash__ |
sets, claus de dict | int coherent amb __eq__ |
Necessari si defineixes __eq__ i vols sets |
__len__ |
len(), truthiness |
int ≥ 0 |
Articles del cistell |
__contains__ |
x in obj |
bool |
El llibre és al cistell? |
__add__ |
+ |
Objecte nou o NotImplemented |
Fusionar cistells |
__getitem__ |
obj[clau], slicing |
L'element | (No el necessitem... encara) |
__iter__ |
for x in obj |
Un iterador | Arribarà amb els generadors (M8) |
No abusar de la màgia
Els mètodes màgics són sucre potentíssim, i per això mateix perillós. La regla: implementa un dunder només si l'operació té un significat obvi i indiscutible en el teu domini.
cistell_a + cistell_b? És clar: ajuntar comandes. ✔llibre_a + llibre_b? Què seria això: un tom doble, sumar preus, concatenar títols? Ambigu → mètode explícit amb nom (empaquetar_amb(altre)o similar). ✘cistell - llibre? Discutible;cistell.treure(llibre)s'entén sense obrir la documentació. ✘llibre * 3? No:llibre.preu_final() * 3o un mètodesubtotal(unitats=3). ✘
Qui llegeix a + b no pot consultar el manual de cada + del programa. Si dubtes entre un operador i un mètode amb nom, guanya el nom. Els dunder de representació (__str__, __repr__) són l'excepció: defineix-los gairebé sempre, mai no fan nosa.
Errors Comuns i Consells
- Cridar els dunder directament (
llibre.__str__(),cistell.__len__()): funciona, però és estil pobre. Escriustr(llibre)ilen(cistell); els dunder són per definir, no per cridar. - Retornar alguna cosa que no és cadena a
__str__/__repr__:TypeError: __str__ returned non-string. Retorna semprestr, no facisprinta dins. - Llançar un error a
__eq__davant de tipus aliens en lloc de retornarNotImplemented: trenca coses tan bàsiques comllibre in llista_mixta. Compara ambisinstancei delega ambNotImplemented. - Definir
__eq__i oblidar l'efecte sobre__hash__: els teus objectes deixaran d'entrar en sets i dicts (TypeError: unhashable type). Si els hi necessites, defineix__hash__sobre les mateixes dades que la igualtat:def __hash__(self): return hash(self.titol.strip().casefold()). - Mutar
selfdins de__add__:+ha de crear un objecte nou. Si vols mutar, l'operador és+=(__iadd__) o, millor per començar, un mètodeafegir(). - Un
__str__que amaga informació en depuració: per inspeccionar usarepr()(o!ra les f-strings), que per a això existeix la parella. - Consell: l'ordre d'escriptura sa en una classe nova és:
__init__→__repr__→__str__→ i només després, els operadors que el domini demani a crits.
Exercicis
Exercici 1: Soci presentable i comparable
Dona a la classe Soci (nom, codi) un __repr__ canònic, un __str__ tipus "Lluis (LLUIS-001)" i un __eq__ que consideri iguals dos socis amb el mateix codi (tant li fa el nom). Comprova que Soci("Lluis", "LLUIS-001") == Soci("Luis", "LLUIS-001") és True.
Exercici 2: la biblioteca mesurable i ordenada
Crea la classe Biblioteca que contingui objectes Llibre (composició) amb: __len__ (nombre d'exemplars totals, sumant estocs — compte, no nombre de títols), __contains__ (per igualtat de llibres) i __str__ ("Biblioteca Papyrus: 18 exemplars de 4 titols"). Prova-la amb el catàleg canònic (estocs 4, 6, 8, 0).
Exercici 3: sumar una llista de cistells
Al tancament del dia, l'Anna vol fusionar tots els cistells pendents amb sum(llista_de_cistells). Tal qual, falla: sum comença sumant 0 + primer_cistell, i ni int ni el nostre __add__ no saben resoldre això. Implementa a Cistell el mètode màgic __radd__ (suma reflectida) mínim perquè funcioni: ha d'acceptar el cas 0 + cistell retornant el mateix cistell, i delegar amb NotImplemented en qualsevol altre.
Solucions
Solució 1:
class Soci:
def __init__(self, nom, codi):
self.nom = nom
self.codi = codi
def __repr__(self):
return f"Soci({self.nom!r}, {self.codi!r})"
def __str__(self):
return f"{self.nom} ({self.codi})"
def __eq__(self, altre):
if not isinstance(altre, Soci):
return NotImplemented
return self.codi == altre.codi
print(Soci("Lluis", "LLUIS-001") == Soci("Luis", "LLUIS-001")) # TrueEl codi de soci és l'identificador de negoci; el nom és només presentació. Decidir què significa la igualtat és disseny, no sintaxi.
Solució 2:
class Biblioteca:
def __init__(self, nom, llibres=None):
self.nom = nom
self._llibres = list(llibres) if llibres else []
def __len__(self):
return sum(llibre.estoc for llibre in self._llibres)
def __contains__(self, cercat):
return any(llibre == cercat for llibre in self._llibres)
def __str__(self):
return f"Biblioteca {self.nom}: {len(self)} exemplars de {len(self._llibres)} titols"
papyrus = Biblioteca("Papyrus", [
Llibre("L'Odissea", 12.50, 4), Llibre("Hamlet", 9.95, 6),
Llibre("El Quixot", 15.90, 8), Llibre("Faust", 21.00, 0),
])
print(papyrus) # Biblioteca Papyrus: 18 exemplars de 4 titols
print(Llibre("faust", 0) in papyrus) # True (igualtat per titol normalitzat)Fixa't que len() respon una pregunta de negoci (exemplars), no la trivial (títols): tu decideixes la semàntica, però documenta-la, perquè qui llegeixi len(papyrus) assumirà alguna cosa.
Solució 3:
class Cistell:
# ... tot l'anterior ...
def __radd__(self, altre):
if altre == 0: # sum() arrenca amb 0: 0 + primer_cistell
return self
return NotImplemented
total = sum([cistell_julia, cistell_omar])
print(total) # Cistell de Julia i Omar: 3 articles, 39.89 €__radd__ (suma reflectida) s'invoca quan l'operand esquerre no sap sumar amb nosaltres: 0 + cistell falla a int.__add__ i Python prova cistell.__radd__(0). Retornar self en aquest cas fa que sum() encadeni després els __add__ normals.
Conclusió
Deute saldat: print(odissea) ja parla clar gràcies a __str__, amb __repr__ cobrint la depuració; __eq__ ha fixat la regla de negoci de Papyrus (mateix títol normalitzat, mateix llibre) retornant NotImplemented davant d'estranys; __lt__ ha donat a sorted() l'ordre alfabètic sense key=; i el Cistell es comporta com una col·lecció de ple dret amb __len__, __contains__ i un __add__ que crea cistells nous sense mutar els originals. També has après el fre: només operadors amb significat indiscutible, i noms explícits per a tota la resta. Però siguem honestos amb el preu pagat: entre __init__, __repr__, __eq__ i __lt__, la classe Llibre acumula ja una pila de codi repetitiu que només recita els seus atributs una vegada i una altra. Python ho sap, i porta la solució de sèrie: les dataclasses, que generen tota aquesta maquinària automàticament en una línia. Amb elles tancarem el mòdul — i deixarem el catàleg de Papyrus en la seva forma definitiva.
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
