Llistes, tuples, diccionaris, conjunts i cadenes: l'arsenal bàsic està complet, i el catàleg de Papyrus viu per fi en una sola estructura. Però la biblioteca estàndard (03-05) guarda un mòdul pensat exactament per polir els patrons que hem anat improvisant: collections. Aquí saldem la promesa de 04-02 amb namedtuple a fons, comptem els llibres més venuts amb Counter, agrupem comandes per client amb defaultdict i muntem una cua de reserves eficient amb deque — i tanquem el mòdul amb la taula de criteri definitiva: quina estructura triar en cada situació.
Contingut
namedtuple: registres amb nom, a fonsCounter: comptar sense buclesdefaultdict: diccionaris amb valor per defectedeque: cues eficients pels dos extremsOrderedDict: nota històrica- Criteris d'elecció: la taula-resum del mòdul
namedtuple: registres amb nom, a fons
A 04-02 la vam tastar com a aperitiu; ara, el plat complet. Una namedtuple és una fàbrica de classes de tuples: defineixes el nom del registre i els seus camps, i obtens un tipus nou les instàncies del qual són tuples amb camps accessibles per nom.
from collections import namedtuple
Llibre = namedtuple("Llibre", ["titol", "preu", "estoc"])
faust = Llibre("Faust", 21.00, 0)
odissea = Llibre(titol="L'Odissea", preu=12.50, estoc=4) # tambe amb keywords
print(faust.preu) # 21.0 → per nom: s'han acabat els indexs muts
print(faust[1]) # 21.0 → continua sent una tupla: index, llescat, len()...
titol, preu, estoc = faust # i desempaquetatge
print(faust) # Llibre(titol='Faust', preu=21.0, estoc=0) → repr llegible gratis!Tres utilitats extra que convé conèixer:
print(faust._asdict()) # {'titol': 'Faust', 'preu': 21.0, 'estoc': 0} → a dict
rebaixat = faust._replace(preu=18.90) # "modificar" un immutable: crea UNA ALTRA namedtuple
print(rebaixat.preu, faust.preu) # 18.9 21.0 → l'original, intacte
Llibre2 = namedtuple("Llibre2", "titol preu estoc") # els camps tambe valen com a cadenaI el catàleg? La namedtuple competeix amb el dict imbricat de 04-03, i l'elecció és una decisió de disseny real:
# Opcio A (04-03): dict de dicts — flexible, camps com a cadenes
cataleg = {"Faust": {"preu": 21.00, "estoc": 0}}
# Opcio B: dict de namedtuples — camps amb nom real i protegits contra errors de tecleig
cataleg_nt = {"Faust": Llibre("Faust", 21.00, 0)}
print(cataleg_nt["Faust"].preu) # 21.0
# cataleg_nt["Faust"].estoc = 3 # AttributeError: es immutableAmb l'opció B, un error tipogràfic (fitxa["prue"]) que al dict crearia silenciosament una clau nova, aquí falla a l'instant (AttributeError). A canvi, actualitzar l'estoc exigeix _replace() i reassignar — incòmode per a dades que canvien a cada venda. Per a Papyrus mantenim el dict imbricat com a estructura canònica mutable i fem servir namedtuple per a registres de només lectura, com les línies d'un tiquet ja cobrat.
Counter: comptar sense bucles
Comptar aparicions és tan comú que collections ho porta resolt. Counter és un diccionari especialitzat: claus = elements, valors = quantes vegades apareixen.
from collections import Counter
# Vendes del mes a Papyrus, tal com es van registrar
vendes = ["Hamlet", "El Quixot", "Hamlet", "L'Odissea", "Hamlet", "El Quixot"]
comptador = Counter(vendes)
print(comptador) # Counter({'Hamlet': 3, 'El Quixot': 2, "L'Odissea": 1})
print(comptador["Hamlet"]) # 3
print(comptador["Faust"]) # 0 → clau inexistent retorna 0, NO KeyError
print(comptador.most_common(2)) # [('Hamlet', 3), ('El Quixot', 2)] → top vendes, ordenat
comptador.update(["Faust", "Hamlet"]) # arriben mes vendes
print(comptador["Hamlet"]) # 4
print(sum(comptador.values())) # 8 → total d'exemplars venuts
lletres = Counter("papyrus") # tambe compta caracters d'una cadenaCompara-ho amb l'alternativa manual — el bucle amb if titol in comptes: ... else: ... que hauries escrit al mòdul 2 — i aprecia la diferència: most_common() et dona el rànquing de supervendes de l'Anna en una crida, a punt per a l'aparador. Com que és un dict amb esteroides, tot el de 04-03 funciona: items(), comprensions, in...
defaultdict: diccionaris amb valor per defecte
Patró clàssic: agrupar coses. L'Anna vol les comandes organitzades per client. Amb un dict normal cal comprovar si la clau existeix abans de fer append:
comandes = [("Lluis", "El Quixot"), ("Marta", "Hamlet"),
("Lluis", "Faust"), ("Marta", "L'Odissea"), ("Lluis", "Hamlet")]
# Amb dict normal: la comprovacio fa nosa
per_client = {}
for client, titol in comandes:
if client not in per_client:
per_client[client] = [] # la cerimonia d'inicialitzar
per_client[client].append(titol)defaultdict elimina la cerimònia: es construeix amb una fàbrica (una funció sense arguments, com list, int o set) que s'invoca automàticament la primera vegada que s'accedeix a una clau inexistent:
from collections import defaultdict
per_client = defaultdict(list) # clau nova → es crea [] automaticament
for client, titol in comandes:
per_client[client].append(titol) # sense if: la primera vegada crea la llista sola
print(dict(per_client))
# {'Lluis': ['El Quixot', 'Faust', 'Hamlet'], 'Marta': ['Hamlet', "L'Odissea"]}
despesa = defaultdict(float) # float() → 0.0: acumuladors sense inicialitzar
despesa["Lluis"] += 15.71
despesa["Lluis"] += 20.75
print(despesa["Lluis"]) # 36.46| Fàbrica | Valor inicial | Ús típic |
|---|---|---|
list |
[] |
Agrupar (append per clau) |
int |
0 |
Comptar (tot i que Counter sol ser millor) |
float |
0.0 |
Acumular imports |
set |
set() |
Agrupar sense duplicats |
Matís important: llegir també crea. per_client["Ningu"] insereix "Ningu": [] només per consultar-lo. Si has de fer moltes consultes de només lectura, converteix-lo abans a dict normal (dict(per_client)) o fes servir get().
deque: cues eficients pels dos extrems
A 04-01 vam muntar la cua de comandes amb una llista i vam avisar: pop(0) desplaça tots els elements restants. deque (double-ended queue, es pronuncia "dec") està optimitzada per afegir i treure per tots dos extrems sense desplaçar res:
from collections import deque
reserves = deque() # la cua de reserves de "Faust" (esgotat, recordes?)
reserves.append("Lluis") # entra per la dreta, com en una llista
reserves.append("Marta")
reserves.appendleft("Pau") # urgent! entra per l'esquerra
print(reserves) # deque(['Pau', 'Lluis', 'Marta'])
seguent = reserves.popleft() # surt per l'esquerra: el primer de la cua
print(seguent) # 'Pau' → arriben exemplars de Faust: avis a en Pau
ultim = reserves.pop() # tambe pot sortir per la dreta
ultimes_vendes = deque(maxlen=3) # amb maxlen: en omplir-se, expulsa per l'altre extrem
for t in ["Hamlet", "L'Odissea", "El Quixot", "Faust"]:
ultimes_vendes.append(t)
print(ultimes_vendes) # deque(["L'Odissea", 'El Quixot', 'Faust'], maxlen=3)El maxlen és una joia per a "les N últimes operacions" (historial del menú de Papyrus, per exemple): es manté sol. La contrapartida de deque: accedir per índex al mig (reserves[500]) és lent comparat amb una llista. Regla: extrems → deque; posicions interiors → llista.
graph LR
A["appendleft()"] --> D["deque: Pau · Lluís · Marta"]
D --> B["pop()"]
C["popleft()"] --- D
D --- E["append()"]
OrderedDict: nota històrica
En codi antic veuràs collections.OrderedDict: un dict que garantia l'ordre d'inserció quan els dicts normals no ho feien. Des de Python 3.7 tots els dict conserven l'ordre, així que avui gairebé mai no el necessites. Sobreviu per compatibilitat i per dos detalls menors (la seva igualtat sí que compara l'ordre, i té move_to_end()). Si el veus en un tutorial, ja saps per què hi és — i que pots fer servir un dict normal.
Criteris d'elecció: la taula-resum del mòdul
La decisió d'estructura és la primera decisió de disseny de qualsevol programa. Tot el mòdul 4, en una taula:
| Estructura | Ordenada | Mutable | Duplicats | Accés | Tria-la quan... |
|---|---|---|---|---|---|
list |
Sí | Sí | Sí | Per índex | Col·lecció homogènia que creix/minva/s'ordena (cua de comandes, títols) |
tuple |
Sí | No | Sí | Per índex | Registre fix i heterogeni; retorn múltiple; clau composta de dict |
dict |
Inserció | Sí | Claus no | Per clau | Associar identitat → dades: el catàleg {títol: fitxa} |
set |
No | Sí | No | Pertinença | Unicitat, in massiu, àlgebra de conjunts (socis per mes) |
frozenset |
No | No | No | Pertinença | Un set que hagi de ser clau o constant |
str |
Sí | No | Sí | Per índex | Text; normalitzar amb els seus mètodes; construir amb join() |
namedtuple |
Sí | No | Sí | Per nom | Registre de només lectura amb camps llegibles (línia de tiquet) |
Counter |
— | Sí | (compta) | Per clau | Comptar aparicions i rànquings (most_common) |
defaultdict |
Inserció | Sí | Claus no | Per clau | Agrupar/acumular sense inicialitzar claus |
deque |
Sí | Sí | Sí | Extrems | Cues i "últims N" (reserves de Faust) |
I com a guia de decisió ràpida:
graph TD
A["Què necessito guardar?"] --> B{"Parells clau→valor?"}
B -- "Sí" --> C{"Claus que poden faltar en agrupar?"}
C -- "Sí" --> D["defaultdict"]
C -- "És comptar?" --> E["Counter"]
C -- "No" --> F["dict"]
B -- "No" --> G{"Importen ordre i posició?"}
G -- "No: només unicitat/pertinença" --> H["set / frozenset"]
G -- "Sí" --> I{"Canviarà després de crear-se?"}
I -- "Sí" --> J{"Entra/surt pels extrems?"}
J -- "Sí" --> K["deque"]
J -- "No" --> L["list"]
I -- "No" --> M{"Camps amb significat propi?"}
M -- "Sí" --> N["namedtuple"]
M -- "No" --> O["tuple"]
Errors Comuns i Consells
- Oblidar l'import:
NameError: name 'Counter' is not defined. Tot el d'aquesta lliçó viu acollections:from collections import Counter, defaultdict, deque, namedtuple. - Mutar una
namedtuple:llibre.estoc = 3llançaAttributeError. És una tupla: fes servir_replace()i reassigna, o reconeix que aquella dada demanava un dict. defaultdictque "s'engreixa" sol: cada lectura d'una clau nova la crea. Per consultar sense crear,d.get(clau)o convertir adicten acabar de construir.- Fer servir
Countercom a validador d'existència:comptador["X"]retorna0en comptes deKeyError, així queif comptador["X"]:és correcte peròcomptador["X"]mai no delatarà una clau mal escrita. - Indexar el mig d'una
dequeen bucle: per a això és dolenta; si necessites posicions interiors, era una llista. - Triar estructura per costum ("tot llistes"): revisa la taula. L'estructura correcta elimina codi — el gran refactor de 04-03 va esborrar més línies de les que va afegir.
- Consell:
Counter,defaultdictidequesón dicts/cues normals en la seva interfície; tot el que has après a 04-03 i 04-01 els aplica. No són estructures noves per aprendre, sinó dreceres sobre les que ja domines.
Exercicis
- Rànquing de supervendes. Amb
vendes = ["Hamlet", "El Quixot", "Hamlet", "L'Odissea", "Hamlet", "El Quixot", "Faust"], fes servirCounterper imprimir el podi (top 3) amb format1. Hamlet — 3 ut.fent servirmost_common()ienumerate(start=1)(mòdul 2, al rescat). Afegeix-hi el total d'exemplars venuts. - Agrupador de reserves. Amb la llista de tuples
reserves = [("Faust", "Lluis"), ("Faust", "Marta"), ("Iliada", "Lluis"), ("Faust", "Pau")], construeix ambdefaultdict(list)el dicttitol → [clients en ordre]. Després converteix la llista de "Faust" en unadequei simula l'arribada de 2 exemplars: fespopleft()dues vegades i informa a qui s'avisa i qui continua esperant. - Tiquet immutable. Defineix la
namedtupleLiniaTiquetamb campstitol,unitatsiquantia. Crea les línies d'una compra d'en Lluís (2 "Hamlet" a 9.83 cadascun → quantia 19.66; 1 "El Quixot" → 15.71), guarda-les en una llista i imprimeix el tiquet amb f-strings alineades (04-05) i el total ambsum()i una expressió generadora sobre.quantia.
Solucions
# Exercici 1
from collections import Counter
vendes = ["Hamlet", "El Quixot", "Hamlet", "L'Odissea", "Hamlet", "El Quixot", "Faust"]
comptador = Counter(vendes)
for lloc, (titol, unitats) in enumerate(comptador.most_common(3), start=1):
print(f"{lloc}. {titol} — {unitats} ut.")
print(f"Total venuts: {sum(comptador.values())}")
# 1. Hamlet — 3 ut. / 2. El Quixot — 2 ut. / 3. L'Odissea — 1 ut. / Total: 7Fixa't: most_common(3) retorna tuples (titol, unitats), i el for les desempaqueta dins del parell d'enumerate — desempaquetatge imbricat amb parèntesis.
# Exercici 2
from collections import defaultdict, deque
reserves = [("Faust", "Lluis"), ("Faust", "Marta"), ("Iliada", "Lluis"), ("Faust", "Pau")]
per_titol = defaultdict(list)
for titol, client in reserves:
per_titol[titol].append(client)
cua_faust = deque(per_titol["Faust"]) # deque(['Lluis', 'Marta', 'Pau'])
for _ in range(2): # arriben 2 exemplars
print(f"Avisar {cua_faust.popleft()}: el seu Faust ha arribat")
print(f"Continuen esperant: {list(cua_faust)}") # ['Pau']# Exercici 3
from collections import namedtuple
LiniaTiquet = namedtuple("LiniaTiquet", ["titol", "unitats", "quantia"])
linies = [
LiniaTiquet("Hamlet", 2, 19.66),
LiniaTiquet("El Quixot", 1, 15.71),
]
for linia in linies:
print(f"{linia.unitats} x {linia.titol:<12}{linia.quantia:>8.2f} EUR")
total = sum(linia.quantia for linia in linies)
print(f"{'TOTAL':<16}{total:>8.2f} EUR") # 35.37 EURConsell: les línies d'un tiquet cobrat no han de canviar mai — la immutabilitat de la namedtuple no és una limitació aquí, és exactament la garantia que el negoci necessita.
Conclusió
El mòdul 4 compleix el que va prometre en tancar el mòdul 3: cada llibre de Papyrus és per fi una sola peça d'informació. Has repassat les llistes a fons amb la seva mutabilitat i els seus paranys d'àlies (04-01); les tuples et van donar registres immutables i el retorn múltiple que 03-02 va deixar promès (04-02); els diccionaris van executar el gran refactor — el catàleg és avui {títol: {"preu": ..., "estoc": ...}} amb accés directe per clau (04-03); els conjunts van aportar unicitat, pertinença ultraràpida i àlgebra per comparar socis i deduplicar comandes (04-04); les cadenes van revelar la seva caixa d'eines completa i la normalització definitiva de cercar_llibre() (04-05); i collections va posar nom als patrons que faltaven — namedtuple, Counter, defaultdict, deque — juntament amb la taula de criteri per triar estructura sense dubtar. Però mira el codi de Papyrus amb ulls nous: el catàleg viu en un diccionari i les funcions que saben operar-lo — cercar_llibre(), mostrar_cataleg(), preu_final() — viuen a part, a papyrus_utils.py, confiant que ningú no els passi una estructura equivocada. Dades per una banda, comportament per l'altra. El mòdul 5 presenta l'eina que els uneix en una sola peça amb nom propi: les classes. Cada llibre deixarà de ser una entrada de diccionari per convertir-se en un objecte que sap calcular el seu propi preu de soci — la programació orientada a objectes comença on les estructures de dades acaben.
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
