La classe Llibre de la lliçó anterior funciona, però Papyrus no ven només llibres. L'Anna vol incorporar revistes literàries (amb número d'exemplar) i el Pau insisteix a provar amb llibres digitals, que no ocupen prestatgeria ni tenen estoc físic. Copiar la classe Llibre tres vegades i retocar-la seria repetir codi — i ja saps des del mòdul 3 que el codi repetit és codi que es desincronitza. L'herència resol el problema: una classe base (Producte) concentra allò comú, i cada classe derivada afegeix o ajusta només el que la fa diferent.

Contingut

  1. El problema: tres articles, un 80 % de codi comú
  2. Classe base i classe derivada
  3. super().__init__(): reutilitzar la inicialització del pare
  4. Estendre vs sobreescriure mètodes
  5. La jerarquia completa de Papyrus
  6. isinstance() i issubclass()
  7. Quan NO heretar: la composició com a alternativa
  8. L'MRO: en quin ordre cerca Python
  9. Errors comuns i consells
  10. Exercicis amb solucions

El problema: tres articles, un 80 % de codi comú

Llibres, revistes i llibres digitals ho comparteixen gairebé tot: títol, preu, la fórmula del preu de soci, la comprovació d'estoc. Difereixen en detalls: la revista té número; el llibre digital té format de fitxer i sempre està disponible. L'herència expressa aquesta relació "és un": una Revista és un Producte, un LlibreDigital és un Llibre.

Classe base i classe derivada

Primer, la classe base. Generalitzem preu_soci() de 05-01 en un mètode més flexible, preu_final(soci=False), que reprodueix exactament la signatura de la vella funció de papyrus_utils.py — una altra peça del mòdul 3 que es muda a viure amb les seves dades:

class Producte:
    """Qualsevol article a la venda a Papyrus."""

    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):
        """Preu amb IVA, i amb descompte si el client es soci."""
        descompte = Producte.DESCOMPTE_SOCI if soci else 0
        brut = self.preu * (1 - descompte) * (1 + Producte.IVA_LLIBRES)
        return round(brut, 2)

    def hi_ha_estoc(self):
        return self.estoc > 0

    def descripcio(self):
        return f"{self.titol} — {self.preu_final():.2f} €"

Per derivar una classe, s'escriu la base entre parèntesis:

class Revista(Producte):
    """Una revista literaria: com un Producte, pero amb numero."""

    def __init__(self, titol, numero, preu, estoc=0):
        super().__init__(titol, preu, estoc)   # delega el que es comu a Producte
        self.numero = numero                   # i afegeix el que es seu

Revista hereta tot allò que no redefineix: preu_final(), hi_ha_estoc(), els atributs de classe. Ja funciona:

quimera = Revista("Quimera", 482, 6.50, 10)
print(quimera.preu_final(soci=True))   # 6.42  → la formula heretada de Producte
print(quimera.numero)                  # 482   → l'atribut propi
print(quimera.hi_ha_estoc())           # True  → metode heretat sense tocar

super().__init__(): reutilitzar la inicialització del pare

super() retorna un accés a la classe base, i super().__init__(...) executa el seu inicialitzador. Sense aquesta crida, titol, preu i estoc no es crearien mai a l'objecte Revista:

class RevistaMal(Producte):
    def __init__(self, titol, numero, preu):
        self.numero = numero   # ha oblidat cridar super().__init__!

r = RevistaMal("Quimera", 482, 6.50)
r.preu_final()   # AttributeError: 'RevistaMal' object has no attribute 'preu'

La regla pràctica: si la derivada defineix el seu propi __init__, la seva primera línia és gairebé sempre super().__init__(...) amb els arguments que la base necessita. Si la derivada no defineix __init__, hereta el de la base automàticament i no cal fer res.

Estendre vs sobreescriure mètodes

Una classe derivada pot fer tres coses amb cada mètode heretat:

Estratègia Què fa Exemple a Papyrus
Heretar tal qual No el redefineix; usa el del pare Revista hereta preu_final()
Sobreescriure (override) El redefineix del tot LlibreDigital redefineix hi_ha_estoc()
Estendre El redefineix, però crida el del pare amb super() i hi afegeix alguna cosa Revista.descripcio() reutilitza i decora

Sobreescriure — reemplaçament total:

class LlibreDigital(Llibre):
    def hi_ha_estoc(self):
        return True   # un fitxer digital mai no s'esgota

Estendre — el pare fa la seva part i el fill la completa:

class Revista(Producte):
    def __init__(self, titol, numero, preu, estoc=0):
        super().__init__(titol, preu, estoc)
        self.numero = numero

    def descripcio(self):
        base = super().descripcio()           # "Quimera — 6.76 €"
        return f"[Revista n. {self.numero}] {base}"

La jerarquia completa de Papyrus

Ajuntem les peces. Llibre passa a heretar de Producte (i aprofita per guanyar l'atribut autor), i LlibreDigital hereta de Llibre, perquè un llibre digital és un llibre:

class Llibre(Producte):
    """Un llibre en paper del cataleg."""

    def __init__(self, titol, autor, preu, estoc=0):
        super().__init__(titol, preu, estoc)
        self.autor = autor

    def descripcio(self):
        return f"[Llibre] {self.titol} ({self.autor}) — {self.preu_final():.2f} €"


class Revista(Producte):
    """Una revista literaria numerada."""

    def __init__(self, titol, numero, preu, estoc=0):
        super().__init__(titol, preu, estoc)
        self.numero = numero

    def descripcio(self):
        return f"[Revista n. {self.numero}] {self.titol} — {self.preu_final():.2f} €"


class LlibreDigital(Llibre):
    """Un llibre sense exemplars fisics: es lliura per baixada."""

    def __init__(self, titol, autor, preu, format_fitxer="EPUB"):
        super().__init__(titol, autor, preu)   # l'estoc queda al valor per defecte, 0
        self.format_fitxer = format_fitxer

    def hi_ha_estoc(self):
        return True   # sobreescrit: sempre disponible

    def descripcio(self):
        return f"[Digital {self.format_fitxer}] {self.titol} ({self.autor}) — {self.preu_final():.2f} €"
classDiagram
    class Producte {
        +IVA_LLIBRES = 0.04
        +DESCOMPTE_SOCI = 0.05
        +titol
        +preu
        +estoc
        +preu_final(soci)
        +hi_ha_estoc()
        +descripcio()
    }
    class Llibre {
        +autor
        +descripcio()
    }
    class Revista {
        +numero
        +descripcio()
    }
    class LlibreDigital {
        +format_fitxer
        +hi_ha_estoc()
        +descripcio()
    }
    Producte <|-- Llibre
    Producte <|-- Revista
    Llibre <|-- LlibreDigital

En acció, amb el catàleg canònic enriquit:

articles = [
    Llibre("L'Odissea", "Homer", 12.50, 4),
    Llibre("Faust", "Goethe", 21.00, 0),
    Revista("Quimera", 482, 6.50, 10),
    LlibreDigital("Hamlet", "Shakespeare", 4.95, "EPUB"),
]

for article in articles:
    estat = "disponible" if article.hi_ha_estoc() else "ESGOTAT"
    print(f"{article.descripcio():<55} [{estat}]")
[Llibre] L'Odissea (Homer) — 13.00 €                    [disponible]
[Llibre] Faust (Goethe) — 21.84 €                       [ESGOTAT]
[Revista n. 482] Quimera — 6.76 €                       [disponible]
[Digital EPUB] Hamlet (Shakespeare) — 5.15 €            [disponible]

Fixa't en el detall clau: el bucle tracta els quatre per igual, i cada objecte respon a descripcio() i hi_ha_estoc() a la seva manera. Aquest fenomen té nom — polimorfisme — i és el tema complet de la propera lliçó.

isinstance() i issubclass()

  • isinstance(objecte, Classe) pregunta si un objecte és instància d'una classe o de qualsevol de les seves derivades.
  • issubclass(ClasseA, ClasseB) pregunta si una classe descendeix d'una altra.
hamlet_epub = LlibreDigital("Hamlet", "Shakespeare", 4.95)

isinstance(hamlet_epub, LlibreDigital)   # True
isinstance(hamlet_epub, Llibre)          # True  — un LlibreDigital ES un Llibre
isinstance(hamlet_epub, Producte)        # True  — i tambe un Producte
isinstance(hamlet_epub, Revista)         # False

issubclass(LlibreDigital, Producte)      # True
issubclass(Revista, Llibre)              # False — son germanes, no mare i filla

Prefereix sempre isinstance(x, Producte) a type(x) is Producte: la primera respecta l'herència; la segona només accepta el tipus exacte. Tot i així, usa-ho amb moderació: a 05-03 veuràs que encadenar isinstance per decidir comportament és un tuf de codi que el polimorfisme elimina.

Quan NO heretar: la composició com a alternativa

L'herència modela "és un". Si la relació real és "té un", l'eina correcta és la composició: guardar un objecte dins d'un altre com a atribut.

class Prestatgeria:
    """Una prestatgeria TE productes; no ES un producte."""

    def __init__(self, passadis):
        self.passadis = passadis
        self.productes = []   # composicio: objectes Producte a dins

    def posar(self, producte):
        self.productes.append(producte)

Senyals que heretar és mala idea:

  • Heretes només per "aprofitar un parell de mètodes", però la frase "X és un Y" sona forçada (una Prestatgeria és un Producte? No).
  • La derivada ha d'anul·lar o buidar la meitat del que hereta.
  • Només vols reutilitzar codi: per a això ja tens funcions, mòduls (03-04) i composició.

Regla breu que farem servir la resta del curs: herència per a "és un", composició per a "té un".

L'MRO: en quin ordre cerca Python

Quan crides hamlet_epub.preu_final(), Python cerca el mètode seguint l'MRO (Method Resolution Order, ordre de resolució de mètodes): primer a LlibreDigital, després a Llibre, després a Producte i finalment a object, la classe base implícita de totes les classes de Python. S'atura al primer lloc on el troba — per això hi_ha_estoc() respon la versió de LlibreDigital i preu_final() la de Producte.

print(LlibreDigital.__mro__)
# (<class 'LlibreDigital'>, <class 'Llibre'>, <class 'Producte'>, <class 'object'>)

Amb herència simple (una sola base per classe, com tota la jerarquia de Papyrus) l'MRO és simplement la cadena de pares. Python també permet l'herència múltipleclass C(A, B) — on l'MRO es torna més subtil; en tens prou de saber que existeix i que en aquest curs no la necessitarem.

Errors Comuns i Consells

  • Oblidar super().__init__(...) a l'__init__ de la derivada: els atributs del pare no es creen i els mètodes heretats fallen amb AttributeError en usar-los. Si defineixes __init__, delega primer.
  • Canviar l'ordre dels arguments en delegar. super().__init__(preu, titol, estoc) no dona error immediat: simplement guarda el preu com a títol. Usa arguments amb nom si dubtes: super().__init__(titol=titol, preu=preu, estoc=estoc).
  • Heretar per conveniència, no per semàntica. Si "X és un Y" no s'aguanta dit en veu alta, usa composició. Les jerarquies forçades es paguen cares quan creixen.
  • Sobreescriure un mètode i trencar-ne el contracte. Si hi_ha_estoc() retorna bool a Producte, la versió de LlibreDigital també ha de retornar bool. Qui usa un Producte qualsevol confia en aquest comportament (ho veurem formalitzat com a polimorfisme a 05-03).
  • Comparar tipus amb type(x) is Llibre quan vols incloure-hi derivades: un LlibreDigital quedaria fora. isinstance() és gairebé sempre el que busques.
  • Consell: mantén les jerarquies poc profundes (2-3 nivells com la de Papyrus). Si necessites un quart nivell, atura't i pregunta't si la composició no ho resoldria millor.

Exercicis

Exercici 1: la classe Mapa

L'Anna comença a vendre mapes il·lustrats de ciutats literàries. Crea Mapa(Producte) amb un atribut extra ciutat i una descripcio() que retorni "[Mapa] Dublin de Joyce (Dublin) — 8.32 €" (usa preu_final()). Crea'n un amb títol "Dublin de Joyce", ciutat "Dublin", preu 8.00 i estoc 3, i imprimeix-ne la descripció.

Exercici 2: estendre, no sobreescriure

Afegeix a LlibreDigital un mètode descripcio() que reutilitzi la descripció de Llibre mitjançant super() i hi afegeixi el sufix " (baixada immediata)", en lloc de construir tot el text des de zero com hem fet més amunt. Comprova el resultat amb el Hamlet digital.

Exercici 3: auditoria de tipus

Donada la llista articles de la lliçó, escriu un fragment que compti quants articles són Llibre (incloent-hi els digitals) i quants són exactament llibres en paper (és a dir, Llibre però no LlibreDigital). Usa isinstance().

Solucions

Solució 1:

class Mapa(Producte):
    """Mapa d'una ciutat literaria."""

    def __init__(self, titol, ciutat, preu, estoc=0):
        super().__init__(titol, preu, estoc)
        self.ciutat = ciutat

    def descripcio(self):
        return f"[Mapa] {self.titol} ({self.ciutat}) — {self.preu_final():.2f} €"

dublin = Mapa("Dublin de Joyce", "Dublin", 8.00, 3)
print(dublin.descripcio())   # [Mapa] Dublin de Joyce (Dublin) — 8.32 €

Mapa només escriu el que li és propi (ciutat i el seu format de descripció); preu, estoc i la fórmula de l'IVA li arriben gratis des de Producte.

Solució 2:

class LlibreDigital(Llibre):
    def __init__(self, titol, autor, preu, format_fitxer="EPUB"):
        super().__init__(titol, autor, preu)
        self.format_fitxer = format_fitxer

    def hi_ha_estoc(self):
        return True

    def descripcio(self):
        return super().descripcio() + " (baixada immediata)"

hamlet_epub = LlibreDigital("Hamlet", "Shakespeare", 4.95)
print(hamlet_epub.descripcio())
# [Llibre] Hamlet (Shakespeare) — 5.15 € (baixada immediata)

Aquesta és la versió "estendre": si demà Llibre.descripcio() canvia de format, LlibreDigital s'actualitza sol. És el mateix principi DRY que va motivar les funcions al mòdul 3.

Solució 3:

llibres = [a for a in articles if isinstance(a, Llibre)]
nomes_paper = [a for a in articles if isinstance(a, Llibre) and not isinstance(a, LlibreDigital)]

print(f"Llibres (paper + digital): {len(llibres)}")      # 3
print(f"Nomes llibres en paper:    {len(nomes_paper)}")  # 2

isinstance(a, Llibre) accepta també els LlibreDigital perquè hereten de Llibre; per excloure'ls cal demanar-ho explícitament. Les comprensions de llistes de 02-04 continuen sent la manera més neta de filtrar.

Conclusió

L'herència ha convertit la classe solta de 05-01 en una família: Producte concentra títol, preu, estoc i la fórmula canònica de preu_final(); Llibre hi afegeix l'autor; Revista, el seu número; i LlibreDigital hereta de Llibre, aporta el format de fitxer i sobreescriu hi_ha_estoc() perquè allò digital no s'esgota. Has après a delegar la inicialització amb super().__init__(), a distingir heretar, estendre i sobreescriure, a preguntar per tipus amb isinstance()/issubclass(), a reconèixer quan la composició ("té un") és millor que l'herència ("és un"), i a llegir l'MRO que governa la cerca de mètodes. Però queda pendent l'observació més interessant de la lliçó: aquell bucle que recorria llibres, revistes i digitals cridant descripcio() sense preguntar mai què era cada cosa. Aquest poder — un mateix missatge, moltes respostes — es diu polimorfisme, i és el que fa que les jerarquies valguin la pena. És el tema de la propera lliçó.

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