En tancar el mòdul 2 vam deixar un diagnòstic clar: el codi de Papyrus funciona, però viu dispers. La cerca de llibres és dins de menu.py, el càlcul del preu de soci es repeteix a venda_soci.py i a tiquet.py, i cada millora obliga l'Anna a tocar diversos scripts. La solució promesa són les funcions: blocs de codi amb nom propi que defineixes una vegada i executes tantes vegades com vulguis. I aquí va la bona notícia: en fas servir des del primer dia — print(), len(), input(), enumerate() són funcions que ja venien fetes. En aquesta lliçó aprendràs a crear les teves amb def, a retornar resultats amb return, a documentar-les amb docstrings i a entendre on "viuen" les seves variables. Com a projecte, començarem la reorganització de menu.py extraient-ne les dues primeres peces: mostrar_cataleg() i cercar_llibre().

Contingut

  1. Què és una funció i per què fer-la servir
  2. Definir amb def i cridar la funció
  3. Paràmetres: dades d'entrada
  4. return: retornar resultats (i el cas None)
  5. Docstrings: documentar la funció
  6. Àmbit de les variables: local davant de global
  7. Projecte: primera refactorització de menu.py

Què és una funció i per què fer-la servir

Una funció és un bloc de codi amb nom, que opcionalment rep dades d'entrada (paràmetres) i opcionalment retorna un resultat. Pensa en la fotocopiadora de Papyrus: l'Anna no reconstrueix la màquina cada vegada que necessita una còpia; hi posa un original (entrada), prem un botó (crida) i recull la còpia (resultat).

Les tres raons clàssiques per fer servir funcions:

Motiu Sense funcions Amb funcions
Reutilització El càlcul del preu de soci està copiat a 3 scripts S'escriu una vegada i es crida des d'on calgui
Manteniment Canviar l'IVA obliga a editar cada còpia (i oblidar-ne una = bug) Es canvia en un únic lloc
Llegibilitat menu.py és un bloc de 35 línies que cal llegir sencer mostrar_cataleg() s'explica sola pel seu nom

Definir amb def i cridar la funció

La sintaxi mínima:

def saludar():
    print("Benvinguda a la gestio de Papyrus")

saludar()
saludar()

Desglossament línia a línia:

  • def (de define) inicia la definició.
  • saludar és el nom: les mateixes regles que les variables (snake_case, PEP 8). Sol ser un verb, perquè una funció fa alguna cosa.
  • Els parèntesis () contindran els paràmetres (aquí, cap) i els dos punts : obren el bloc.
  • El cos va indentat, exactament igual que en un if o un for.
  • saludar() — amb parèntesis — crida (executa) la funció. Sense parèntesis, saludar només anomena la funció sense executar-la; és un error silenciós molt habitual.

Un detall important: definir no és executar. Python llegeix el def, memoritza el bloc sota aquest nom i continua endavant. El cos només corre quan algú crida la funció. Per això aquest script imprimeix en un ordre que sorprèn els principiants:

def acomiadar():
    print("2. Dins de la funcio")

print("1. Abans de cridar")
acomiadar()
print("3. Despres de cridar")
1. Abans de cridar
2. Dins de la funcio
3. Despres de cridar
sequenceDiagram
    participant S as Script principal
    participant F as acomiadar()
    S->>S: print("1. Abans de cridar")
    S->>F: crida acomiadar()
    F->>F: print("2. Dins de la funcio")
    F-->>S: fi de la funció (torna al punt de la crida)
    S->>S: print("3. Despres de cridar")

Paràmetres: dades d'entrada

Una funció sense entrades sempre fa el mateix. Els paràmetres la fan flexible:

def etiquetar(titol, preu):
    print(f"{titol:<12} {preu:>6.2f} €")

etiquetar("Hamlet", 9.95)
etiquetar("Faust", 21.00)
  • titol i preu són paràmetres: noms que la funció fa servir internament.
  • "Hamlet" i 9.95 són arguments: els valors concrets que envies a la crida. El primer va al primer paràmetre, el segon al segon (per posició).
  • A cada crida, els paràmetres neixen amb els valors rebuts, la funció treballa i en acabar desapareixen.

De moment en tenim prou amb aquesta forma bàsica; la lliçó 03-02 està dedicada per complet als tipus d'arguments (amb nom, valors per defecte, quantitats variables...).

return: retornar resultats (i el cas None)

print() mostra, però no lliura. Si vols fer servir el resultat d'una funció en un càlcul posterior, necessites return:

def preu_soci(base):
    """Preu per a socis: descompte del 5% i despres IVA del 4%."""
    final = base * (1 - 0.05) * (1 + 0.04)
    return round(final, 2)

preu_hamlet = preu_soci(9.95)
print(f"Lluis paga {preu_hamlet} €")          # Lluis paga 9.83 €
print(f"Dos exemplars: {preu_soci(9.95) * 2} €")

Claus de return:

  • Lliura un valor al punt de la crida: l'expressió preu_soci(9.95) val 9.83, i pots assignar-la, imprimir-la o operar-hi.
  • Acaba la funció immediatament: el codi posterior al return executat no corre mai. Això el converteix en la sortida d'emergència perfecta — recorda el consell del mòdul 2: allà on break només surt del bucle més intern, return surt de la funció sencera, bucles imbricats inclosos.
  • Una funció pot tenir diversos return (per exemple, un dins d'un if i un altre al final); a cada crida se n'executa com a molt un.

I si una funció no té return, o escriu return a seques? Retorna None, el "valor buit" que vas conèixer al mòdul 1:

def avisar_esgotat(titol):
    print(f"'{titol}' esta esgotat.")

resultat = avisar_esgotat("Faust")
print(resultat)        # None

La taula que evita el 90 % de les confusions inicials:

print(x) dins de la funció return x
Mostra res per pantalla? No (llevat que qui crida ho imprimeixi)
La crida té un valor útil? No (val None) Sí (val x)
Es pot continuar operant amb el resultat? No
Ús típic Informar l'usuari Calcular per a un altre codi

Regla pràctica per a Papyrus: les funcions que calculen (preus, cerques) han de retornar amb return; les que presenten (mostrar el catàleg) poden limitar-se a print() i retornar None.

Docstrings: documentar la funció

La primera línia del cos, si és una cadena, es converteix en la docstring: la documentació oficial de la funció. S'escriu entre cometes triples:

def preu_soci(base):
    """Retorna el preu final per a socis de Papyrus.

    Aplica el descompte de soci (5%) i despres l'IVA de llibres (4%),
    arrodonint a 2 decimals.
    """
    return round(base * (1 - 0.05) * (1 + 0.04), 2)

help(preu_soci)      # mostra la docstring al REPL
  • No és un comentari decoratiu: help(), VS Code (en passar el ratolí per sobre de la crida) i les eines de documentació la llegeixen.
  • Convenció: primera línia en forma de resum que càpiga en una línia; si cal més detall, línia en blanc i paràgraf explicatiu.
  • Els comentaris # expliquen com funciona un fragment per dins; la docstring explica què fa la funció i com fer-la servir des de fora.

Àmbit de les variables: local davant de global

On "viu" una variable? Depèn d'on s'assigni:

NOM_BOTIGA = "Papyrus"          # ambit global (nivell de l'script)

def crear_tiquet():
    linia = f"Tiquet de {NOM_BOTIGA}"   # linia: ambit LOCAL
    return linia

print(crear_tiquet())   # Tiquet de Papyrus
print(linia)            # NameError: name 'linia' is not defined
  • linia és local: neix en cridar la funció i mor en acabar. A fora, no existeix. Això és un avantatge enorme: dues funcions poden fer servir linia, i o total sense trepitjar-se.
  • NOM_BOTIGA és global: es va assignar al nivell de l'script, i les funcions poden llegir-la sense fer res d'especial. Python, en no trobar NOM_BOTIGA a l'àmbit local, la busca "un pis més amunt".

El matís delicat arriba en intentar modificar una global dins d'una funció:

caixa = 0.0

def cobrar(quantia):
    caixa = caixa + quantia   # UnboundLocalError

En veure l'assignació caixa = ..., Python decideix que caixa és local a cobrar... i llavors caixa + quantia intenta llegir una local que encara no té valor. Existeix una paraula clau que ho "arregla":

def cobrar(quantia):
    global caixa             # funciona, pero es mala idea
    caixa = caixa + quantia

global caixa diu a Python "quan assigni caixa, fes servir la global". Funciona, però convé evitar-la: una funció que modifica globals té efectes invisibles des de la crida — llegint cobrar(12.35) ningú no sospita que caixa ha canviat, i amb diverses funcions tocant les mateixes globals el programa es torna indepurable. L'alternativa neta és rebre i retornar:

def cobrar(caixa, quantia):
    """Retorna el nou total de caixa despres de cobrar la quantia."""
    return caixa + quantia

caixa = 0.0
caixa = cobrar(caixa, 12.35)   # el canvi es VEU al punt de la crida

Resum de regles d'àmbit:

Operació dins de la funció Necessita res d'especial? Recomanable?
Llegir una global (constants com IVA_LLIBRES) No Sí — és l'ús natural de les constants
Assignar una variable nova No (serà local)
Modificar una global Sí, global nom No — rep la dada com a paràmetre i retorna el resultat

Projecte: primera refactorització de menu.py

Complim la promesa del mòdul 2. Refactoritzar és reorganitzar codi sense canviar el que fa, i menu.py ho demana a crits: extraurem l'opció 1 a mostrar_cataleg() i el cor de l'opció 2 a cercar_llibre(). A la carpeta papyrus (amb el .venv activat), la nova versió:

NOM_BOTIGA = "Papyrus"
cataleg = ["L'Odissea", "Hamlet", "El Quixot"]
preus = [12.50, 9.95, 15.90]
estocs = [4, 6, 8]


def mostrar_cataleg():
    """Imprimeix la taula de titols, preus i estoc."""
    print(f"\n{'Titol':<12} {'Preu':>8} {'Estoc':>6}")
    print("-" * 28)
    for i, titol in enumerate(cataleg):
        print(f"{titol:<12} {preus[i]:>6.2f} € {estocs[i]:>6}")


def cercar_llibre(cercat):
    """Retorna l'index del llibre amb titol coincident, o None si no hi es."""
    for i, titol in enumerate(cataleg):
        if titol.lower() == cercat.lower():
            return i
    return None


print(f"Benvinguda a la gestio de {NOM_BOTIGA}")

while True:
    print()
    print("1. Veure cataleg")
    print("2. Cercar un llibre")
    print("3. Sortir")
    opcio = input("Tria una opcio (1-3): ")

    match opcio:
        case "1":
            mostrar_cataleg()
        case "2":
            cercat = input("Titol a cercar: ")
            if not cercat:
                print("No has escrit res.")
                continue
            index = cercar_llibre(cercat)
            if index is None:
                print(f"'{cercat}' no es al cataleg.")
            else:
                print(f"'{cataleg[index]}': {preus[index]:.2f} €, estoc {estocs[index]}.")
        case "3":
            print(f"Tancant {NOM_BOTIGA}. Fins dema, Anna!")
            break
        case _:
            print("Opcio no valida. Tria 1, 2 o 3.")

Fixa't en les decisions de disseny:

  1. mostrar_cataleg() presenta: imprimeix i retorna None; el while queda llegible — l'opció 1 és ara una sola línia que es llegeix com una frase.
  2. cercar_llibre() calcula: substitueix el patró for/break/else de la versió anterior per una cosa encara més directa — return i talla la cerca tan bon punt hi ha coincidència (fa el paper del break) i el return None final cobreix el cas "no trobat" (el paper de l'else del bucle). Retornar l'índex en comptes d'imprimir permet que qui crida decideixi què fer-ne; ens ho agrairem a la lliçó 03-04.
  3. if index is None distingeix "no trobat" d'un resultat vàlid; compte, if not index seria un bug, perquè l'índex 0 (L'Odissea) també és falsy — truthiness del mòdul 2 aplicada.
  4. Les funcions llegeixen les globals cataleg, preus i estocs però no les modifiquen: ús legítim de l'àmbit global. Quan extraiem aquestes funcions a un mòdul propi (03-04) veuràs com eliminar també aquesta dependència.

El comportament del menú és idèntic al del mòdul 2 — aquesta és la gràcia de refactoritzar — però ara la cerca és reutilitzable: tiquet.py o venda_soci.py podran fer servir cercar_llibre() tan bon punt sapiguem compartir codi entre fitxers (lliçó 03-04).

Errors Comuns i Consells

  • Cridar sense parèntesis: mostrar_cataleg (sense ()) no executa res ni dona error; simplement anomena la funció. Si "la funció no fa res", revisa els parèntesis.
  • Fer servir la funció abans de definir-la: Python llegeix el fitxer de dalt a baix; cridar cercar_llibre() en una línia anterior al seu def produeix NameError. Convenció pràctica: constants i dades a dalt, després els def, i el programa principal al final.
  • Confondre print amb return: si total = preu_soci(9.95) et dona None, gairebé segur que la funció imprimeix en comptes de retornar. Repassa la taula comparativa d'aquesta lliçó.
  • Codi després del return: qualsevol línia després d'un return que sempre s'executa és codi mort. VS Code l'atenua visualment; esborra'l o replanteja la lògica.
  • Abusar de global: si et descobreixes escrivint global en diverses funcions, el disseny coixeja. Passa les dades com a paràmetres i retorna els resultats.
  • Oblidar la docstring: per a funcions de projecte (les que altres —o tu d'aquí a un mes— reutilitzaran), una línia de docstring és el mínim educat.
  • Consell: una funció ha de fer una cosa. Si en descriure-la necessites la paraula "i" ("mostra el catàleg i demana un llibre i el cerca"), probablement són dues o tres funcions.

Exercicis

Exercici 1: fitxa amb i sense return

Escriu dues funcions: hi_ha_estoc(unitats), que rebi un nombre d'unitats i retorni True si és més gran que 0 i False en cas contrari; i mostrar_disponibilitat(titol, unitats), que faci servir la primera per imprimir '<titol>': disponible o '<titol>': esgotat. Prova-les amb Hamlet (6 unitats) i Faust (0 unitats). Afegeix docstring a totes dues.

Exercici 2: comptar esgotats sense tocar globals

Donada la llista global estocs = [4, 6, 8, 0], escriu comptar_esgotats(llista_estocs) que rebi una llista d'estocs per paràmetre i retorni quants valen 0 (patró comptador del mòdul 2). Crida-la amb estocs i imprimeix Llibres esgotats: 1. Comprova que la funció funciona igual amb [0, 0, 3]. Per què és millor rebre la llista com a paràmetre que llegir la global directament?

Exercici 3: el detectiu de l'àmbit

Sense executar-lo, prediu què imprimeix aquest script i per què. Després executa'l i comprova la teva hipòtesi:

missatge = "caixa tancada"

def obrir_caixa():
    missatge = "caixa oberta"
    return missatge

print(obrir_caixa())
print(missatge)

Solucions

Exercici 1:

def hi_ha_estoc(unitats):
    """Retorna True si queden unitats disponibles."""
    return unitats > 0

def mostrar_disponibilitat(titol, unitats):
    """Imprimeix si un titol esta disponible o esgotat."""
    if hi_ha_estoc(unitats):
        print(f"'{titol}': disponible")
    else:
        print(f"'{titol}': esgotat")

mostrar_disponibilitat("Hamlet", 6)   # 'Hamlet': disponible
mostrar_disponibilitat("Faust", 0)    # 'Faust': esgotat

Observa que return unitats > 0 retorna directament el booleà de la comparació: no cal if unitats > 0: return True else: return False. I una funció pot cridar-ne una altra: és la base de construir programes per capes.

Exercici 2:

estocs = [4, 6, 8, 0]

def comptar_esgotats(llista_estocs):
    """Retorna quants elements de la llista valen 0."""
    esgotats = 0
    for unitats in llista_estocs:
        if unitats == 0:
            esgotats += 1
    return esgotats

print(f"Llibres esgotats: {comptar_esgotats(estocs)}")    # 1
print(comptar_esgotats([0, 0, 3]))                        # 2

Rebre la llista com a paràmetre fa la funció reutilitzable amb qualsevol llista (la de Papyrus avui, la d'una altra botiga demà, una llista de prova en un test) i deixa clar a la crida quines dades fa servir. La variable esgotats és local: no embruta l'script.

Exercici 3: Imprimeix caixa oberta i després caixa tancada. L'assignació missatge = "caixa oberta" dins de la funció crea una variable local que amaga la global mentre la funció corre; la global no canvia mai. Perquè la funció modifiqués la global caldria global missatge — que, com hem vist, és millor evitar retornant el valor (just el que fa el return).

Conclusió

Ja saps crear funcions amb def, distingir paràmetres (entrada) de return (sortida), documentar-les amb docstrings i raonar sobre l'àmbit de les seves variables: locals que neixen i moren amb cada crida, globals que es poden llegir (constants) però que no convé modificar amb global. Amb això, menu.py ha fet el primer pas de "script dispers" a "programa organitzat": mostrar_cataleg() i cercar_llibre() tenen nom, docstring i una única responsabilitat. Però les nostres funcions encara són rígides: cercar_llibre() rep un únic argument posicional i no hi ha manera de donar-li opcions. A la lliçó següent explorarem a fons els arguments de funció — amb nom, valors per defecte, quantitats variables amb *args i **kwargs — i saldarem un deute pendent del mòdul 2: entendre de debò zip(), l'eina que recorre en paral·lel cataleg, preus i estocs.

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