Amb el DEFINICIO.md de Definició del projecte i el DISSENY.md de Planificació i disseny sobre la taula, comença la part que esperaves: escriure el programa. I aquí és on es perden la majoria dels primers projectes, no per falta de coneixements —els tens tots— sinó per mètode. Qui comença sol obrir els cinc fitxers alhora, escriure quatre-centes línies sense executar res i trobar-se amb trenta errors encadenats que ja no sap per on agafar. Aquesta lliçó ensenya el contrari: construir en increments petits, amb el programa sempre executable, provant i fent commit a cada pas, i amb un procediment concret per als dos moments difícils —quan alguna cosa falla i no saps per què, i quan et bloqueges i no saps continuar—.
Contingut
- L'estratègia: incremental i sempre executable
- Ordre de construcció
- Fase 1: l'esquelet que arrenca
- Fase 2: el model i les seves proves
- Fase 3: el magatzem i la seva prova d'anada i tornada
- Fase 4: la lògica de la col·lecció
- Fase 5: la interfície i la seva prova manual
- Quan alguna cosa no funciona: depurar de debò
- Gestió dels errors de l'usuari
- Commits durant el desenvolupament
- Quan et bloqueges
- Checklist d'«acabat»
- Errors comuns i consells
- Exercicis
- Conclusió
- L'estratègia: incremental i sempre executable
La regla és una sola frase: el programa ha de poder executar-se en tot moment, des del minut u —quan només mostra un menú buit— fins a la v1.0. Mai no hi ha un estat intermedi de «està a mitges, no arrenca». Compara les dues maneres de treballar. En l'enfocament «tot alhora» escrius els cinc mòduls abans d'executar, la primera arrencada treu trenta errors encadenats sense que sàpigues quin causa quin, el commit és gegant i no hi ha res per ensenyar fins al final. En l'enfocament incremental escrius el mínim i executes, cada error apareix sol i sobre el codi que acabes de tocar, hi ha un commit per increment i alguna cosa que funciona des del primer dia.
La diferència de fons és al segon punt. Quan una fallada apareix just després d'escriure deu línies, el sospitós són aquestes deu línies. Quan apareix després de quatre-centes, el sospitós és tot el programa, i depurar passa de cinc minuts a dues hores.
El cicle de treball, doncs, és aquest bucle curt:
flowchart LR
E["Escriure<br/>un increment petit"] --> R["Executar<br/>python -m lesmevesdespeses"]
R --> P["Provar<br/>pytest"]
P --> C["Commit<br/>missatge descriptiu"]
C --> E
R -->|falla| D["Depurar"]
D --> R
P -->|falla| D
Una volta completa d'aquest bucle hauria de durar entre quinze minuts i una hora: si portes tres hores sense tornar a la casella «Executar», l'increment era massa gran.
- Ordre de construcció
L'ordre no és indiferent: es construeix de dins cap a fora, perquè cada capa depèn de l'anterior i perquè les capes interiors són les que es poden provar automàticament.
| Fase | Què construeixes | Com ho comproves | Est. |
|---|---|---|---|
| 1. Esquelet | __main__.py i interficie.py amb el menú i la sortida |
Arrenca i surt | 1 h |
| 2. Model | model.py: entitat, validació, excepció |
pytest tests/test_model.py |
2 h |
| 3. Magatzem | magatzem.py: desar i carregar |
pytest amb tmp_path |
2 h |
| 4. Lògica | quadern.py: afegir, cercar, filtrar, totals |
pytest tests/test_quadern.py |
3 h |
| 5. Interfície | Les opcions del menú, una a una | Guió de prova manual | 6 h |
| 6. Extres | Should, robustesa, logging, format |
Guió + revisió final | 4 h |
Dos advertiments sobre aquest ordre. El primer: encara que la interfície sigui l'últim de la llista, la fita de la primera funcionalitat completa de cap a cap arriba abans d'acabar totes les capes. Tan bon punt tinguis model, magatzem i un afegir a la lògica, fes l'opció «registrar» del menú sencera; veure el programa fent alguna cosa real canvia la motivació més que qualsevol altra cosa. I el segon: les fases 2, 3 i 4 es proven automàticament; la 5, no. Provar una interfície de línia d'ordres automàticament és possible però costós, i no és el que correspon a un primer projecte: per a ella es fa servir el guió manual de l'apartat 7.
- Fase 1: l'esquelet que arrenca
El primer objectiu és que python -m lesmevesdespeses funcioni. Res més.
# lesmevesdespeses/interficie.py -- capa de presentacio
OPCIONS = {"1": "Registrar despesa", "2": "Registrar ingres", "3": "Moviments del mes",
"4": "Resum per categoria", "5": "Esborrar moviment", "0": "Sortir"}
def executar() -> None:
"""Bucle principal de l'aplicacio."""
while True:
print("\n=== LesMevesDespeses ===")
for clau, text in OPCIONS.items():
print(f" {clau}. {text}")
opcio = input("Opcio: ").strip()
if opcio == "0":
print("Fins aviat.")
break
print(f"[pendent] {OPCIONS[opcio]}" if opcio in OPCIONS else "Opcio no valida.")
# lesmevesdespeses/__main__.py -> from .interficie import executar; executar()Són vint línies i cap no fa res útil encara, però resolen de cop els tres problemes que més bloquegen al principi: que el paquet s'importi bé, que l'execució amb -m funcioni i que existeixi el bucle principal de Menús interactius. El [pendent] de cada opció és deliberat: el menú complet existeix des del primer dia i les opcions es van omplint una a una. Commit: Esquelet: menu principal que arrenca i surt.
- Fase 2: el model i les seves proves
Ara l'entitat, amb tota la seva validació a dins. És la peça més important del programa: si el model garanteix que no pot existir un moviment invàlid, cap altra capa no s'ha de preocupar de comprovar-ho.
# lesmevesdespeses/model.py -- entitats del domini i les seves regles de validacio
CATEGORIES = ("menjar", "transport", "oci", "llar", "salut", "ingressos", "altres")
class MovimentInvalid(ValueError):
"""Es llanca quan les dades d'un moviment no compleixen les regles."""
@dataclass
class Moviment:
"""Un ingres o una despesa. El signe de la quantitat distingeix l'un de l'altra."""
data: date
concepte: str
quantitat: float
categoria: str
id: int = 0
def __post_init__(self) -> None:
self.concepte = self.concepte.strip() # normalitzar...
self.categoria = self.categoria.strip().lower()
if not self.concepte or len(self.concepte) > 60: # ...i despres validar
raise MovimentInvalid("El concepte ha de tenir entre 1 i 60 caracters.")
if self.quantitat == 0:
raise MovimentInvalid("L'import no pot ser zero.")
if self.categoria not in CATEGORIES:
raise MovimentInvalid(f"Categoria desconeguda: {self.categoria!r}.")
if self.data > date.today():
raise MovimentInvalid("La data no pot ser futura.")
def es_despesa(self) -> bool:
return self.quantitat < 0
def __str__(self) -> str: # linia ja formatada de taula
return f"{self.id:>4} {self.data} {self.concepte:<30} {self.quantitat:>10.2f}"Les decisions que hi ha aquí, una a una. @dataclass (07-02) evita escriure un __init__ que només assignaria cinc atributs. __post_init__ és el mètode que dataclass crida just després de construir, i és on va la validació. Es normalitza abans de validar —strip() i lower()— perquè « Menjar » i «menjar» siguin el mateix, que era un dels casos límit previstos a 09-01. L'excepció pròpia hereta de ValueError perquè és un error de valor: qui no conegui MovimentInvalid pot capturar ValueError i funcionarà igual. I __str__ retorna ja la línia formatada de taula, amb les amplades fixes de l'esbós de pantalla.
Les proves, amb el patró AAA de Proves automatitzades:
# tests/test_model.py
def test_normalitza_concepte_i_categoria():
m = Moviment(date(2026, 8, 1), " Cafe ", -1.5, " MENJAR ")
assert m.concepte == "Cafe" and m.categoria == "menjar" and m.es_despesa()
@pytest.mark.parametrize("concepte, quantitat, categoria", [
("", -10.0, "menjar"), # concepte buit
("Compra", 0.0, "menjar"), # quantitat zero
("Compra", -10.0, "viatges"), # categoria desconeguda
])
def test_dades_invalides(concepte, quantitat, categoria):
with pytest.raises(MovimentInvalid):
Moviment(date(2026, 8, 1), concepte, quantitat, categoria)
def test_data_futura_rebutjada():
dema = date.today() + timedelta(days=1) # calculada, mai fixa
with pytest.raises(MovimentInvalid):
Moviment(dema, "Compra", -10.0, "menjar")Fixa't en què es prova: un cas bo, la normalització i tots els casos dolents. Els tres invàlids van en un parametrize perquè són la mateixa prova amb dades diferents, i pytest els compta com a tres proves separades, així que si en falla una saps quina. La de la data futura calcula «demà» en lloc d'escriure una data fixa: una prova amb date(2027, 1, 1) començaria a fallar sola el 2027, i una prova que caduca és pitjor que no tenir-la. Commit: Model Moviment amb validacio i proves (RF-1).
- Fase 3: el magatzem i la seva prova d'anada i tornada
desar és la part fàcil: construeix el diccionari amb l'estructura que vas fixar a 09-02 —versio, seguent_id i la llista de moviments convertits amb m.data.isoformat()— i l'escriu amb ruta.write_text(json.dumps(dades, indent=2, ensure_ascii=False), encoding="utf-8"). La part que cal fer bé és la lectura:
# lesmevesdespeses/magatzem.py (RUTA = Path("despeses.json"), logger = getLogger(__name__))
def carregar(ruta: Path = RUTA) -> tuple[list[Moviment], int]:
"""Llegeix els moviments desats. Retorna llista buida si no hi ha fitxer."""
if not ruta.exists():
return [], 1 # primera arrencada: no es un error
try:
dades = json.loads(ruta.read_text(encoding="utf-8"))
except json.JSONDecodeError:
logger.error("Fitxer de dades corrupte: %s", ruta)
return [], 1 # es registra i es continua, no es mor
moviments = [
Moviment(date.fromisoformat(d["data"]), d["concepte"],
d["quantitat"], d["categoria"], id=d["id"])
for d in dades.get("moviments", [])
]
return moviments, dades.get("seguent_id", len(moviments) + 1)El que cal mirar aquí és la tolerància a fallades, exactament igual que al magatzem.py de TascaFàcil: el fitxer absent és la primera arrencada i el corrupte es registra al log (08-02) en lloc de matar el programa. I el paràmetre ruta amb valor per defecte no és un caprici: és el que permet que les proves facin servir un fitxer temporal.
# tests/test_magatzem.py
def test_cicle_desar_i_carregar(tmp_path):
ruta = tmp_path / "dades.json"
originals = [Moviment(date(2026, 8, 1), "Compra", -62.35, "menjar", id=1),
Moviment(date(2026, 8, 3), "Nomina", 1450.0, "ingressos", id=2)]
desar(originals, seguent_id=3, ruta=ruta)
recuperats, seguent = carregar(ruta)
assert seguent == 3 and len(recuperats) == 2
assert recuperats[0].quantitat == -62.35 # continua sent float
assert recuperats[1].data == date(2026, 8, 3) # continua sent date
def test_fitxer_inexistent_no_falla(tmp_path):
assert carregar(tmp_path / "no_existeix.json") == ([], 1)
def test_fitxer_corrupte_no_falla(tmp_path):
(tmp_path / "trencat.json").write_text("{no es json", encoding="utf-8")
assert carregar(tmp_path / "trencat.json")[0] == []La primera és la prova d'anada i tornada: deso, carrego i comprovo que el que he recuperat és idèntic a l'original. És la prova més rendible de tota la persistència, perquè un sol assert detecta fallades de serialització, de tipus i d'estructura. La fixture tmp_path de pytest dóna un directori temporal diferent per prova i el esborra en acabar, així que les proves no toquen mai les teves dades reals ni s'estorben entre si. Commit: Magatzem JSON tolerant a fitxer absent o corrupte (RF-6).
- Fase 4: la lògica de la col·lecció
# lesmevesdespeses/quadern.py -- colleccio de moviments i operacions sobre ella
class Quadern:
"""Desa els moviments i respon consultes sobre ells."""
def __init__(self, moviments: list[Moviment] | None = None, seguent_id: int = 1):
self._moviments = moviments or []
self.seguent_id = seguent_id
def afegir(self, moviment: Moviment) -> Moviment:
"""Assigna identificador al moviment i l'afegeix al quadern."""
moviment.id = self.seguent_id
self.seguent_id += 1
self._moviments.append(moviment)
return moviment
def del_mes(self, mes: str) -> list[Moviment]:
"""Moviments d'un mes 'AAAA-MM', ordenats per data."""
del_mes = [m for m in self._moviments if m.data.isoformat().startswith(mes)]
return sorted(del_mes, key=lambda m: m.data)
def total_per_categoria(self, mes: str) -> dict[str, float]:
"""Suma les quantitats de cada categoria del mes, de mes despesa a menys."""
totals: dict[str, float] = {}
for m in self.del_mes(mes):
totals[m.categoria] = totals.get(m.categoria, 0.0) + m.quantitat
return dict(sorted(totals.items(), key=lambda parell: parell[1]))
def saldo(self, mes: str) -> float:
"""Diferencia entre ingressos i despeses del mes."""
return round(sum(m.quantitat for m in self.del_mes(mes)), 2)És la traducció literal del pseudocodi de 09-02 (falten __len__, __iter__, cercar —un next() sobre la llista que retorna None si no hi ha coincidència— i eliminar, que fa servir cercar i retorna True o False), i val la pena assenyalar tres coses. total_per_categoria reutilitza del_mes en lloc de repetir el filtre: una responsabilitat, un sol lloc. El key=lambda parell: parell[1] ordena per valor, i com que les despeses són negatives, l'ordre ascendent posa primer la despesa més gran, que és el que volia l'esbós de pantalla (06-02 i 04-05 treballant plegats). I el round(..., 2) de saldo evita que tregui el nas l'aritmètica de coma flotant: sense ell, -62.35 + 1450.0 pot imprimir 1387.6500000000001.
Les proves d'aquesta capa se centren en els càlculs, que és on de debò pot haver-hi un error. I aquí no cal inventar res, perquè cada taula de prova d'escriptori de 09-02 es converteix en un def test_: els mateixos quatre moviments d'aquella taula, el mateix mes i el mateix resultat esperat.
def test_total_per_categoria_agrupa_i_filtra_el_mes():
q = Quadern()
q.afegir(Moviment(date(2026, 7, 30), "Juliol", -20.0, "menjar")) # un altre mes
q.afegir(Moviment(date(2026, 8, 1), "Compra", -62.35, "menjar"))
q.afegir(Moviment(date(2026, 8, 4), "Sopar", -15.0, "menjar"))
q.afegir(Moviment(date(2026, 8, 1), "Abonament", -32.0, "transport"))
totals = q.total_per_categoria("2026-08")
assert totals == {"menjar": -77.35, "transport": -32.0}
assert list(totals)[0] == "menjar" # la despesa mes gran va primerCommit: Quadern: alta, baixa, filtre per mes i totals (RF-3, RF-4, RF-5).
- Fase 5: la interfície i la seva prova manual
La interfície s'omple una opció cada vegada, i cada opció s'acaba del tot abans de començar la següent. La seva regla és la de 04-04: aquí es fa input i print, i res més; cap càlcul del domini.
def registrar_despesa(quadern: Quadern) -> None:
"""Opcio 1 del menu: dona d'alta una despesa."""
concepte = input("Concepte: ")
quantitat = demanar_quantitat("Import (EUR): ")
categoria = input(f"Categoria {CATEGORIES}: ")
text_data = input("Data AAAA-MM-DD (buit = avui): ").strip()
try:
data = date.fromisoformat(text_data) if text_data else date.today()
moviment = quadern.afegir(Moviment(data, concepte, -quantitat, categoria))
except (ValueError, MovimentInvalid) as error:
print(f"No s'ha registrat: {error}")
return
print(f"Despesa registrada (id {moviment.id}).")Tres detalls amb intenció. demanar_quantitat (un bucle while True amb try/except ValueError que només retorna quan el valor és més gran que zero) insisteix fins a obtenir un número vàlid en lloc de rendir-se (02-04), i fa .replace(",", ".") per acceptar la coma decimal, que era un altre cas límit previst. Es demana l'import en positiu i es desa com a -quantitat: a l'usuari no se li pot exigir que entengui el conveni de signes del disseny. I el try/except captura tant ValueError (data mal escrita) com MovimentInvalid (regles del model) i torna al menú amb un missatge, complint el RNF-1.
Com que això no es prova automàticament, es prova amb un guió escrit que executes sencer abans de donar per bona cada sessió de treball:
| # | Què faig | Què ha de passar | OK? |
|---|---|---|---|
| 1 | Esborro despeses.json i arrenco |
Arrenca sense error, quadern buit | |
| 2 | Opció 3 sense dades | «Sense moviments a 2026-08», no una taula buida | |
| 3 | Registro una despesa correcta | Confirma amb id 1 | |
| 4 | Registro amb import dotze i després 12,50 |
Torna a demanar-ho sense trencar-se; després l'accepta com a 12,50 | |
| 5 | Registro amb categoria viatges |
Missatge de categoria desconeguda, torna al menú | |
| 6 | Registro amb data 2026-02-31 |
Missatge de data invàlida, torna al menú | |
| 7 | Surto i torno a entrar | Els moviments continuen allà | |
| 8 | Esborro l'id 99 i després un de real | «No existeix el moviment 99» sense traceback; el real desapareix del fitxer | |
| 9 | Opció 7 i després abc |
«Opció no vàlida» en tots dos casos | |
| 10 | Opció 4 amb dades de dos mesos | Només suma el mes demanat; els percentatges quadren a 100 % |
Aquest guió és la teva xarxa de seguretat per al que les proves automàtiques no cobreixen. Desa'l a tests/guio_manual.md i recorre'l sencer abans de cada etiqueta de versió.
- Quan alguna cosa no funciona: depurar de debò
Tard o d'hora alguna cosa falla i no saps per què. Aplica el mètode de Depuració i gestió d'errors a un cas real: el resum d'agost mostra un percentatge del 137 %.
| Pas | Què es fa | En aquesta fallada concreta |
|---|---|---|
| 1. Reproduir | Trobar la recepta exacta que el provoca sempre | Amb dades_exemple.json, opció 4, mes 2026-08 |
| 2. Llegir l'error | El traceback es llegeix de baix a dalt: tipus, fitxer i línia; la primera línia dels teus fitxers és la sospitosa | Aquí no hi ha traceback: és un resultat incorrecte, el cas més difícil |
| 3. Hipòtesi concreta | Una frase falsable, no «alguna cosa falla» | «El denominador inclou els ingressos, que són positius, i per això el total és menor del que hauria de ser» |
| 4. Comprovar-la | Punt d'interrupció a la línia sospitosa i inspecció de variables | totals inclou "ingressos": 1450.0 i total_despeses val 892.9: confirmada |
| 5. Arreglar la causa | Mai el símptoma | sum(totals.values()) → sum(v for v in totals.values() if v < 0) |
| 6. Prova de regressió | La prova que hauria fallat abans de l'arreglada | assert total_despeses == -100.0 amb dues despeses i un ingrés |
El pas 5 mereix èmfasi: l'arreglada dolenta hauria estat restar 1450 en algun lloc perquè el número quadrés. Hauria funcionat amb aquelles dades i hauria tornat a fallar amb unes altres. I el pas 6 no és opcional: una fallada arreglada sense prova torna, normalment a la refactorització següent. Commit: Corregeix el percentatge del resum, que incloia els ingressos.
- Gestió dels errors de l'usuari
Abans de tancar la implementació, fes un repàs sistemàtic: llista totes les entrades de l'usuari i decideix per a cadascuna què fas. Hi ha dues estratègies i convé no confondre-les. Validar és comprovar abans d'actuar i tornar a demanar la dada: serveix per al que l'usuari pot corregir. Capturar és deixar que l'operació falli i atrapar l'excepció amb try/except: serveix per al que no es pot comprovar per avançat sense duplicar la feina.
| Entrada | Risc | Estratègia | On |
|---|---|---|---|
| Opció del menú | Text o número fora de rang | Validar contra el diccionari | interficie.executar |
| Import | No numèric, zero, negatiu, amb coma | Validar en bucle | interficie.demanar_quantitat |
| Data i mes | Format o dia impossible | Capturar ValueError |
interficie.registrar_despesa |
| Categoria | Desconeguda, majúscules, espais | Normalitzar i validar | model.Moviment |
| Id a esborrar | No numèric o inexistent | Capturar + comprovar None |
interficie + quadern.cercar |
| Fitxer de dades | Absent, corrupte, sense permisos | Capturar i arrencar buit | magatzem.carregar |
La regla d'or és a l'última columna: la validació de les regles del domini viu al model, la del format d'entrada viu a la interfície. Si barreges les dues, acabes validant la categoria en tres llocs diferents i oblidant-te'n d'un.
- Commits durant el desenvolupament
Un commit és una unitat de treball amb sentit: alguna cosa que funciona i que podries explicar en una frase. Ni cada Ctrl+S ni una vegada per setmana. A la pràctica, un per volta del bucle de l'apartat 1.
$ git log --oneline
a91c02f Afegeix exportacio a CSV del mes (RF-7)
7d4e1b8 Corregeix el percentatge del resum, que incloia els ingressos
3f2a90c Afegeix pantalla de resum per categoria (RF-4)
c18b7e5 Quadern: alta, baixa, filtre per mes i totals (RF-3, RF-5)
9e04a13 Magatzem JSON tolerant a fitxer absent o corrupte (RF-6)
2a1f6e0 Esquelet: menu principal que arrenca i surtAquest historial es llegeix com el diari del projecte: cada línia és un increment, cada missatge comença per un verb i uns quants citen el requisit que implementen. Tres regles pràctiques: no facis mai commit amb les proves en vermell (si necessites desar a mitges, fes servir una branca), no barregis refactorització amb funcionalitat nova al mateix commit (08-05), i no desis dades personals ni l'entorn virtual, que per això hi ha el .gitignore de 09-02.
- Quan et bloqueges
Bloquejar-se és normal i li passa a tothom. El que distingeix qui avança és tenir un procediment en lloc de mirar la pantalla:
- L'ànec de goma. Explica el problema en veu alta, línia a línia, a un objecte inanimat. Sona ridícul i funciona: en obligar-te a verbalitzar el que creus que fa el codi, trobes el punt on el que creus i el que passa no coincideixen.
- Redueix a un exemple mínim i divideix. Treu el tros sospitós a un fitxer nou de deu línies amb dades inventades: o la fallada desapareix —i la causa és al context que has tret— o es reprodueix en deu línies, que ja saps depurar. Amb
assertoprintlocalitza el punt exacte en què les dades deixen de ser el que esperes; la fallada és entre l'últim punt correcte i el primer incorrecte. - Busca el missatge exacte. Copia el text de l'error sense els teus noms de variable:
TypeError: unsupported operand type(s) for -: 'str' and 'int', noerror a la meva funcio resum. La cerca del missatge literal gairebé sempre encerta el cas. - Llegeix la documentació oficial.
docs.python.orgté la resposta a qualsevol dubte sobre la biblioteca estàndard, amb exemples. Un tutorial d'un blog et dóna una recepta; la documentació et dóna el model mental, i aquest serveix per als vint dubtes següents. - Descansa. Després de quaranta minuts encallat, la probabilitat de resoldre-ho cau en picat. Aixecar-se vint minuts és la tècnica de depuració més eficaç que existeix i la més difícil d'aplicar.
Sobre els assistents d'IA: són una eina legítima i seran a la teva vida professional, però en el teu primer projecte poden robar-te justament l'aprenentatge que busques. Sí que expliquin un missatge d'error, que preguntis per què una funció es comporta com ho fa, que demanis una revisió del teu codi ja escrit i que t'aclareixin un concepte. No a «fes-me el projecte» ni a enganxar codi que no entens: un projecte que no saps explicar (09-04) no et serveix ni en una entrevista ni quan calgui arreglar-lo. Regla pràctica: intenta el problema vint minuts pel teu compte abans de preguntar, i no enganxis mai codi que no sabries reescriure de memòria en allò essencial.
- Checklist d'«acabat»
Abans de donar per tancada la implementació i passar a la presentació:
| # | Comprovació | Fet? |
|---|---|---|
| 1 | Tots els requisits Must funcionen de cap a cap | |
| 2 | El guió de prova manual passa sencer | |
| 3 | pytest en verd, amb proves de model, lògica i persistència |
|
| 4 | Primera arrencada sense fitxer de dades: funciona | |
| 5 | Cap entrada de l'usuari no produeix un traceback i els casos límit de 09-01 estan comprovats | |
| 6 | black . i ruff check . sense queixes |
|
| 7 | Sense codi mort, print de depuració ni TODO oblidats |
|
| 8 | Tot committejat; git status net |
|
| 9 | El programa arrenca en una carpeta acabada de clonar |
La 9 és la que més falla i la que més vergonya fa en una demostració: el programa funcionava només perquè a la teva carpeta hi havia un fitxer que no era al repositori. Prova-ho de debò: clona en un altre directori i executa.
Errors Comuns i Consells
- Escriure-ho tot abans d'executar res. L'error clàssic. Trenta errors encadenats no es depuren, s'abandonen. Executa cada quinze minuts.
- Deixar la validació per al final. «Ja posaré els
try/exceptquan funcioni» acaba amb un programa que es trenca a la demostració. La validació s'escriu amb la funció, no després. - Perseguir el símptoma. Restar un número màgic perquè el total quadri tapa la fallada, que tornarà amb unes altres dades: arregla la causa i escriu la prova de regressió.
- Abandonar les proves a mitges. Quan pitgen les ganes d'avançar, el primer que cau són les proves, i justament aleshores és quan comencen a fer falta. Prova almenys model i càlculs. I no barregis mai refactorització amb funcionalitat nova al mateix commit: si alguna cosa es trenca, no sabràs quina de les dues va ser.
- Consell: acaba cada sessió amb el programa funcionant i committejat, i deixa un
SEGUENT.mdde dues línies amb el que ve. Tornar a un projecte trencat és la millor manera de no tornar-hi, i aquestes dues línies t'estalvien vint minuts de recontextualització a cada sessió.
Exercicis
Són les següents tasques reals del teu projecte. En acabar, hauries de tenir la implementació tancada.
Exercici 1: Esquelet i model provat
Implementa la fase 1 (el programa arrenca, mostra el menú i surt) i la fase 2 (la teva entitat principal amb validació completa i excepció pròpia). Escriu tests/test_model.py amb almenys sis proves: un cas vàlid, la normalització de dades i quatre casos invàlids, fent servir parametrize per als que comparteixin estructura. Fes un commit per fase.
Exercici 2: Persistència amb prova d'anada i tornada
Implementa el magatzem: desar i carregar en el format que vas decidir a 09-02, tolerant a fitxer absent i corrupte. Escriu les tres proves amb tmp_path: cicle complet d'anada i tornada, fitxer inexistent i fitxer corrupte. Després connecta el magatzem a l'esquelet: en arrencar carrega, en sortir desa.
Exercici 3: Un requisit de cap a cap i el seu guió
Tria el teu requisit funcional principal (l'equivalent al RF-1) i implementa'l complet: opció del menú, entrada validada, crida a la lògica, persistència i confirmació en pantalla. Després escriu el teu guió de prova manual amb un mínim de deu comprovacions que incloguin els casos límit de 09-01, i executa'l sencer anotant el que falli.
Solucions
Solució 1. Els apartats 3 i 4 contenen la solució per a LesMevesDespeses. Contrasta el teu model amb aquests criteris: la validació és dins de l'entitat i no a la interfície, la normalització passa abans que la validació, l'excepció pròpia hereta de ValueError, i cap prova no fa servir una data fixa que pugui caducar. Rúbrica: el teu model fa impossible construir un objecte invàlid? Tens almenys una prova per regla de validació? pytest passa en menys d'un segon?
Solució 2. Apartat 5. Els tres punts que solen fallar: que la ruta sigui un paràmetre amb valor per defecte (sense això no pots fer servir tmp_path), que el fitxer absent retorni buit en lloc de llançar, i que els tipus sobrevisquin al viatge —una data ha de tornar com a date i un import com a float, no com a cadenes—. Rúbrica: la prova d'anada i tornada comprova valors concrets i no només la longitud de la llista? Les proves fan servir tmp_path i no toquen el teu fitxer real? La primera arrencada sense fitxer funciona?
Solució 3. Apartat 7, amb registrar_despesa i la taula de deu comprovacions. L'essencial no és el codi sinó el repartiment: la interfície demana i mostra, el model valida, el quadern desa en memòria, el magatzem escriu. Si la teva funció d'alta calcula alguna cosa del domini o el teu model imprimeix, la separació de capes s'ha trencat. Rúbrica: el teu guió inclou el cas de llista buida, el d'entrada no numèrica i el d'identificador inexistent? Passa sencer, sense ni un sol traceback? Has fet commit del requisit citant-ne el número?
Conclusió
Implementar bé és, sobretot, una qüestió de mètode. L'estratègia és incremental i el programa està sempre executable: primer l'esquelet que arrenca i surt, després una funcionalitat completa de cap a cap i només llavors la següent, girant cada quinze o seixanta minuts el bucle escriure → executar → provar → commit. L'ordre de construcció va de dins cap a fora —model amb les seves proves, magatzem amb la seva prova d'anada i tornada, lògica de la col·lecció, interfície opció a opció i extres al final— perquè les capes interiors són les que es proven automàticament i les que sostenen tota la resta.
Les proves s'escriuen amb el codi, no després: un cas bo, la normalització i tots els casos dolents al model, amb parametrize per als que comparteixen estructura; el cicle desar/carregar i els fitxers absent i corrupte al magatzem amb tmp_path; els càlculs a la lògica, convertint cada prova d'escriptori de 09-02 en un def test_; i per a la interfície, un guió manual escrit que es recorre sencer abans de cada versió. Quan alguna cosa falla, el mètode de 08-02 en sis passos —reproduir, llegir, formular una hipòtesi concreta, comprovar-la amb el depurador, arreglar la causa i escriure la prova de regressió— converteix un misteri en una tasca de vint minuts. Els errors de l'usuari es reparteixen entre validar (el corregible, a la interfície) i capturar (l'imprevisible, amb try/except), amb les regles del domini sempre al model. Els commits marquen cada increment amb un verb i el requisit implementat, mai en vermell i mai barrejant refactorització amb funcionalitat. I per als bloquejos hi ha procediment: ànec de goma, exemple mínim, buscar el missatge exacte, documentació oficial, dividir i provar, i descansar —amb els assistents d'IA fets servir per entendre i revisar, mai per substituir-te, i sempre després de vint minuts pel teu compte—. La checklist d'acabat, amb la seva comprovació final de clonar en una carpeta neta i arrencar, tanca la implementació.
Arribats aquí tens un programa que funciona, està provat i viu en un repositori endreçat. Falta el que converteix un projecte en alguna cosa que compta per als altres: saber-lo ensenyar. A Presentació del projecte deixarem el repositori presentable, escriurem el README com a carta de presentació, prepararem una demostració de cinc minuts i aprendrem a defensar les decisions tècniques i a parlar de les limitacions sense restar-nos valor.
Fonaments de la Programació
Mòdul 1: Introducció a la Programació
- Què és la programació?
- Història de la programació
- Llenguatges de programació
- Entorns de desenvolupament
- Del problema a l'algorisme
Mòdul 2: Conceptes Bàsics
- Variables i tipus de dades
- Operadors i expressions
- Entrada i sortida de dades
- Conversió de tipus i validació de dades
Mòdul 3: Estructures de Control
Mòdul 4: Funcions i Procediments
- Definició i ús de funcions
- Paràmetres i retorn de valors
- Àmbit de variables
- Descompondre un programa en funcions
- Funcions com a valors: lambda i ordre superior
Mòdul 5: Estructures de Dades
- Llistes i arrays
- Cadenes de caràcters
- Diccionaris i conjunts
- Tuples i estructures imbricades
- Desar dades en fitxers: text, CSV i JSON
Mòdul 6: Algorismes Bàsics
Mòdul 7: Objectes i Organització del Codi
- De les dades als objectes: classes i instàncies
- Atributs, mètodes i constructor
- Col·leccions d'objectes
- Mòduls, paquets i importacions
Mòdul 8: Bones Pràctiques i Eines
- Documentació i comentaris
- Depuració i gestió d'errors
- Control de versions
- Proves automatitzades
- Estil, llegibilitat i refactorització
