El polimorfisme va tancar amb una advertència: res no impedeix escriure odissea.preu = -5 o faust.estoc = -3, i a partir d'aquí tots els càlculs de Papyrus menteixen. A 05-01 vas escriure vendre() precisament perquè l'estoc no quedés mai negatiu... però qualsevol pot saltar-se el mètode i tocar l'atribut directament. L'encapsulació és el principi que corregeix això: cada objecte protegeix les seves dades internes i només permet modificar-les per vies controlades, de manera que les seves invariants — les regles que sempre s'han de complir, com "l'estoc mai no és negatiu" — no es puguin trencar ni per accident. Python ho resol a la seva manera: sense cadenats de debò, amb convencions clares i una eina elegant anomenada property.

Contingut

  1. Què és encapsular i per què importa
  2. La convenció del guió baix: _protegit
  3. Doble guió baix i name mangling: __privat
  4. El problema dels getters/setters a l'estil Java
  5. @property: atributs calculats i validats
  6. El setter: validar sense canviar la interfície
  7. Producte blindat: la versió final
  8. Errors comuns i consells
  9. Exercicis amb solucions

Què és encapsular i per què importa

Encapsular és separar el què (la interfície pública: preu_final(), vendre()) del com (els detalls interns: en quin atribut es guarda l'estoc, com es valida). Els beneficis són concrets:

  • Invariants del negoci garantides: a Papyrus, l'estoc mai no és negatiu, el preu mai no és negatiu, un codi de soci sempre té el format NOM-NNN. Si l'objecte vigila les seves pròpies dades, cap part del programa no les pot corrompre.
  • Llibertat per canviar per dins: si demà l'estoc passa d'un enter a un desglossament per magatzem, la resta del codi no se n'assabenta mentre hi_ha_estoc() continuï responent igual.
  • Menys superfície d'error: qui usa la classe només veu el que ha d'usar.

Ara bé, una diferència cultural important: Python no té atributs privats de debò (com private en Java o C#). La seva filosofia es resumeix en la frase "tots som adults que hi consenten": el llenguatge senyala què és intern, i confia que ho respectis.

La convenció del guió baix: _protegit

Un guió baix inicial marca un atribut o mètode com a intern: "no el toquis des de fora; pot canviar sense avís previ". És només una convenció — Python no ho impedeix — però tota la comunitat la respecta.

class Soci:
    """Un soci del club de Papyrus."""

    def __init__(self, nom, codi):
        self.nom = nom
        self._codi = codi              # intern: no manipular des de fora
        self._compres = []             # intern: historial de compres

    def registrar_compra(self, quantia):
        self._compres.append(quantia)  # unica via legitima de modificar-lo

    def total_gastat(self):
        return round(sum(self._compres), 2)

lluis = Soci("Lluis", "LLUIS-001")
lluis.registrar_compra(12.35)
lluis._compres.append(-999)   # Python NO ho impedeix... pero esta mal vist

L'última línia funciona, i aquí hi ha la clau: _compres és un senyal de trànsit, no un mur. Els bons linters i revisors de codi el marcaran, i aquest contracte social és sorprenentment eficaç.

Doble guió baix i name mangling: __privat

Dos guions baixos inicials (sense dos de finals) activen el name mangling (recargolament de noms): Python rebateja l'atribut internament com a _NomClasse__atribut.

class Soci:
    def __init__(self, nom, codi):
        self.nom = nom
        self.__codi = codi

lluis = Soci("Lluis", "LLUIS-001")
print(lluis.__codi)           # AttributeError: 'Soci' object has no attribute '__codi'
print(lluis._Soci__codi)      # LLUIS-001  → continua sent accessible!

Punts que has de tenir clars:

  • No és seguretat real: qualsevol que conegui el truc hi accedeix amb _Soci__codi. És ocultació superficial, no xifratge ni protecció.
  • El seu propòsit genuí és evitar col·lisions de noms en herència: si Producte usa __comptador i una subclasse defineix un altre __comptador, el mangling els manté separats (_Producte__comptador vs _Llibre__comptador).
  • En el dia a dia, la comunitat Python prefereix el guió baix simple. Reserva __privat per a quan escriguis classes pensades per ser heretades per desconeguts.
Notació Nom real Significat Bloqueja l'accés?
preu preu Públic: usa'l amb llibertat No
_preu _preu Intern per convenció No (contracte social)
__preu _Classe__preu Intern + antixoc en herència No (només el disfressa)
__preu__ __preu__ Reservat per als mètodes màgics (05-05) No te n'inventis

El problema dels getters/setters a l'estil Java

En llenguatges amb privacitat estricta, la recepta clàssica és: atribut privat + mètode get + mètode set. Traslladada literalment a Python queda així:

class LlibreJavaner:
    def __init__(self, titol, preu):
        self._titol = titol
        self._preu = preu

    def get_preu(self):
        return self._preu

    def set_preu(self, valor):
        if valor < 0:
            raise ValueError("El preu no pot ser negatiu")
        self._preu = valor

llibre = LlibreJavaner("Hamlet", 9.95)
llibre.set_preu(10.50)          # verbos
print(llibre.get_preu())        # i poc natural en Python

Valida, sí, però al preu d'una interfície lletja i de trencar tot el codi existent que feia llibre.preu. Python té alguna cosa millor.

@property: atributs calculats i validats

Una property és un mètode disfressat d'atribut: es llegeix com llibre.preu (sense parèntesis), però per sota executa codi. És un decorador de la biblioteca estàndard (els decoradors en general s'estudien al mòdul 8; aquí n'hi ha prou de saber-lo usar).

class Llibre:
    def __init__(self, titol, preu, estoc=0):
        self.titol = titol
        self._preu = preu      # la dada real viu a l'atribut intern
        self._estoc = estoc

    @property
    def preu(self):
        """Preu base del llibre, en euros."""
        return self._preu

odissea = Llibre("L'Odissea", 12.50, 4)
print(odissea.preu)        # 12.50 — es LLEGEIX com a atribut, sense ()
odissea.preu = 13.00       # AttributeError: property 'preu' has no setter

Sense setter, la property crea un atribut de només lectura: ja ningú no pot assignar un preu per la cara. Les properties també serveixen per a atributs calculats que no s'emmagatzemen:

    @property
    def preu_amb_iva(self):
        return round(self._preu * 1.04, 2)

print(odissea.preu_amb_iva)   # 13.00 — calculat al vol, sempre coherent

El setter: validar sense canviar la interfície

Per permetre l'assignació amb control, s'afegeix el setter amb @nom.setter:

    @preu.setter
    def preu(self, valor):
        if valor < 0:
            raise ValueError(f"No es permet un preu negatiu: {valor}")
        self._preu = valor

Ara la validació és automàtica i la sintaxi continua sent natural:

odissea.preu = 13.50     # OK: passa pel setter i valida
odissea.preu = -5        # ValueError: No es permet un preu negatiu: -5

raise ValueError(...) atura el programa amb un missatge clar; ja el vas usar conceptualment en validar entrades, i el mòdul 7 ensenyarà a capturar aquests errors amb try/except. L'important avui: l'objecte rebutja la dada invàlida en el moment exacte en què intenta entrar.

Estil Java en Python Estil pythònic (@property)
Llegir llibre.get_preu() llibre.preu
Escriure llibre.set_preu(13.5) llibre.preu = 13.5
Validació Sí, a set_preu Sí, al setter
Trenca el codi que usava l'atribut? Sí, cal reescriure'l No: la mateixa sintaxi de sempre
Només lectura Ometre set_preu (però _preu continua a la vista) Property sense setter
Veredicte Evitar Preferir

L'última fila de "trenca el codi?" amaga el gran avantatge estratègic: pots començar amb un atribut públic normal (self.preu) i convertir-lo en property anys després, quan calgui validar, sense tocar ni una línia del codi client. Per això en Python no s'escriuen getters/setters preventius "per si de cas", com exigeix l'ortodòxia Java.

Producte blindat: la versió final

Apliquem-ho tot a la classe base de la jerarquia (05-02). Invariants a protegir: preu ≥ 0, estoc enter ≥ 0.

class Producte:
    """Article de Papyrus amb invariants protegides."""

    IVA_LLIBRES = 0.04
    DESCOMPTE_SOCI = 0.05

    def __init__(self, titol, preu, estoc=0):
        self.titol = titol
        self.preu = preu     # passa pel setter fins i tot des de __init__!
        self.estoc = estoc   # igual: valida des del primer moment

    @property
    def preu(self):
        return self._preu

    @preu.setter
    def preu(self, valor):
        if valor < 0:
            raise ValueError(f"No es permet un preu negatiu: {valor}")
        self._preu = valor

    @property
    def estoc(self):
        return self._estoc

    @estoc.setter
    def estoc(self, valor):
        if not isinstance(valor, int):
            raise ValueError(f"L'estoc ha de ser un enter, no {type(valor).__name__}")
        if valor < 0:
            raise ValueError(f"No es permet un estoc negatiu: {valor}")
        self._estoc = valor

    def vendre(self, unitats=1):
        """Unica via recomanada per descomptar estoc."""
        if unitats > self._estoc:
            return False
        self._estoc -= unitats
        return True

    def preu_final(self, soci=False):
        descompte = Producte.DESCOMPTE_SOCI if soci else 0
        return round(self._preu * (1 - descompte) * (1 + Producte.IVA_LLIBRES), 2)

Dues subtileses d'or en aquest codi:

  • __init__ assigna a self.preu, no a self._preu: així la validació s'aplica també en construir. Producte("Fantasma", -3, 1) falla immediatament, que és on ha de fallar.
  • Les subclasses hereten el blindatge gratis: Llibre, Revista i LlibreDigital no canvien ni una línia i els seus preus i estocs queden protegits, perquè els seus super().__init__(...) desemboquen en aquests setters.
faust = Llibre("Faust", "Goethe", 21.00, 0)
faust.estoc = -3        # ValueError: No es permet un estoc negatiu: -3
faust.estoc = 2.5       # ValueError: L'estoc ha de ser un enter, no float
faust.estoc = 5         # OK: han arribat exemplars — atents, els de la cua de reserves
faust.vendre(2)         # True
print(faust.estoc)      # 3

Errors Comuns i Consells

  • Recursió infinita a la property: escriure return self.preu dins del getter de preu (o self.preu = valor al seu setter) crida la property una vegada i una altra fins a RecursionError. Dins del getter/setter, usa sempre l'atribut intern self._preu.
  • Validar al setter però assignar self._preu a __init__: l'objecte neix sense validar. Assigna a __init__ al nom públic (self.preu = preu) perquè el setter actuï des del primer segon.
  • Creure que __privat protegeix dades sensibles: el name mangling se salta amb obj._Classe__atribut; no és un mecanisme de seguretat, només d'higiene de noms en herència.
  • Escriure getters/setters get_x()/set_x() preventius per a tot: és soroll heretat de Java. Atribut públic primer; property quan (i només quan) necessitis validar o calcular.
  • Oblidar el @property a sobre del getter i deixar només @preu.setter: NameError, perquè el setter es defineix a partir de la property existent. L'ordre és: primer @property, després @nom.setter.
  • Consell: decideix què és interfície pública (allò que documentes: titol, preu, estoc, vendre(), preu_final()) i marca la resta amb _. Una classe amb poques portes d'entrada és una classe fàcil de raonar i de mantenir.

Exercicis

Exercici 1: títol de només lectura

A Papyrus, el títol d'un producte no ha de canviar un cop creat (és la seva identitat al catàleg). Converteix titol en una property sense setter que a més validi a __init__ (a través d'un mètode intern o del mateix desat) que no sigui una cadena buida després de strip(). Comprova que odissea.titol = "Un altre" falla i que Producte(" ", 5.0) llança ValueError.

Exercici 2: el codi de soci blindat

Crea la classe Soci amb nom públic i una property codi amb getter i setter. El setter ha de validar el format NOM-NNN de Papyrus: la part esquerra alfabètica i en majúscules, un guió, i tres dígits (repassa split(), isalpha(), isdigit(), isupper() de 04-05). "LLUIS-001" i "MARTA-002" han de passar; "lluis-001", "LLUIS001" i "LLUIS-1" han de llançar ValueError.

Exercici 3: reserves encapsulades

Afegeix a Producte un atribut intern _reserves (llista) i els mètodes reservar(client) i atendre_reserva(). reservar només es permet si NO hi ha estoc (si n'hi ha, que retorni l'avís "Hi ha estoc: compra directa"); atendre_reserva treu el primer client de la llista (ordre d'arribada) o retorna None si no hi ha reserves. Simula: Faust amb estoc 0, reserven la Júlia i l'Omar, arriba estoc, s'atén la Júlia.

Solucions

Solució 1:

class Producte:
    def __init__(self, titol, preu, estoc=0):
        titol = titol.strip()
        if not titol:                       # truthiness (02-01): "" es fals
            raise ValueError("El titol no pot estar buit")
        self._titol = titol
        self.preu = preu
        self.estoc = estoc

    @property
    def titol(self):
        return self._titol
    # ... la resta de properties, com abans ...

odissea = Producte("L'Odissea", 12.50, 4)
odissea.titol = "Un altre"    # AttributeError: property 'titol' has no setter
Producte("   ", 5.0)          # ValueError: El titol no pot estar buit

Sense setter, la property és de només lectura: la identitat del producte queda segellada en néixer.

Solució 2:

class Soci:
    def __init__(self, nom, codi):
        self.nom = nom
        self.codi = codi              # passa pel setter

    @property
    def codi(self):
        return self._codi

    @codi.setter
    def codi(self, valor):
        parts = valor.split("-")
        valid = (
            len(parts) == 2
            and parts[0].isalpha() and parts[0].isupper()
            and len(parts[1]) == 3 and parts[1].isdigit()
        )
        if not valid:
            raise ValueError(f"Codi de soci incorrecte: {valor!r} (format NOM-NNN)")
        self._codi = valor

Soci("Lluis", "LLUIS-001")    # OK
Soci("Lluis", "lluis-001")    # ValueError (minuscules)
Soci("Lluis", "LLUIS-1")      # ValueError (falten digits)

És la mateixa regla que es_soci_valid() de papyrus_utils.py, però ara viu on ha de viure: a l'objecte que guarda la dada, impossible d'oblidar.

Solució 3:

class Producte:
    def __init__(self, titol, preu, estoc=0):
        self.titol = titol
        self.preu = preu
        self.estoc = estoc
        self._reserves = []           # intern: nomes es toca via metodes

    def reservar(self, client):
        if self.estoc > 0:
            return "Hi ha estoc: compra directa"
        self._reserves.append(client)
        return f"Reserva registrada per a {client} ({len(self._reserves)} a la cua)"

    def atendre_reserva(self):
        if self._reserves:
            return self._reserves.pop(0)   # el primer d'arribar, el primer servit
        return None

faust = Producte("Faust", 21.00, 0)
print(faust.reservar("Julia"))    # Reserva registrada per a Julia (1 a la cua)
print(faust.reservar("Omar"))     # Reserva registrada per a Omar (2 a la cua)
faust.estoc = 3                   # arriben exemplars (validat pel setter)
print(faust.atendre_reserva())    # Julia

La llista _reserves no es manipula mai des de fora: l'ordre d'arribada queda garantit per construcció. (Per a cues llargues, recorda que la deque de 04-06 fa popleft() en temps constant; aquí pop(0) és suficient.)

Conclusió

L'encapsulació ha tancat l'esquerda que el polimorfisme va deixar a la vista: _intern senyala allò que no s'ha de tocar, __privat afegeix el name mangling (higiene de noms, mai seguretat real), i @property amb el seu @setter aconsegueix el millor de dos mons — la sintaxi natural d'un atribut amb la validació d'un mètode. La classe Producte de Papyrus ja no admet preus negatius ni estocs impossibles, i totes les seves subclasses hereten el blindatge sense canviar ni una línia. Amb dades unides al seu comportament (05-01), organitzades en jerarquia (05-02), polimòrfiques (05-03) i protegides (05-04), només falta que els teus objectes parlin l'idioma natiu de Python: que print(odissea) mostri alguna cosa digna, que sorted(cataleg) ordeni llibres sense key=, que llibre in cistell simplement funcioni. Tot això ho concedeixen els mètodes màgics, i amb ells saldarem per fi el ganxo pendent des de 05-01: el print "lleig". És 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