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

  1. Què és un mètode màgic (i un que ja coneixes)
  2. __str__ vs __repr__: saldar el print lleig
  3. __eq__: quan dos llibres són el mateix llibre
  4. __lt__: ordenar amb sorted() sense key=
  5. La classe Cistell de Papyrus
  6. __len__, __contains__ i __add__ al cistell
  7. Taula dels dunder més usats
  8. No abusar de la màgia
  9. Errors comuns i consells
  10. 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 usen print(), 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 identics

Per 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 == 42 retorna False netament 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).
print(Llibre("Hamlet", 9.95) == Llibre("  hamlet ", 12.00))   # True: mateix titol normalitzat

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 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 intactes

Tres decisions de disseny que convé subratllar:

  • __contains__ reutilitza __eq__: com que Llibre compara per títol normalitzat, Llibre(" hamlet ", 0) in cistell_julia dona True. 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'un if, així que if 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() * 3 o un mètode subtotal(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. Escriu str(llibre) i len(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 sempre str, no facis print a dins.
  • Llançar un error a __eq__ davant de tipus aliens en lloc de retornar NotImplemented: trenca coses tan bàsiques com llibre in llista_mixta. Compara amb isinstance i delega amb NotImplemented.
  • 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 self dins de __add__: + ha de crear un objecte nou. Si vols mutar, l'operador és += (__iadd__) o, millor per començar, un mètode afegir().
  • Un __str__ que amaga informació en depuració: per inspeccionar usa repr() (o !r a 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"))   # True

El 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

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