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

  1. namedtuple: registres amb nom, a fons
  2. Counter: comptar sense bucles
  3. defaultdict: diccionaris amb valor per defecte
  4. deque: cues eficients pels dos extrems
  5. OrderedDict: nota històrica
  6. 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 cadena

I 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 immutable

Amb 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 cadena

Compara-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 Per índex Col·lecció homogènia que creix/minva/s'ordena (cua de comandes, títols)
tuple No Per índex Registre fix i heterogeni; retorn múltiple; clau composta de dict
dict Inserció Claus no Per clau Associar identitat → dades: el catàleg {títol: fitxa}
set No 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 No Per índex Text; normalitzar amb els seus mètodes; construir amb join()
namedtuple No Per nom Registre de només lectura amb camps llegibles (línia de tiquet)
Counter (compta) Per clau Comptar aparicions i rànquings (most_common)
defaultdict Inserció Claus no Per clau Agrupar/acumular sense inicialitzar claus
deque 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 a collections: from collections import Counter, defaultdict, deque, namedtuple.
  • Mutar una namedtuple: llibre.estoc = 3 llança AttributeError. És una tupla: fes servir _replace() i reassigna, o reconeix que aquella dada demanava un dict.
  • defaultdict que "s'engreixa" sol: cada lectura d'una clau nova la crea. Per consultar sense crear, d.get(clau) o convertir a dict en acabar de construir.
  • Fer servir Counter com a validador d'existència: comptador["X"] retorna 0 en comptes de KeyError, així que if comptador["X"]: és correcte però comptador["X"] mai no delatarà una clau mal escrita.
  • Indexar el mig d'una deque en 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, defaultdict i deque só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

  1. Rànquing de supervendes. Amb vendes = ["Hamlet", "El Quixot", "Hamlet", "L'Odissea", "Hamlet", "El Quixot", "Faust"], fes servir Counter per imprimir el podi (top 3) amb format 1. Hamlet — 3 ut. fent servir most_common() i enumerate(start=1) (mòdul 2, al rescat). Afegeix-hi el total d'exemplars venuts.
  2. Agrupador de reserves. Amb la llista de tuples reserves = [("Faust", "Lluis"), ("Faust", "Marta"), ("Iliada", "Lluis"), ("Faust", "Pau")], construeix amb defaultdict(list) el dict titol → [clients en ordre]. Després converteix la llista de "Faust" en una deque i simula l'arribada de 2 exemplars: fes popleft() dues vegades i informa a qui s'avisa i qui continua esperant.
  3. Tiquet immutable. Defineix la namedtuple LiniaTiquet amb camps titol, unitats i quantia. 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 amb sum() 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: 7

Fixa'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 EUR

Consell: 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

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