La lliçó anterior va acabar amb una escena aparentment trivial: un bucle recorria llibres, revistes i llibres digitals cridant descripcio() i hi_ha_estoc(), i cada objecte responia a la seva manera sense que el bucle preguntés mai "i tu què ets?". Aquest fenomen es diu polimorfisme ("moltes formes"): un mateix missatge produeix respostes diferents segons qui el rep. És la recompensa real de construir jerarquies, i en Python va encara més enllà gràcies al duck typing. En aquesta lliçó aprendràs a escriure codi que funciona amb tipus que encara ni existeixen — com el terminal de venda de Papyrus, que cobrarà mapes i làmines el dia que l'Anna decideixi vendre'n, sense canviar ni una línia.
Contingut
- Un missatge, moltes respostes
- El terminal de venda de Papyrus: recórrer sense preguntar
- Duck typing: "si camina com un ànec..."
- Polimorfisme a les funcions integrades
- Sobreescriptura vs sobrecàrrega (i per què Python no té la segona)
- El tuf de codi: cadenes d'
isinstance - Errors comuns i consells
- Exercicis amb solucions
Un missatge, moltes respostes
Partim de la jerarquia de 05-02: Producte com a base, amb Llibre, Revista i LlibreDigital (que hereta de Llibre). Les tres derivades sobreescriuen descripcio(), i LlibreDigital sobreescriu a més hi_ha_estoc().
odissea = Llibre("L'Odissea", "Homer", 12.50, 4)
quimera = Revista("Quimera", 482, 6.50, 10)
hamlet_epub = LlibreDigital("Hamlet", "Shakespeare", 4.95, "EPUB")
for article in (odissea, quimera, hamlet_epub):
print(article.descripcio())[Llibre] L'Odissea (Homer) — 13.00 € [Revista n. 482] Quimera — 6.76 € [Digital EPUB] Hamlet (Shakespeare) — 5.15 €
La crida és idèntica (article.descripcio()), però Python resol en temps d'execució quina versió executar seguint l'MRO de cada objecte (05-02): comença a la classe real de l'objecte i puja. El mateix missatge, tres formes.
El terminal de venda de Papyrus: recórrer sense preguntar
Vegem el polimorfisme fent feina útil. El Lluís (soci LLUIS-001) s'acosta al taulell amb una cistella variada, i el terminal de l'Anna cobra sense distingir tipus:
def cobrar(cistella, soci=False):
"""Imprimeix el tiquet i retorna el total d'una cistella de Productes."""
total = 0
for article in cistella:
if not article.hi_ha_estoc():
print(f" (sense estoc: {article.titol} passa a reserves)")
continue
quantia = article.preu_final(soci) # cada classe calcula el que li toca
total += quantia
print(f" {article.descripcio()}")
return round(total, 2)
cistella_lluis = [
Llibre("L'Odissea", "Homer", 12.50, 4),
Llibre("Faust", "Goethe", 21.00, 0), # esgotat des del modul 4
Revista("Quimera", 482, 6.50, 10),
LlibreDigital("Hamlet", "Shakespeare", 4.95),
]
print(f"Total (soci): {cobrar(cistella_lluis, soci=True):.2f} €")[Llibre] L'Odissea (Homer) — 13.00 € (sense estoc: Faust passa a reserves) [Revista n. 482] Quimera — 6.76 € [Digital EPUB] Hamlet (Shakespeare) — 5.15 € Total (soci): 23.66 €
Detalls que fan polimòrfica aquesta funció:
cobrar()no esmenta cap classe concreta: només exigeix que cada element respongui ahi_ha_estoc(),preu_final(),descripcio()i tinguititol.- "Faust" queda fora perquè la seva pròpia versió de
hi_ha_estoc()(l'heretada deProducte, amb estoc 0) retornaFalse; el Hamlet digital hi entra perquè la seva versió sobreescrita sempre retornaTrue. - El preu de soci de "L'Odissea" (12.35 €), Quimera (6.42 €) i l'EPUB (4.89 €) suma 23.66 € — cada objecte ha aplicat la fórmula canònica sobre el seu propi preu base.
- Si el mes que ve l'Anna crea
Mapa(Producte)oLamina(Producte),cobrar()els acceptarà sense modificar-se. Això es coneix com a codi obert a extensió, tancat a modificació.
flowchart LR
A["cobrar(cistella)"] -->|"article.descripcio()"| B{"Quina classe és<br>l'objecte real?"}
B -->|Llibre| C["'[Llibre] L'Odissea (Homer)...'"]
B -->|Revista| D["'[Revista n. 482] Quimera...'"]
B -->|LlibreDigital| E["'[Digital EPUB] Hamlet...'"]
Duck typing: "si camina com un ànec..."
En molts llenguatges, el polimorfisme exigeix herència: només pots passar a cobrar() objectes declarats com a Producte. Python és més pragmàtic. La seva filosofia és el duck typing (tipatge d'ànec): "si camina com un ànec i fa nyec-nyec com un ànec, és un ànec". No importa de quina classe descendeix un objecte; importa a quins mètodes respon.
La Marta proposa vendre vals regal, i ni tan sols els fa heretar de Producte:
class ValRegal:
"""No hereta de Producte, pero fa nyec-nyec igualment."""
def __init__(self, quantia):
self.titol = f"Val regal de {quantia:.2f} €"
self.quantia = quantia
def hi_ha_estoc(self):
return True # un val sempre es pot emetre
def preu_final(self, soci=False):
return round(self.quantia, 2) # sense IVA ni descompte: val el que val
def descripcio(self):
return f"[Val] {self.titol}"
cistella = [Llibre("El Quixot", "Cervantes", 15.90, 8), ValRegal(20)]
print(f"Total: {cobrar(cistella):.2f} €") # Total: 36.54 € (16.54 + 20.00)cobrar() l'accepta sense piular: té els quatre "nyecs" que la funció utilitza. Això és polimorfisme sense herència.
| Polimorfisme per herència | Duck typing | |
|---|---|---|
| Requisit | Descendir d'una base comuna | Respondre als mètodes usats |
| Verificació | isinstance(x, Producte) possible |
Només en executar: si falta el mètode, AttributeError |
| Estil típic de | Java, C# | Python |
| A Papyrus | Llibre, Revista, LlibreDigital |
ValRegal |
Quan convé cadascun? L'herència aporta codi compartit i documentació de la relació; el duck typing aporta flexibilitat màxima. A la pràctica, Python combina totes dues coses: jerarquia per a allò que comparteix implementació, duck typing per als convidats ocasionals. (Al mòdul 8, els type hints amb protocols posaran nom formal a aquests "nyecs".)
Polimorfisme a les funcions integrades
Fa temps que uses polimorfisme sense saber-ho, des del mòdul 1. Les funcions integrades de Python són polimòrfiques per disseny:
len("Papyrus") # 7 — caracters d'una cadena
len(["L'Odissea", "Hamlet"]) # 2 — elements d'una llista
len({"LLUIS-001", "MARTA-002"}) # 2 — elements d'un conjunt
len(cataleg) # 4 — claus d'un diccionari
sum([12.50, 9.95, 15.90]) # 38.35 — suma llistes...
sum((4, 6, 8, 0)) # 18 — ...i tuples, i qualsevol iterablelen() no pregunta el tipus: envia el mateix missatge ("quant mesures?") i cada tipus respon a la seva manera. El bucle for, sorted(), min()/max() amb key= (03-03) funcionen igual: accepten qualsevol objecte que sàpiga comportar-se com a iterable. A 05-05 descobriràs el mecanisme exacte (mètodes màgics com __len__) i faràs que les teves pròpies classes responguin a len() i companyia.
Sobreescriptura vs sobrecàrrega (i per què Python no té la segona)
Dos termes que es confonen amb facilitat:
| Concepte | Què significa | Existeix en Python? |
|---|---|---|
| Sobreescriptura (overriding) | La classe derivada redefineix un mètode del pare | Sí — LlibreDigital.hi_ha_estoc() |
| Sobrecàrrega (overloading) | Diverses versions del mateix mètode amb signatures diferents, a la mateixa classe | No en el sentit clàssic |
En Java podries declarar preuFinal() i preuFinal(boolean soci) com dos mètodes diferents. En Python, si defineixes dues vegades preu_final en una classe, la segona definició reemplaça la primera silenciosament. L'equivalent pythònic de la sobrecàrrega són els recursos del mòdul 3:
def preu_final(self, soci=False, unitats=1):
"""Una sola signatura cobreix tots els casos d'us."""
descompte = Producte.DESCOMPTE_SOCI if soci else 0
brut = self.preu * unitats * (1 - descompte) * (1 + Producte.IVA_LLIBRES)
return round(brut, 2)
odissea.preu_final() # 13.00 — sense arguments
odissea.preu_final(soci=True) # 12.35 — "una altra signatura"
odissea.preu_final(soci=True, unitats=2) # 24.70 — "una altra mes"- Els valors per defecte (
soci=False,unitats=1) cobreixen les variants habituals. - Per a casos extrems,
*args/**kwargs(03-02) permeten signatures totalment flexibles. - Existeix
functools.singledispatchper despatxar segons el tipus d'argument, però és una eina de nínxol: els valors per defecte resolen el 95 % dels casos.
El tuf de codi: cadenes d'isinstance
isinstance() (05-02) és legítim per validar entrades o filtrar col·leccions. Però quan s'usa per decidir comportament, delata un disseny que ignora el polimorfisme. Compara:
# MALAMENT: el terminal pregunta el tipus a cada article
def descriure_malament(article):
if isinstance(article, LlibreDigital):
return f"[Digital {article.format_fitxer}] {article.titol}"
elif isinstance(article, Llibre):
return f"[Llibre] {article.titol} ({article.autor})"
elif isinstance(article, Revista):
return f"[Revista n. {article.numero}] {article.titol}"
else:
return article.titolProblemes de la versió "malament":
- Cada classe nova obliga a editar la funció (i totes les funcions semblants escampades pel programa). Amb polimorfisme, la classe nova porta el comportament posat.
- L'ordre importa i traeix: si comproves
Llibreabans queLlibreDigital, els digitals cauen a la branca equivocada (perquè unLlibreDigitalés unLlibre). És un error real i silenciós. - Duplica coneixement: el format de descripció viu lluny de la classe que descriu.
Regla pràctica: si escrius isinstance dins d'un if/elif per variar comportament, pregunta't si aquest comportament no hauria de ser un mètode sobreescrit a cada classe. Gairebé sempre la resposta és que sí.
Errors Comuns i Consells
- Ordenar malament les branques d'
isinstance: la classe derivada s'ha de comprovar abans que la seva base, o la seva branca no s'assolirà mai. Millor encara: elimina la cadena amb un mètode polimòrfic. - Sobreescriure canviant la signatura: si
Producte.preu_final(self, soci=False)accepta un argument, la versió d'una derivada també l'ha d'acceptar. Si la derivada defineixpreu_final(self)a seques, el terminal fallarà ambTypeErrorjustament amb aquest tipus — trencant la promesa que "qualsevol Producte serveix". - Definir dos mètodes amb el mateix nom en una classe esperant sobrecàrrega: Python es queda només amb l'últim, sense avisar. Usa valors per defecte.
- Confiar en el duck typing sense documentar els "nyecs": si
cobrar()exigeix quatre mètodes, digues-ho al seu docstring (03-01). Qui escrigui unValRegalt'ho agrairà. - Excés de defensa: comprovar
isinstancede tot, a tot arreu, "per si de cas". Python afavoreix l'estil EAFP (és més fàcil demanar perdó que permís), que desenvoluparem amb les excepcions al mòdul 7. - Consell: quan dubtis de si el teu disseny és polimòrfic, fes la prova de l'article nou: pots afegir
Mapasense editarcobrar()? Si la resposta és que no, hi ha unisinstanceamagat per eliminar.
Exercicis
Exercici 1: un ànec nou a la cistella
Crea la classe Lamina (una làmina il·lustrada) sense heretar de Producte: amb titol, preu, i els mètodes hi_ha_estoc() (sempre True), preu_final(soci=False) (fórmula canònica de Papyrus) i descripcio() ("[Lamina] ..."). Comprova que cobrar([Lamina("El corb", 5.00)]) funciona per pur duck typing.
Exercici 2: eliminar el tuf de codi
Refactoritza aquesta funció heretada de l'antic terminal perquè usi polimorfisme (et caldrà tocar també les classes: pista — l'enviament és comportament de cada classe):
def cost_enviament(article):
if isinstance(article, LlibreDigital):
return 0.0
elif isinstance(article, Revista):
return 1.50
elif isinstance(article, Llibre):
return 2.95Exercici 3: inventari polimòrfic
Escriu valor_inventari(articles) que retorni la suma de preu_final() × estoc dels articles físics (els que tenen l'atribut estoc més gran que 0), recorrent la llista una sola vegada i sense usar isinstance. Prova-la amb "L'Odissea" (estoc 4), "El Quixot" (estoc 8) i un LlibreDigital (que ha d'aportar 0).
Solucions
Solució 1:
class Lamina:
IVA_LLIBRES = 0.04
DESCOMPTE_SOCI = 0.05
def __init__(self, titol, preu):
self.titol = titol
self.preu = preu
def hi_ha_estoc(self):
return True
def preu_final(self, soci=False):
descompte = Lamina.DESCOMPTE_SOCI if soci else 0
return round(self.preu * (1 - descompte) * (1 + Lamina.IVA_LLIBRES), 2)
def descripcio(self):
return f"[Lamina] {self.titol} — {self.preu_final():.2f} €"
print(f"Total: {cobrar([Lamina('El corb', 5.00)]):.2f} €") # Total: 5.20 €cobrar() no comprova herència: Lamina fa nyec-nyec (respon als quatre mètodes), per tant és un ànec.
Solució 2:
class Producte:
# ... tot l'anterior ...
def cost_enviament(self):
return 2.95 # valor per defecte raonable per als articles en paper
class Revista(Producte):
# ... tot l'anterior ...
def cost_enviament(self):
return 1.50 # sobreescriu: les revistes van en sobre
class LlibreDigital(Llibre):
# ... tot l'anterior ...
def cost_enviament(self):
return 0.0 # sobreescriu: res a enviar
def cost_enviament(article):
return article.cost_enviament() # o directament eliminar la funcioEl coneixement de l'enviament ara viu a cada classe. Afegir Mapa amb enviament en tub (3.50 €) ja no toca cap funció existent. Fixa't que Llibre ni tan sols hi apareix: hereta el 2.95 de Producte.
Solució 3:
def valor_inventari(articles):
"""Valor a preu de venda (sense soci) de l'estoc fisic."""
total = sum(a.preu_final() * a.estoc for a in articles)
return round(total, 2)
inventari = [
Llibre("L'Odissea", "Homer", 12.50, 4),
Llibre("El Quixot", "Cervantes", 15.90, 8),
LlibreDigital("Hamlet", "Shakespeare", 4.95),
]
print(f"{valor_inventari(inventari):,.2f} €") # 184.32 €sum() amb una expressió generadora recorre la llista una sola vegada. El LlibreDigital no necessita tracte especial: el seu estoc heretat val 0, així que multiplica per 0 i no aporta res — la mateixa dada és polimòrfica. (13.00 × 4 + 16.54 × 8 = 184.32 €.)
Conclusió
El polimorfisme és la raó de ser de la jerarquia que vas construir a 05-02: cobrar() envia els mateixos missatges — hi_ha_estoc(), preu_final(), descripcio() — i cada objecte respon segons la seva classe real, sense ni un sol if de tipus. Has vist que Python estén la idea amb el duck typing (el ValRegal s'hi cola sense heretar de ningú), que les funcions integrades com len() i sum() són polimòrfiques de sèrie, que la "sobrecàrrega" clàssica se substitueix amb valors per defecte, i que les cadenes d'isinstance són el tuf de codi que delata un disseny rígid. Ara bé: tot aquest edifici s'aguanta sobre una confiança delicada — res no impedeix que algú escrigui odissea.preu = -5 o faust.estoc = -3 i faci mentir els comptes de l'Anna. Protegir les dades d'un objecte perquè només canviïn per les vies legítimes és l'encapsulació, 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
