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
- Què és encapsular i per què importa
- La convenció del guió baix:
_protegit - Doble guió baix i name mangling:
__privat - El problema dels getters/setters a l'estil Java
@property: atributs calculats i validats- El setter: validar sense canviar la interfície
Producteblindat: la versió final- Errors comuns i consells
- 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 vistL'ú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
Producteusa__comptadori una subclasse defineix un altre__comptador, el mangling els manté separats (_Producte__comptadorvs_Llibre__comptador). - En el dia a dia, la comunitat Python prefereix el guió baix simple. Reserva
__privatper 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 PythonValida, 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 setterSense 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 coherentEl 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 = valorAra 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: -5raise 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 aself.preu, no aself._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,RevistaiLlibreDigitalno canvien ni una línia i els seus preus i estocs queden protegits, perquè els seussuper().__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) # 3Errors Comuns i Consells
- Recursió infinita a la property: escriure
return self.preudins del getter depreu(oself.preu = valoral seu setter) crida la property una vegada i una altra fins aRecursionError. Dins del getter/setter, usa sempre l'atribut internself._preu. - Validar al setter però assignar
self._preua__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
__privatprotegeix dades sensibles: el name mangling se salta ambobj._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
@propertya 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 buitSense 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()) # JuliaLa 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
- 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
