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

  1. Un missatge, moltes respostes
  2. El terminal de venda de Papyrus: recórrer sense preguntar
  3. Duck typing: "si camina com un ànec..."
  4. Polimorfisme a les funcions integrades
  5. Sobreescriptura vs sobrecàrrega (i per què Python no té la segona)
  6. El tuf de codi: cadenes d'isinstance
  7. Errors comuns i consells
  8. 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 a hi_ha_estoc(), preu_final(), descripcio() i tingui titol.
  • "Faust" queda fora perquè la seva pròpia versió de hi_ha_estoc() (l'heretada de Producte, amb estoc 0) retorna False; el Hamlet digital hi entra perquè la seva versió sobreescrita sempre retorna True.
  • 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) o Lamina(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 iterable

len() 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.singledispatch per 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.titol
# BE: cada classe ja sap descriure's
def descriure_be(article):
    return article.descripcio()

Problemes 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 Llibre abans que LlibreDigital, els digitals cauen a la branca equivocada (perquè un LlibreDigital és un Llibre). É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 defineix preu_final(self) a seques, el terminal fallarà amb TypeError justament 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 un ValRegal t'ho agrairà.
  • Excés de defensa: comprovar isinstance de 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 Mapa sense editar cobrar()? Si la resposta és que no, hi ha un isinstance amagat 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.95

Exercici 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 funcio

El 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

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