Les cadenes t'acompanyen des de la primera línia del curs (print("Benvingut a Papyrus")), i mètodes com upper(), lower() o isdigit() han anat apareixent de passada. Ha arribat el moment de tractar-les com es mereixen: com una seqüència immutable de caràcters amb la caixa d'eines més rica de Python. En aquesta lliçó sistematitzem els seus mètodes, espremem les f-strings més enllà del :.2f del mòdul 1, i resolem un problema real de Papyrus: quan en Lluís tecleja " el quixot " al menú, el programa ha d'entendre "El Quixot" — la normalització d'entrades que cercar_llibre() fa a mitges des de fa temps.

Contingut

  1. Les cadenes com a seqüències: indexació, llescat, in
  2. Netejar i trossejar: strip(), split(), join()
  3. Cercar i substituir: replace(), find() vs index(), startswith()/endswith()
  4. Majúscules i minúscules: lower(), casefold(), title(), upper()
  5. Classificar i farcir: isdigit(), isalpha(), isalnum(), zfill()
  6. Immutabilitat i les seves conseqüències pràctiques
  7. F-strings avançades: milers, percentatges, dates i =
  8. Normalització d'entrades a Papyrus
  9. Construir cadenes eficientment: join() vs concatenació en bucle

Les cadenes com a seqüències

Tot el que has après a 04-01 sobre seqüències aplica a les cadenes — cada caràcter és un element:

botiga = "Papyrus"

print(botiga[0])       # P        → indexacio des de 0
print(botiga[-1])      # s        → negativa des del final
print(botiga[:4])      # Papy     → llescat: [inici:fi:pas]
print(botiga[::-1])    # surypaP  → invertir, l'idiom de sempre
print(len(botiga))     # 7
print("pyr" in botiga) # True     → in cerca SUBCADENES, no nomes caracters
for lletra in botiga[:3]:
    print(lletra)      # P, a, p  → iterable com qualsevol sequencia

Diferència clau amb les llistes: in sobre cadenes troba subcadenes completes ("Qui" in "El Quixot" és True), no només elements solts.

Netejar i trossejar: strip(), split(), join()

El trio imprescindible per processar text d'usuaris i de fitxers (en traurem partit de debò al mòdul 6):

entrada = "   El Quixot   "
print(entrada.strip())      # 'El Quixot'  → sense espais als extrems
print(entrada.lstrip())     # 'El Quixot   ' (nomes esquerra) / rstrip(): nomes dreta
print("--Faust--".strip("-"))   # 'Faust' → strip accepta quins caracters pelar

linia = "Faust;21.00;0"
camps = linia.split(";")
print(camps)                # ['Faust', '21.00', '0'] → de cadena a LLISTA

frase = "el quixot de cervantes"
print(frase.split())        # ['el', 'quixot', 'de', 'cervantes'] → sense argument: per espais

titols = ["L'Odissea", "Hamlet", "Faust"]
print(", ".join(titols))    # "L'Odissea, Hamlet, Faust" → de LLISTA a cadena
print(" | ".join(titols))   # "L'Odissea | Hamlet | Faust"

split() i join() són inversos: l'un trosseja, l'altre cus. Fixa't en la sintaxi de join(), que despista al principi: es crida sobre el separador, i rep la llista. Tots els elements han de ser str (converteix abans amb una comprensió: ", ".join(str(p) for p in preus)).

Cercar i substituir

titol = "El Quixot"

print(titol.replace("El", "Don"))     # 'Don Quixot' → retorna una cadena NOVA
print(titol.find("Qui"))              # 3  → index de la primera aparicio
print(titol.find("xyz"))              # -1 → NO trobat: -1, sense error
print(titol.index("Qui"))             # 3  → igual que find...
# titol.index("xyz")                  # ...pero llanca ValueError si no hi es

print(titol.startswith("El "))        # True → comenca per...?
print(titol.endswith("xot"))          # True → acaba en...?
print("banana".count("na"))           # 2   → aparicions d'una subcadena
Mètode Si no ho troba... Fes-lo servir quan...
find(sub) Retorna -1 L'absència és un cas normal
index(sub) Llança ValueError L'absència és un bug que ha de fallar aviat

Et sona el patró? És el mateix criteri que get() vs claudàtors als dicts i discard() vs remove() als sets. Python és consistent en la seva filosofia. Compte amb el clàssic if titol.find("x"): — si la subcadena és a la posició 0, find retorna 0, que és fals. Compara sempre explícitament: if titol.find("x") != -1: (o millor, fes servir in).

Majúscules i minúscules

titol = "el quixot"

print(titol.upper())      # 'EL QUIXOT'
print(titol.lower())      # 'el quixot'
print(titol.title())      # 'El Quixot'  → Primera Lletra De Cada Paraula
print(titol.capitalize()) # 'El quixot'  → nomes la primera de tot

print("STRASSE".lower())     # 'strasse'
print("straße".casefold())   # 'strasse' → casefold es un lower() "agressiu"

casefold() vs lower(): per al català gairebé no es nota, però casefold() està dissenyat específicament per comparar text ignorant majúscules en qualsevol idioma (la ß alemanya es plega a "ss", per exemple). Regla pràctica: per mostrar, lower()/title(); per comparar, casefold(). Cap dels dos no modifica la cadena original — de seguida veurem per què.

Classificar i farcir

print("42".isdigit())      # True  → nomes digits (ho vas fer servir per validar input al M2)
print("4.2".isdigit())     # False → el punt no es un digit
print("Faust".isalpha())   # True  → nomes lletres
print("Faust3".isalnum())  # True  → lletres i/o digits
print("   ".isspace())     # True  → nomes espais en blanc

print("7".zfill(3))        # '007' → farceix amb zeros a l'esquerra
print("LLUIS-1".split("-")[1].zfill(3))   # '001' → normalitzar codis de soci

zfill() és or per a codis i identificadors de longitud fixa: els socis de Papyrus són LLUIS-001, no LLUIS-1. Per farcir amb altres caràcters existeixen ljust(), rjust() i center(), tot i que per presentar en pantalla les f-strings (secció següent) solen ser més clares.

Immutabilitat i les seves conseqüències pràctiques

Com les tuples, les cadenes són immutables. Cap mètode no les modifica: tots retornen una cadena nova.

titol = "el quixot"
titol.upper()                # correcte... pero no es guarda enlloc
print(titol)                 # 'el quixot' → sense canvis: el resultat s'ha perdut!

titol = titol.upper()        # l'idiom correcte: reassignar
print(titol)                 # 'EL QUIXOT'

titol[0] = "x"               # TypeError: no hi ha assignacio per index

Conseqüències pràctiques:

  • Reassigna sempre: s = s.strip().lower(). Cridar un mètode "a l'aire" no fa res de visible.
  • Encadenar mètodes és segur i idiomàtic: cadascun retorna la cadena nova sobre la qual es crida el següent: " EL QUIXOT ".strip().lower().title()'El Quixot'.
  • Les cadenes poden ser claus de dict i elements de set (són hashables) — tot el catàleg de 04-03 en depèn.
  • Concatenar en bucle és car: cada += crea una cadena nova copiant tot l'anterior. La solució, al final de la lliçó.

F-strings avançades

Del mòdul 1 coneixes {x:.2f} i les alineacions {x:<12} / {x:>6}. La minisintaxi de format dona molt més de si:

from datetime import date

recaptacio_anual = 48231.5
quota_socis = 0.62
avui = date(2026, 7, 13)
preu = 15.90

print(f"{recaptacio_anual:,.2f}")      # '48,231.50'  → separador de milers
print(f"{recaptacio_anual:,.2f} EUR".replace(",", "."))  # estil europeu: '48.231.50 EUR'
print(f"{quota_socis:.1%}")            # '62.0%'      → multiplica per 100 i afegeix %
print(f"{avui:%d/%m/%Y}")              # '13/07/2026' → format de data (datetime, 03-05)
print(f"{preu:>8.2f}")                 # '   15.90'   → amplada 8, dreta, 2 decimals
print(f"{'TOTAL':^20}")                # '       TOTAL        ' → centrat en 20
print(f"{preu=}")                      # 'preu=15.9' → el = imprimeix nom I valor: depuracio
Especificador Efecte Exemple
:, Separador de milers f"{48231.5:,.2f}"48,231.50
:.1% Percentatge amb 1 decimal f"{0.62:.1%}"62.0%
:%d/%m/%Y Format de data/hora f"{avui:%d/%m/%Y}"
:>8.2f / :<8 / :^8 Alinear en amplada 8 tiquets alineats
{expr=} Nom i valor alhora f"{estoc=}"estoc=4

El = mereix un amor especial: print(f"{cataleg['Faust']['estoc']=}") et diu què estàs mirant i quant val, sense escriure l'etiqueta dues vegades. És l'eina de depuració ràpida fins que arribi el mòdul 9.

Normalització d'entrades a Papyrus

Ajuntem les peces. El problema: en Lluís tecleja títols com " el quixot ", "HAMLET" o "faust", i el catàleg canònic fa servir claus exactes. La solució: una funció de normalització única i oficial a papyrus_utils.py:

def normalitzar_titol(text):
    """Prepara un titol teclejat per l'usuari per comparar-lo amb el cataleg."""
    return text.strip().casefold()

def cercar_llibre(cataleg, cercat):
    """Retorna la clau canonica del cataleg o None. Versio definitiva."""
    objectiu = normalitzar_titol(cercat)
    for titol in cataleg:
        if normalitzar_titol(titol) == objectiu:
            return titol
    return None

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},
}
print(cercar_llibre(cataleg, "  el quixot "))    # 'El Quixot'
print(cercar_llibre(cataleg, "L'ODISSEA"))       # "L'Odissea" → normalitzada: "l'odissea"
print(cercar_llibre(cataleg, "dracula"))         # None

Dues decisions de disseny que convé subratllar:

  • La normalització viu en una funció: si demà cal treure també els accents, es toca un sol lloc.
  • Es normalitzen les dues parts de la comparació (entrada i clau) — fixa't que la clau "L'Odissea" es converteix en "l'odissea", apòstrof inclòs —, però el catàleg conserva les seves claus boniques per mostrar. Normalitzar només una banda és el bug més habitual.

Construir cadenes eficientment: join() vs concatenació en bucle

Per muntar el tiquet de Papyrus podríem concatenar línia a línia... però cada += copia tota la cadena acumulada (la immutabilitat obliga):

# Versio ingenua: funciona, pero copia i copia i copia
tiquet = ""
for titol, fitxa in cataleg.items():
    tiquet += f"{titol:<12} {fitxa['preu']:>6.2f} EUR\n"   # cadena nova a cada volta

# Versio idiomatica: acumular en LLISTA i cosir una sola vegada al final
linies = [f"— Tiquet de {'Papyrus'} —"]
for titol, fitxa in cataleg.items():
    linies.append(f"{titol:<12} {fitxa['preu']:>6.2f} EUR")
tiquet = "\n".join(linies)
print(tiquet)

Amb 4 llibres la diferència és invisible; amb 100 000 línies d'un fitxer de vendes (mòdul 6), la versió amb join() és ordres de magnitud més ràpida. El patró "acumula en llista, uneix amb join" és dels que distingeixen codi principiant de codi professional.

Errors Comuns i Consells

  • Oblidar reassignar: s.strip() sense s = no fa res de visible. Les cadenes són immutables; tots els mètodes en retornen una de nova.
  • if s.find(x): — retorna 0 (fals) si és al principi i -1 (verdader!) si no hi és. Fes servir in per a pertinença i != -1 si necessites la posició.
  • join() amb no-cadenes: ", ".join([12.5, 9.95]) llança TypeError. Converteix abans: ", ".join(str(x) for x in ...).
  • Normalitzar només una banda de la comparació: entrada.lower() == "El Quixot" mai no serà cert. Normalitza totes dues, amb la mateixa funció.
  • isdigit() no valida floats: "4.2".isdigit() és False. Per a preus teclejats necessitaràs una altra estratègia (el mòdul 7 la resoldrà amb try/except).
  • title() i els connectors: "el quixot de cervantes".title()'El Quixot De Cervantes' — capitalitza també "De" (i darrere d'un apòstrof: "l'odissea".title()"L'Odissea", que aquí quadra de casualitat). Per a títols editorials perfectes caldrà lògica pròpia; no ho donis per resolt.
  • Consell: quan processis text d'usuari, normalitza a la frontera (tan bon punt el reps) i treballa ja sempre amb la forma neta. Evita anar repartint strip() per tot el codi.

Exercicis

  1. Parser d'altes. L'Anna rep altes de llibres com " faust ; 21.00 ; 0 ". Escriu parsejar_alta(linia) que retorni la tupla (titol, preu, estoc) amb el títol en format title(), el preu com a float i l'estoc com a int, tolerant espais al voltant de cada camp. Prova-la i incorpora el resultat al catàleg canònic amb una sola assignació.
  2. Tiquet professional. Genera amb el patró llista + join() un tiquet per a la compra d'en Lluís (1 "El Quixot" i 1 "Hamlet", preus de soci 15.71 i 9.83): capçalera centrada en 24 caràcters amb el nom de la botiga, una línia per llibre (títol a l'esquerra en 14, quantia a la dreta en 8 amb 2 decimals), una línia de separació de guions i el total. Extra: mostra l'estalvi del soci com a percentatge amb :.1% sobre la suma de preus sense descompte (16.54 + 10.35).
  3. Validador de codis de soci. Els codis vàlids tenen el format NOM-NNN (lletres, guió, exactament 3 dígits). Escriu es_soci_valid(codi) que normalitzi (strip().upper()), faci split("-"), i validi amb isalpha(), isdigit() i len(). Ha d'acceptar " lluis-001 " i rebutjar "LLUIS001", "LLUIS-1" i "LL2IS-001".

Solucions

# Exercici 1
def parsejar_alta(linia):
    """De 'titol ; preu ; estoc' (amb espais) a (titol, float, int)."""
    titol, preu, estoc = linia.split(";")           # desempaquetatge de la llista (3 camps!)
    return titol.strip().title(), float(preu.strip()), int(estoc.strip())

titol, preu, estoc = parsejar_alta("  faust ; 21.00 ; 0  ")
cataleg[titol] = {"preu": preu, "estoc": estoc}
print(cataleg["Faust"])    # {'preu': 21.0, 'estoc': 0}

Fixa't: float() i int() ja toleren espais exteriors, però l'strip() explícit documenta la intenció i protegeix el title() del títol.

# Exercici 2
NOM_BOTIGA = "Papyrus"
compra = [("El Quixot", 15.71), ("Hamlet", 9.83)]
sense_descompte = 16.54 + 10.35

linies = [f"{NOM_BOTIGA:^24}"]
total = 0.0
for titol, quantia in compra:
    linies.append(f"{titol:<14}{quantia:>8.2f} EUR")
    total += quantia
linies.append("-" * 24)
linies.append(f"{'TOTAL':<14}{total:>8.2f} EUR")
estalvi = (sense_descompte - total) / sense_descompte
linies.append(f"Estalvi de soci: {estalvi:.1%}")
print("\n".join(linies))
# Exercici 3
def es_soci_valid(codi):
    """Format NOM-NNN: lletres, guio, exactament 3 digits."""
    parts = codi.strip().upper().split("-")
    if len(parts) != 2:
        return False
    nom, numero = parts
    return nom.isalpha() and numero.isdigit() and len(numero) == 3

print(es_soci_valid(" lluis-001 "))    # True
print(es_soci_valid("LLUIS001"))       # False (sense guio)
print(es_soci_valid("LLUIS-1"))        # False (no son 3 digits)
print(es_soci_valid("LL2IS-001"))      # False (el nom no es alfabetic)

Error comú: validar amb len(parts) != 2 després de desempaquetar — el desempaquetatge ja hauria llançat ValueError amb "LLUIS001". Comprova la longitud primer, com aquí.

Conclusió

Les cadenes han passat de companyes silencioses a eina dominada: són seqüències immutables (tot mètode retorna una cadena nova — reassigna sempre), es netegen amb strip(), es trossegen i cusen amb split()/join(), es cerquen amb el criteri find() vs index() que ja coneixies de dicts i sets, i es comparen amb casefold(). Les f-strings avançades (:,, :.1%, dates, =) eleven tiquets i informes de Papyrus a nivell professional, normalitzar_titol() ha deixat cercar_llibre() en la seva versió definitiva, i el patró llista + join() t'estalvia el cost ocult de concatenar en bucle. El mòdul ja té totes les estructures fonamentals; l'última lliçó puja un esglaó: el mòdul collections — la namedtuple promesa a 04-02, Counter, defaultdict, deque — i la taula de criteri definitiva per triar estructura sense dubtar.

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