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
- El problema: tres articles, un 80 % de codi comú
- Classe base i classe derivada
super().__init__(): reutilitzar la inicialització del pare- Estendre vs sobreescriure mètodes
- La jerarquia completa de Papyrus
isinstance()iissubclass()- Quan NO heretar: la composició com a alternativa
- L'MRO: en quin ordre cerca Python
- Errors comuns i consells
- 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 seuRevista 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 tocarsuper().__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:
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 fillaPrefereix 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 unProducte? 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últiple — class 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 ambAttributeErroren 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()retornaboolaProducte, la versió deLlibreDigitaltambé ha de retornarbool. Qui usa unProductequalsevol confia en aquest comportament (ho veurem formalitzat com a polimorfisme a 05-03). - Comparar tipus amb
type(x) is Llibrequan vols incloure-hi derivades: unLlibreDigitalquedaria 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)}") # 2isinstance(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
- 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
