La lliçó anterior va acabar amb una pregunta: per què consultar cataleg["Faust"] és pràcticament instantani? La resposta viu en una estructura germana del diccionari: el conjunt (set). Un conjunt és una col·lecció sense ordre garantit, sense duplicats i sense valors associats — només elements, com les claus d'un diccionari sense els seus valors. A canvi de renunciar a l'ordre i a la repetició, ofereix dos superpoders: comprovar pertinença a tota velocitat i operar amb la lògica de la teoria de conjunts (unió, intersecció, diferència) que a Papyrus resoldrà preguntes com "quins socis van comprar aquest mes però no el passat?" en una sola línia.

Contingut

  1. Crear conjunts (i el parany del {} buit)
  2. Unicitat: els duplicats desapareixen
  3. Afegir i eliminar elements
  4. Pertinença ultraràpida: per què in vola
  5. Operacions de conjunts: unió, intersecció, diferències
  6. Subconjunts i superconjunts
  7. frozenset: el conjunt immutable
  8. Comprensions de conjunts

Crear conjunts (i el parany del {} buit)

generes = {"narrativa", "teatre", "poesia"}           # literal: claus amb elements solts
de_llista = set(["narrativa", "teatre", "narrativa"]) # set() converteix iterables i DEDUPLICA
print(de_llista)                                      # {'narrativa', 'teatre'} → el duplicat ha marxat

lletres = set("papyrus")    # {'p', 'a', 'y', 'r', 'u', 's'} → la 'p' repetida, fora

buit = set()                # UNICA manera de crear un conjunt buit
parany = {}                 # aixo es un DICCIONARI buit, no un set!
print(type(buit), type(parany))    # <class 'set'> <class 'dict'>

El parany del {} és històric: els diccionaris van arribar abans a Python i es van quedar les claus buides. Memoritza-ho: conjunt buit = set(), sempre.

Els elements han de ser immutables (hashables), com les claus d'un dict: cadenes, nombres i tuples sí; llistes i diccionaris no. I no hi ha indexació: generes[0] és un TypeError, perquè un conjunt no té posicions.

Unicitat: els duplicats desapareixen

La unicitat no és una restricció molesta: és la funcionalitat estrella. El cas d'ús més freqüent del món real — eliminar duplicats d'una llista — és una línia:

# Comandes del dia a Papyrus: en Lluis ha demanat El Quixot dues vegades per error
comandes = ["El Quixot", "Hamlet", "El Quixot", "Faust", "Hamlet"]

titols_unics = set(comandes)
print(titols_unics)                # {'Faust', 'Hamlet', 'El Quixot'} (ordre no garantit)
print(len(titols_unics))           # 3 titols diferents, tot i que hi va haver 5 comandes

# Si necessites conservar una llista (p. ex., ordenada), converteix de tornada:
unics_ordenats = sorted(set(comandes))
print(unics_ordenats)              # ['El Quixot', 'Faust', 'Hamlet']

Compte: set() no conserva l'ordre original de la llista. Si l'ordre d'aparició importa, hi ha un idiom amb dict que veuràs als errors comuns.

Afegir i eliminar elements

Els conjunts són mutables (n'existeix la versió immutable: frozenset, més avall):

generes = {"narrativa", "teatre"}

generes.add("poesia")            # afegeix un element (si ja hi es, no passa res)
generes.add("teatre")            # sense efecte ni error: ja hi era
generes.update(["assaig", "narrativa"])   # n'afegeix diversos de cop (com extend a les llistes)

generes.remove("assaig")         # elimina; KeyError si no existeix
generes.discard("ciencia")       # elimina si hi es; SILENCI si no hi es
element = generes.pop()          # treu un element ARBITRARI (no "l'ultim": no hi ha ordre)
generes.clear()                  # buida el conjunt
Mètode Si l'element no hi és...
remove(x) Llança KeyError
discard(x) No fa res (l'opció tranquil·la)

Pertinença ultraràpida: per què in vola

Amb una llista, x in llista obliga Python a comparar element a element, des del principi: si la llista té un milió d'entrades, en el pitjor cas fa un milió de comparacions. Amb un conjunt (o les claus d'un dict), Python calcula el hash de l'element — una empremta numèrica — i salta directament a la casella interna on seria si existís. Una comprovació, tingui el conjunt deu elements o deu milions.

socis = {"LLUIS-001", "MARTA-002", "PAU-003"}   # codis de soci de Papyrus

def es_soci_valid(codi):
    """La funcio de papyrus_utils, ara amb un set al darrere: validacio immediata."""
    return codi.strip().upper() in socis

print(es_soci_valid("lluis-001"))   # True
print(es_soci_valid("ANNA-999"))    # False

Aquesta és també la resposta a la pregunta de 04-03: cataleg["Faust"] és instantani perquè les claus d'un diccionari fan servir exactament el mateix mecanisme de hash. No necessites la teoria formal de complexitat (arribarà si algun dia la cerques com a "O(1) vs O(n)"); queda't amb la regla pràctica:

Moltes comprovacions de "hi és o no hi és?" sobre la mateixa col·lecció → converteix-la primer en set.

Operacions de conjunts: unió, intersecció, diferències

Aquí els conjunts brillen amb llum pròpia. Cada operació existeix com a operador i com a mètode (el mètode accepta qualsevol iterable; l'operador exigeix dos sets):

Operació Operador Mètode Resultat
Unió a | b a.union(b) Elements a a o a b (o tots dos)
Intersecció a & b a.intersection(b) Elements a a i a b
Diferència a - b a.difference(b) A a però no a b
Diferència simètrica a ^ b a.symmetric_difference(b) A a o a b, però no a tots dos

Amb dades de Papyrus: l'Anna vol analitzar la fidelitat dels seus socis comparant els compradors de juny i juliol:

compradors_juny = {"Lluis", "Marta", "Pau", "Julia"}
compradors_juliol = {"Lluis", "Julia", "Omar"}

print(compradors_juny | compradors_juliol)   # {'Lluis','Marta','Pau','Julia','Omar'} → clientela activa
print(compradors_juny & compradors_juliol)   # {'Lluis', 'Julia'} → els fidels: repeteixen mes a mes
print(compradors_juny - compradors_juliol)   # {'Marta', 'Pau'} → van comprar al juny i al juliol NO: recuperar-los?
print(compradors_juliol - compradors_juny)   # {'Omar'} → client nou al juliol
print(compradors_juny ^ compradors_juliol)   # {'Marta','Pau','Omar'} → nomes un mes dels dos

Cinc preguntes de negoci, cinc línies, zero bucles. La diferència no és commutativa (a - b != b - a); la unió, la intersecció i la diferència simètrica sí que ho són.

Un altre ús natural: els gèneres de cada llibre com a conjunts, per respondre "quins llibres comparteixen gènere?":

generes_llibre = {
    "L'Odissea": {"classic", "epica"},
    "Hamlet": {"classic", "teatre", "tragedia"},
    "Faust": {"classic", "teatre"},
}
comuns = generes_llibre["Hamlet"] & generes_llibre["Faust"]
print(comuns)     # {'classic', 'teatre'}

Subconjunts i superconjunts

tragedies = {"Hamlet", "Faust"}
classics = {"L'Odissea", "Hamlet", "El Quixot", "Faust"}

print(tragedies <= classics)          # True: tragedies es SUBconjunt de classics
print(tragedies.issubset(classics))   # equivalent amb metode
print(classics >= tragedies)          # True: classics es SUPERconjunt
print(tragedies.isdisjoint({"Iliada"}))  # True: no comparteixen cap element

Útil per a validacions: "tots els títols de la comanda són al catàleg?" és set(comanda) <= set(cataleg) — i sí, set(cataleg) sobre el dict de 04-03 n'extreu les claus.

frozenset: el conjunt immutable

La mateixa relació que tupla/llista: frozenset és un conjunt congelat. No admet add() ni remove(), i a canvi és hashable — pot ser clau de diccionari o element d'un altre conjunt:

PACK_CLASSICS = frozenset({"L'Odissea", "Hamlet"})   # oferta fixa de la botiga

ofertes = {
    PACK_CLASSICS: 19.90,        # un frozenset com a clau de dict: legal
}
print(ofertes[frozenset({"Hamlet", "L'Odissea"})])   # 19.9 → l'ordre no importa

Amb un set normal com a clau obtindries TypeError: unhashable type: 'set'. Ús breu i ocasional, però quan el necessites, res més no el substitueix.

Comprensions de conjunts

Tercera comprensió de la família (llista → dict → set). Sintaxi: claus sense dos punts:

comandes = ["  hamlet ", "FAUST", "Hamlet", "el quixot"]

# Titols normalitzats i unics, de cop (la normalitzacio a fons, a 04-05)
unics = {c.strip().lower() for c in comandes}
print(unics)     # {'hamlet', 'faust', 'el quixot'} → 4 comandes, 3 titols reals

# Sobre el cataleg canonic de 04-03: titols amb estoc
cataleg = {
    "L'Odissea": {"preu": 12.50, "estoc": 4},
    "Hamlet": {"preu": 9.95, "estoc": 6},
    "El Quixot": {"preu": 15.90, "estoc": 8},
    "Faust": {"preu": 21.00, "estoc": 0},
}
disponibles = {t for t, fitxa in cataleg.items() if fitxa["estoc"] > 0}
print(disponibles)   # {"L'Odissea", 'Hamlet', 'El Quixot'}
Comprensió Sintaxi Produeix
De llista [x for x in it] Llista (ordenada, amb duplicats)
De diccionari {k: v for ...} Dict (clau: valor)
De conjunt {x for x in it} Set (únic, sense ordre)

Errors Comuns i Consells

  • {} no és un conjunt buit: és un dict. L'error no dona missatge — simplement parany.add(x) fallarà amb AttributeError més tard. Conjunt buit = set().
  • Esperar ordre: els sets no garanteixen ordre d'iteració. Si imprimeixes un set i "surt desordenat", no és un bug. Per presentar, sorted(el_meu_set).
  • Indexar un set: s[0] llança TypeError. Sense ordre no hi ha posicions; si necessites "el primer", et falta una llista.
  • Ficar-hi elements mutables: {["a", "b"]} llança TypeError: unhashable type: 'list'. Converteix a tupla si necessites agrupar.
  • Confondre remove() amb discard(): si l'absència és normal, discard(); si és un bug, remove() (que falli aviat). El mateix criteri que claudàtors vs get() als dicts.
  • Deduplicar amb set quan l'ordre importa: set(llista) perd l'ordre d'aparició. Idiom que el conserva: list(dict.fromkeys(llista)) — els dicts dedupliquen claus i recorden l'ordre d'inserció.
  • Consell: si et sorprens escrivint bucles imbricats per comparar dues llistes ("els d'aquí que no són allà"), gairebé segur que era una operació de conjunts.

Exercicis

  1. Auditoria de comandes. De la llista comandes = ["Hamlet", "Faust", "Hamlet", "Dracula", "El Quixot", "Faust"], obtén: (a) el conjunt de títols diferents demanats; (b) quantes comandes duplicades hi va haver (total de comandes menys títols diferents); (c) fent servir el catàleg canònic de 04-03 i una diferència de conjunts, quins títols demanats no són al catàleg.
  2. Campanya de fidelització. Amb juny = {"Lluis", "Marta", "Pau", "Julia"} i juliol = {"Lluis", "Julia", "Omar"}, calcula: els socis que cal recuperar (van comprar al juny però no al juliol), els nous del juliol, i els fidels de tots dos mesos. Després comprova amb un operador de subconjunt si tots els fidels van comprar al juny.
  3. Gèneres disponibles. Amb el dict generes_llibre de la lliçó (L'Odissea, Hamlet, Faust) i el conjunt disponibles calculat sobre el catàleg canònic, construeix mitjançant operacions i/o una comprensió el conjunt de gèneres realment disponibles avui (els gèneres dels llibres amb estoc > 0). Pista: la unió de diversos conjunts es pot fer amb set().union(...).

Solucions

# Exercici 1
comandes = ["Hamlet", "Faust", "Hamlet", "Dracula", "El Quixot", "Faust"]
cataleg = {
    "L'Odissea": {"preu": 12.50, "estoc": 4},
    "Hamlet": {"preu": 9.95, "estoc": 6},
    "El Quixot": {"preu": 15.90, "estoc": 8},
    "Faust": {"preu": 21.00, "estoc": 0},
}

diferents = set(comandes)                      # (a) {'Hamlet','Faust','Dracula','El Quixot'}
duplicades = len(comandes) - len(diferents)    # (b) 2
desconeguts = diferents - set(cataleg)         # (c) {'Dracula'} → set(dict) pren les claus
print(diferents, duplicades, desconeguts)
# Exercici 2
juny = {"Lluis", "Marta", "Pau", "Julia"}
juliol = {"Lluis", "Julia", "Omar"}

recuperar = juny - juliol        # {'Marta', 'Pau'}
nous = juliol - juny             # {'Omar'}
fidels = juny & juliol           # {'Lluis', 'Julia'}
print(fidels <= juny)            # True: tot fidel va comprar al juny (per definicio)

Consell: anomena els resultats amb vocabulari de negoci (recuperar, fidels), no matemàtic (diferencia, interseccio): el codi s'explica sol.

# Exercici 3
generes_llibre = {
    "L'Odissea": {"classic", "epica"},
    "Hamlet": {"classic", "teatre", "tragedia"},
    "Faust": {"classic", "teatre"},
}
disponibles = {t for t, fitxa in cataleg.items() if fitxa["estoc"] > 0}

generes_avui = set().union(*(generes_llibre[t] for t in disponibles if t in generes_llibre))
print(generes_avui)   # {'classic', 'epica', 'teatre', 'tragedia'} → Faust (esgotat) no hi aporta res

Fixa't: el * desempaqueta els conjunts com a arguments d'union() (el mateix * de 03-02), i l'if t in generes_llibre protegeix contra llibres sense gèneres fitxats — "El Quixot" està disponible però no té entrada, i sense aquest filtre hi hauria un KeyError.

Conclusió

Els conjunts completen el trio de col·leccions "amb claus": saps crear-los (recordant que set() és l'única forma del buit), aprofitar la deduplicació automàtica, i expressar lògica de negoci amb unió, intersecció i diferències en una línia on abans hauries imbricat bucles. Entens a més per què in és ultraràpid en sets i dicts — la màgia del hash — i coneixes frozenset per a quan la immutabilitat importa. A Papyrus ja dedupliquen comandes, validen socis i comparen mesos de vendes. Però en gairebé tots els exemples ha tret el cap un actor que fem servir des del mòdul 1 sense estudiar-lo seriosament: la cadena de text, amb els seus strip(), lower() i companyia. La propera lliçó la tracta per fi com el que és — una seqüència amb superpoders propis — i sistematitza la normalització d'entrades d'usuari que cercar_llibre() i es_soci_valid() fan a mitges des de fa temps.

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