A la lliçó anterior, les nostres funcions rebien dades de la manera més simple: un valor per posició per a cada paràmetre. Suficient per començar, però curt per a Papyrus: el càlcul del preu final depèn de si el client és soci o no, i no volem dues funcions gairebé idèntiques (preu_normal() i preu_soci()) quan la lògica és la mateixa amb un matís. Python ofereix un sistema d'arguments extraordinàriament flexible: arguments per posició i per nom, valors per defecte, quantitats variables amb *args i **kwargs, i desempaquetatge a la crida. En aquesta lliçó el recorrem sencer, construïm la funció estrella de la botiga — preu_final(base, soci=False, iva=IVA_LLIBRES) — i saldem el deute que arrosseguem des del mòdul 2: entendre de debò com funciona zip().
Contingut
- Arguments posicionals i arguments amb nom (keyword)
- Valors per defecte:
preu_final()per a socis i no socis - La trampa dels valors per defecte mutables
*args: quantitat variable de posicionals**kwargs: quantitat variable d'arguments amb nom- L'ordre dels paràmetres a la definició
- Desempaquetatge amb
*i**a la crida - Retornar diversos valors (tuples, presentació breu)
- Deute saldat:
zip()a fons
Arguments posicionals i arguments amb nom
Fins ara hem aparellat arguments amb paràmetres per posició: el primer amb el primer, el segon amb el segon. Python també permet aparellar-los per nom (arguments keyword):
def etiquetar(titol, preu):
print(f"{titol:<12} {preu:>6.2f} €")
etiquetar("Hamlet", 9.95) # posicionals
etiquetar(titol="Hamlet", preu=9.95) # amb nom
etiquetar(preu=9.95, titol="Hamlet") # amb nom: l'ordre es indiferent
etiquetar("Hamlet", preu=9.95) # barreja: posicionals primer
etiquetar(preu=9.95, "Hamlet") # SyntaxError: posicional despres de keywordRegles i criteri:
- En una crida mixta, els posicionals van sempre abans que els keyword.
- Els arguments amb nom fan la crida autoexplicativa:
preu_final(12.50, True)obliga a mirar la definició per saber què és aquestTrue;preu_final(12.50, soci=True)es llegeix sola. Ja vas fer servir aquesta idea al mòdul 2 ambenumerate(cataleg, start=1)i al mòdul 1 ambprint(..., sep=", ", end=""):start,sepiendsón arguments keyword. - Regla pràctica: per posició les dades òbvies i obligatòries; per nom les opcions i banderes.
Valors per defecte
Un paràmetre pot declarar un valor que es fa servir si la crida no el proporciona. És el mecanisme perfecte per a la funció de preus de Papyrus:
IVA_LLIBRES = 0.04
DESCOMPTE_SOCI = 0.05
def preu_final(base, soci=False, iva=IVA_LLIBRES):
"""Retorna el preu final: descompte de soci (si escau) i despres l'IVA."""
if soci:
base = base * (1 - DESCOMPTE_SOCI)
return round(base * (1 + iva), 2)
print(preu_final(12.50)) # 13.0 -> client normal
print(preu_final(12.50, soci=True)) # 12.35 -> Lluis, soci
print(preu_final(12.50, soci=True, iva=0.21)) # 14.37 -> simulacio d'un altre IVADesglossament:
baseno té valor per defecte: és obligatori. Oblidar-lo produeixTypeError: preu_final() missing 1 required positional argument: 'base'.soci=Falseiiva=IVA_LLIBRESsón opcionals: la crida curtapreu_final(12.50)cobreix el cas més freqüent (client no soci, IVA de llibres) i els casos especials es demanen explícitament.- L'ordre d'operacions respecta la convenció de la botiga des del mòdul 1: primer el descompte, després l'IVA. Els preus de soci del catàleg surten clavats: 12.35, 9.83 i 15.71 € per a L'Odissea, Hamlet i El Quixot.
Un matís que sorprèn: els valors per defecte s'avaluen una sola vegada, quan Python llegeix el def. iva=IVA_LLIBRES pren el valor que IVA_LLIBRES tingui en aquell moment (0.04) i el congela; si més tard reassignessis la constant, el defecte no canviaria. Amb constants i immutables això és inofensiu... però condueix directament a la trampa més famosa de Python.
La trampa dels valors per defecte mutables
Com que el defecte s'avalua una única vegada, fer servir un objecte mutable (una llista, per exemple) com a valor per defecte significa que totes les crides comparteixen el mateix objecte:
def apuntar_comanda(titol, comandes=[]): # PERILL!
comandes.append(titol)
return comandes
print(apuntar_comanda("Faust")) # ['Faust']
print(apuntar_comanda("Hamlet")) # ['Faust', 'Hamlet'] <- i aquest Faust?!La segona comanda "hereta" la primera perquè la llista per defecte és la mateixa a totes dues crides: es va crear en llegir el def i encara hi és, acumulant. L'idioma correcte fa servir None com a sentinella:
def apuntar_comanda(titol, comandes=None):
"""Afegeix el titol a la llista de comandes (o crea una llista nova)."""
if comandes is None:
comandes = [] # llista NOVA a cada crida
comandes.append(titol)
return comandes
print(apuntar_comanda("Faust")) # ['Faust']
print(apuntar_comanda("Hamlet")) # ['Hamlet'] <- correcte| Valor per defecte | Segur? | Exemples |
|---|---|---|
| Immutable | Sí | False, 0, 0.04, "es", None |
| Mutable | No — es comparteix entre crides | [], {} (fes servir el patró None) |
*args: quantitat variable de posicionals
De vegades no saps quants valors arribaran. Un paràmetre amb * al davant recull tots els arguments posicionals sobrants:
def total_compra(*quanties):
"""Retorna la suma de totes les quanties rebudes."""
total = 0.0
for quantia in quanties:
total += quantia
return round(total, 2)
print(total_compra(12.35)) # 12.35 -> Lluis s'emporta un llibre
print(total_compra(12.35, 9.83, 15.71)) # 37.89 -> se n'emporta tres
print(total_compra()) # 0.0 -> avui nomes mirava- Dins de la funció,
quantiesés una tupla amb zero o més valors, i es recorre ambforcom qualsevol seqüència. - El nom
argsés pura convenció (aquíquantiesés més expressiu); el que importa és el*. - Ja coneixies això sense saber-ho:
print("a", "b", "c")accepta qualsevol quantitat d'arguments gràcies a aquest mecanisme.
**kwargs: quantitat variable d'arguments amb nom
El doble asterisc recull els arguments keyword sobrants en un diccionari (parells nom-valor; els diccionaris s'estudien a fons al mòdul 4, aquí només en necessites la idea):
def fitxa_llibre(titol, **dades):
"""Imprimeix una fitxa amb el titol i qualsevol dada extra."""
print(f"Fitxa de '{titol}'")
for camp, valor in dades.items():
print(f" {camp}: {valor}")
fitxa_llibre("Faust", autor="Goethe", any=1808, estoc=0)Cada llibre de Papyrus pot tenir dades diferents (editorial, edició, idioma...) sense canviar la signatura de la funció. dades.items() recorre els parells nom-valor; de nou, el nom kwargs (keyword arguments) és només convenció.
L'ordre dels paràmetres a la definició
Quan combinis diverses categories, l'ordre al def és fix:
| Posició | Categoria | Exemple |
|---|---|---|
| 1r | Posicionals obligatoris | base |
| 2n | Amb valor per defecte | soci=False, iva=IVA_LLIBRES |
| 3r | *args |
*quanties |
| 4t | **kwargs |
**dades |
Saltar-se l'ordre (def f(soci=False, base)) és SyntaxError: parameter without a default follows parameter with a default. A la pràctica, la majoria de les teves funcions faran servir només les dues primeres categories — *args/**kwargs són per a casos genuïnament variables, no un comodí per no pensar la signatura.
Desempaquetatge amb * i ** a la crida
Els asteriscos també funcionen en sentit invers: reparteixen una seqüència o un diccionari entre els paràmetres de la crida:
dades_faust = ["Faust", 21.00]
etiquetar(*dades_faust) # equival a etiquetar("Faust", 21.00)
opcions = {"soci": True, "iva": 0.04}
print(preu_final(21.00, **opcions)) # equival a soci=True, iva=0.04 -> 20.75*sequenciaen una crida desempaqueta els seus elements com a arguments posicionals.**diccionaridesempaqueta parells nom-valor com a arguments keyword (les claus han de coincidir amb noms de paràmetres).- Mnemotècnia: al
def, els asteriscos empaqueten (molts arguments → una variable); a la crida, desempaqueten (una variable → molts arguments).
Retornar diversos valors (presentació breu)
return pot lliurar diversos valors separats per comes; tècnicament retorna una tupla (seqüència immutable que veurem a fons al mòdul 4), i l'habitual és desempaquetar-la en rebre-la — el mateix mecanisme de l'assignació múltiple del mòdul 1:
def resum_preu(base, soci=False):
"""Retorna (preu final, estalvi respecte al preu sense descompte)."""
final = preu_final(base, soci=soci)
sense_descompte = preu_final(base)
return final, round(sense_descompte - final, 2)
preu, estalvi = resum_preu(12.50, soci=True)
print(f"Lluis paga {preu} € i estalvia {estalvi} €") # Lluis paga 12.35 € i estalvia 0.65 €Amb això en tenim prou per ara; les tuples tindran la seva pròpia lliçó (04-02).
Deute saldat: zip() a fons
Al mòdul 2 vam fer servir zip() de passada; ha arribat el moment d'entendre-la. Papyrus guarda les seves dades en tres llistes paral·leles — l'element i de cadascuna descriu el mateix llibre:
zip() rep diverses seqüències i va produint tuples amb els elements aparellats per posició: primer ("L'Odissea", 12.50, 4), després ("Hamlet", 9.95, 6), etc. És com la cremallera (zipper) d'una jaqueta: uneix una dent de cada costat a cada pas.
for titol, preu, estoc in zip(cataleg, preus, estocs):
print(f"{titol:<12} {preu:>6.2f} € estoc {estoc}")Compara-la amb el patró d'índexs que fèiem servir fins ara:
| Amb índexs | Amb zip() |
|
|---|---|---|
| Codi | for i, titol in enumerate(cataleg): ... preus[i] ... estocs[i] |
for titol, preu, estoc in zip(...) |
| Llegibilitat | Regular: preus[i] obliga a rastrejar i |
Alta: cada variable té nom propi |
| Dona l'índex? | Sí | No (combina-la amb enumerate si el necessites) |
| Llistes de longitud diferent? | IndexError si la que indexa és més llarga |
S'atura a la més curta, en silenci |
Detalls importants:
zip()és peresosa: no construeix una llista de tuples en memòria, les va lliurant a mesura que elforles demana. Si vols veure-les totes de cop:list(zip(cataleg, preus))→[("L'Odissea", 12.5), ('Hamlet', 9.95), ('El Quixot', 15.9)]. Conseqüència pràctica: unzip()ja recorregut queda esgotat; crea'n un de nou per recórrer-lo una altra vegada.- Que s'aturi a la seqüència més curta és còmode però perillós: si a
cataleghi afegeixes "Faust" i t'oblides d'afegir-ne el preu, elforsimplement ignorarà el llibre sense avisar. Des de Python 3.10,zip(cataleg, preus, strict=True)llança un error si les longituds no coincideixen — fixa-t'hi:strictés un argument keyword, com els que acabes d'aprendre. zip()també combina de meravella amb les funcions d'aquesta lliçó:
def imprimir_tarifa_socis(titols, bases):
"""Imprimeix cada titol amb el seu preu de soci."""
for titol, base in zip(titols, bases):
print(f"{titol:<12} {preu_final(base, soci=True):>6.2f} €")
imprimir_tarifa_socis(cataleg, preus)Aquestes llistes paral·leles comencen a ser fràgils (tres llistes que cal mantenir sincronitzades a mà); al mòdul 4 coneixeràs estructures que agrupen les dades de cada llibre en un sol lloc. Fins llavors, zip() és la cola oficial de Papyrus.
Errors Comuns i Consells
- Posicional després de keyword a la crida (
preu_final(soci=True, 12.50)):SyntaxError. Primer les posicions, després els noms. - Repetir un argument (
preu_final(12.50, base=12.50)):TypeError: got multiple values for argument 'base'. El valor ha arribat per posició i per nom alhora. []o{}com a valor per defecte: la trampa estrella. Fes servirNonei crea l'objecte a dins. Els linters (com els que suggereix VS Code) ho marquen; fes-los cas.- Passar banderes per posició (
preu_final(12.50, True)): funciona, però d'aquí a un mes ningú no recordarà què eraTrue. Escriusoci=True. - Confondre
*aldefamb*a la crida: a la definició empaqueta; a la crida desempaqueta. Si dubtes, pregunta't a quin costat del parèntesi ets. - Abusar de
*args/**kwargs: una signatura explícita documenta i valida sola. Reserva els asteriscos per a funcions genuïnament variàdiques (comtotal_compra). - Oblidar que
zip()calla amb longituds diferents: en dades que han d'anar aparellades sí o sí, fes servirstrict=True(Python ≥ 3.10). - Consell: en dissenyar una signatura, ordena de més a menys important i posa defectes a tot allò que tingui un valor "normal". La crida típica hauria de ser la més curta.
Exercicis
Exercici 1: el tiquet variàdic
Escriu tiquet(client, *quanties, soci=False) que imprimeixi una línia Tiquet de <client>, després cada quantia passada per preu_final(quantia, soci=soci) (una per línia, format :>6.2f), i al final Total: X.XX € amb la suma dels preus finals. Prova-la amb tiquet("Lluis", 12.50, 9.95, 15.90, soci=True) — el total ha de ser 37.89 € — i amb tiquet("Marta", 21.00).
Exercici 2: reposar sense ensurts
L'Anna va escriure aquesta funció per apuntar reposicions pendents i alguna cosa va malament:
Executa apuntar_reposicio("Faust") i després apuntar_reposicio("Hamlet"), observa el problema, explica per què passa i corregeix-la amb el patró del sentinella None.
Exercici 3: tarifa completa amb zip()
Amb cataleg, preus i estocs (més "Faust", 21.00, 0 afegits: fes servir cataleg + ["Faust"], etc., per no modificar les originals), imprimeix amb un sol bucle sobre zip() una taula títol | preu normal | preu soci | estat, on l'estat sigui esgotat si l'estoc és 0 i disponible en cas contrari. Fes servir preu_final() per a tots dos preus.
Solucions
Exercici 1:
def tiquet(client, *quanties, soci=False):
"""Imprimeix el tiquet d'una compra amb preus finals."""
print(f"Tiquet de {client}")
total = 0.0
for quantia in quanties:
final = preu_final(quantia, soci=soci)
total += final
print(f"{final:>6.2f} €")
print(f"Total: {round(total, 2)} €")
tiquet("Lluis", 12.50, 9.95, 15.90, soci=True) # 12.35 / 9.83 / 15.71, Total: 37.89 €
tiquet("Marta", 21.00) # 21.84, Total: 21.84 €Com que soci va després de *quanties, només es pot passar per nom — Python no el confondrà mai amb una quantia més. És un truc de disseny molt utilitzat.
Exercici 2: La segona crida retorna ['Faust', 'Hamlet']: totes dues comparteixen la mateixa llista per defecte, creada una sola vegada en llegir el def. Correcció:
def apuntar_reposicio(titol, pendents=None):
"""Afegeix un titol a la llista de reposicions (nova si no es passa)."""
if pendents is None:
pendents = []
pendents.append(titol)
return pendentsAra cada crida sense llista rep una llista acabada de crear, i qui ja tingui una llista pròpia pot passar-la: apuntar_reposicio("Hamlet", pendents=la_meva_llista).
Exercici 3:
titols = cataleg + ["Faust"]
bases = preus + [21.00]
unitats = estocs + [0]
print(f"{'Titol':<12} {'Normal':>8} {'Soci':>8} Estat")
for titol, base, estoc in zip(titols, bases, unitats):
estat = "esgotat" if estoc == 0 else "disponible"
print(f"{titol:<12} {preu_final(base):>6.2f} € {preu_final(base, soci=True):>6.2f} € {estat}")Cada variable del for rep el seu element aparellat per zip(); el ternari del mòdul 2 resol l'estat en una línia. Sortida: Faust apareix amb 21.84 € / 20.75 € i esgotat.
Conclusió
Les funcions de Papyrus ja parlen un llenguatge ric: arguments posicionals per a l'essencial, keyword per a les opcions (soci=True), valors per defecte per al cas comú — esquivant la trampa dels mutables —, *args/**kwargs per al que és variable i desempaquetatge amb */** a les crides. A més, zip() ha deixat de ser màgia: és la cremallera que recorre en paral·lel les llistes de la botiga, i preu_final(base, soci=False, iva=IVA_LLIBRES) queda establerta com la funció oficial de preus. A la propera lliçó coneixerem una forma abreujada d'escriure funcions diminutes i anònimes — les lambdes — i les posarem a treballar allà on de debò brillen: com a criteri de sorted(), min() i max() per, per fi, ordenar el catàleg de Papyrus per preu.
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
