Tancàvem el mòdul 3 amb una frase: «saps construir la maquinària d'un programa, i ara toca aprendre a organitzar-la». Aquest és aquell moment. TascaFàcil v0.6 funciona —la Marta pot registrar una tasca, consultar-la, canviar-li la prioritat i completar-la sense tornar a llançar el programa—, però el fitxer tascafacil.py s'ha convertit en un mur de gairebé cent línies seguides on if not hi_ha_tasca: apareix tres vegades i quatre bucles de validació es repeteixen gairebé paraula per paraula.
Aquest mur no és un problema estètic, sinó pràctic: quan alguna cosa falla, no saps on mirar; quan vols canviar el missatge d'error de la prioritat, has de recordar canviar-lo als dos llocs on està escrit; i quan vols afegir una opció al menú, has d'obrir-te pas entre el codi de les cinc anteriors.
L'eina que resol tot això es diu funció. És, probablement, la idea més rendible de tot el curs: la que converteix un guió inmanejable en un programa que es pot llegir, provar i ampliar per parts. En aquesta lliçó aprendràs què és una funció, com es defineix, com es crida i què passa exactament quan la crides.
Contingut
- El problema: codi repetit i sense nom
- Què és una funció
- Sintaxi:
def, cos indentat i crida - Definir i cridar no són el mateix
- Ordre de definició i execució: el
NameError - El flux d'una crida, pas a pas
- Procediments i funcions que calculen:
None - Docstring i convencions de nom
- TascaFàcil: les tres primeres funcions
- Errors comuns i consells
- Exercicis
- Conclusió
- El problema: codi repetit i sense nom
Mira aquests dos fragments, extrets tal qual de TascaFàcil v0.6. Un és a l'opció 1 i l'altre a l'opció 3:
# A l'opcio 1 (registrar tasca)
prioritat = input("Prioritat : ").strip().lower()
while prioritat not in PRIORITATS:
prioritat = input("No valida. Prioritat: ").strip().lower()
# A l'opcio 3 (canviar prioritat)
nova = input("Nova prioritat (alta/mitjana/baixa): ").strip().lower()
while nova not in PRIORITATS:
nova = input("No valida. Prioritat: ").strip().lower()Són el mateix algorisme escrit dues vegades, i això comporta tres conseqüències desagradables:
- Manteniment doble. Si demà l'estudi afegeix la prioritat
critica, cal tocar els dos llocs. I el dia que només en toquis un, tindràs un programa que es comporta de manera diferent segons per on hi entris: un error especialment difícil de trobar. - Soroll. En llegir l'opció 1 no hi veus «es demana la prioritat»: hi veus tres línies de mecànica amb
input,strip,loweri unwhile. El què queda enterrat sota el com. - Impossibilitat de provar per parts. No pots comprovar «funciona bé la meva lectura de prioritats?» sense executar l'aplicació sencera, navegar fins a l'opció 1 i teclejar a mà.
El que falta és poder donar nom a un tros de codi i fer servir aquest nom quan el necessitis. Això és una funció.
- Què és una funció
Una funció és un bloc de codi amb nom que s'escriu una vegada i s'executa tantes vegades com calgui, simplement esmentant el seu nom.
Ja has fet servir funcions des de la primera lliçó de Python: print(), input(), len(), int() i type() són funcions integrades (built-in). Pensa en el que et donen: no tens ni idea de com print aconsegueix que unes lletres apareguin a la pantalla —hi ha allà dins una feina considerable amb el sistema operatiu—, i tanmateix la fas servir sense esforç. És l'abstracció de la lliçó 01-02, aplicada ara al teu propi codi. Una funció ben feta et dona quatre coses:
| Benefici | Què significa a la pràctica |
|---|---|
| Reutilització | Escrius l'algorisme una vegada i l'invoques des de deu llocs |
| Nom per a una idea | demanar_prioritat() es llegeix de cop; tres línies de while cal desxifrar-les |
| Verificació per parts | Pots provar una funció sola, sense arrencar tota l'aplicació |
| Canvi localitzat | Corregeixes l'algorisme en un únic lloc i es corregeix a tots |
Dels quatre, el segon és el que més subestimen els principiants. Una funció no és només un estalvi de teclejar: és una paraula nova en el vocabulari del teu programa. Quan escrius mostrar_menu(), deixes de pensar en print i comences a pensar en menús. Programar bé consisteix, en bona mesura, a construir-se un vocabulari a l'altura del problema.
- Sintaxi:
def, cos indentat i crida
def, cos indentat i cridaDefinir una funció en Python són dues parts: la capçalera i el cos.
def saludar_equip():
print("Bon dia, equip d'Estudi Alba.")
print("Avui revisem les tasques pendents.")Desmenucem la capçalera, perquè cada símbol hi té el seu paper:
defés la paraula clau que anuncia «vaig a definir una funció». Ve de l'anglès define.saludar_equipés el nom. Es tria igual que un nom de variable:snake_case, sense accents ni espais, descriptiu.()són els parèntesis, obligatoris encara que estiguin buits. A dins hi aniran els paràmetres, tema de la lliçó vinent; avui sempre hi aniran buits.:els dos punts tanquen la capçalera i obren el bloc, igual que en unifo unwhile.
El cos és tot allò indentat a sota (quatre espais, com sempre) i acaba on acaba la indentació: la primera línia que torni al marge esquerre ja és fora de la funció. Un cop definida, la funció es crida escrivint el seu nom seguit de parèntesis. Si afegeixes això sota el bloc anterior:
Bon dia, equip d'Estudi Alba. Avui revisem les tasques pendents. Bon dia, equip d'Estudi Alba. Avui revisem les tasques pendents.
Dues línies de crida, dues execucions del cos. I fixa't que la definició no imprimeix res per si sola: sense aquestes dues crides, el programa s'executa sense errors i sense cap sortida.
- Definir i cridar no són el mateix
Aquesta distinció és la que més s'entravessa al principi. Definir és escriure la recepta: Python llegeix la capçalera, desa el cos a la memòria sota aquell nom i tira endavant, sense executar res. Cridar és cuinar la recepta: Python va a buscar el cos desat i l'executa ara.
| Definició | Crida | |
|---|---|---|
| Sintaxi | def pausar(): + cos |
pausar() |
| Quantes vegades s'escriu | Una | Les que calguin |
| Executa el cos? | No | Sí |
| Quan passa | En llegir el fitxer | Al punt exacte de la crida |
L'error clàssic és oblidar els parèntesis en cridar:
def pausar():
input("Prem Intro per continuar...")
pausar # <-- sense parentesis: NO crida res
pausar() # <-- amb parentesis: si que la cridaLa línia pausar sense parèntesis és vàlida en Python i no dona error. Simplement avalua el nom i obté l'objecte funció, igual que escriure x tot sol avalua la variable x; el resultat es descarta i el programa continua. Al REPL es veu amb claredat:
Ja ho tens: sense parèntesis, pausar és un valor de tipus function; amb parèntesis, és una ordre d'execució. Això sembla un parany avui, però és una de les propietats més potents de Python i a Funcions com a valors la aprofitarem a fons. Símptoma típic en un programa real: l'executes, no falla, i senzillament no passa res on esperaves veure alguna cosa. Mira els parèntesis abans que res.
- Ordre de definició i execució: el
NameError
NameErrorPython executa el fitxer de dalt a baix. Quan arriba a un def, registra el nom; quan arriba a una crida, busca aquest nom. Per tant: una funció ha d'estar definida abans de la línia que la crida.
mostrar_capcalera() # ERROR: encara no existeix
def mostrar_capcalera():
print("TascaFacil - Estudi Alba")Un NameError significa exactament això: has fet servir un nom que Python no té registrat. Les tres causes habituals són que l'hagis escrit malament (mostar_capcalera), que el facis servir abans de definir-lo o —ho veurem a Àmbit de variables— que existeixi però no sigui visible des d'on ets.
Matís important: el que ha d'anar abans és la crida, no una altra definició. Una funció pot esmentar en el seu cos una altra que es defineix més avall, perquè el cos no s'executa fins que es crida:
def preparar_pantalla():
mostrar_capcalera() # es resol en EXECUTAR, no en definir
def mostrar_capcalera():
print("TascaFacil - Estudi Alba")
preparar_pantalla() # aqui ja existeixen totes dues: funcionaPer això l'estructura habitual d'un fitxer Python és: constants a dalt, després totes les definicions, i el codi que les posa en marxa al final. A Descompondre un programa en funcions formalitzarem aquesta estructura.
- El flux d'una crida, pas a pas
Quan Python troba una crida passen cinc coses en ordre: s'atura l'execució en aquella línia; s'anota el punt de retorn, el lloc exacte on caldrà tornar; se salta al cos i s'executa sencer, de dalt a baix; en acabar es torna al punt anotat; i es continua amb la línia següent a la crida. Aquest registre d'«on érem» s'anomena pila de crides (call stack): cada crida hi apila un marc i cada retorn el desapila.
flowchart TD
A["Principal: linia 1"] --> B["Crida a prendre_nota()"]
B --> C["Cos de prendre_nota"]
C --> D["Fi del cos: retorn"]
D --> E["Principal: linia seguent"]
Comprova-ho amb un exemple traçable:
def prendre_nota():
print("2. Entro a la funcio")
print("3. Surto de la funcio")
print("1. Abans de la crida")
prendre_nota()
print("4. Despres de la crida")Els números surten en ordre perquè el programa principal espera: no executa l'última línia fins que la funció ha acabat del tot. És la «prova d'escriptori» de 01-05, ara amb salts. I si una funció en crida una altra, la pila creix: el principal espera la primera, que al seu torn espera la segona. Aquesta capacitat d'imbricar crides sosté programes de qualsevol mida, i portada a l'extrem —una funció que es crida a si mateixa— dona lloc a la recursivitat.
- Procediments i funcions que calculen:
None
NoneHi ha dues maneres que una funció sigui útil. Les funcions que fan —els clàssics procediments— valen pel efecte que produeixen: imprimir a la pantalla, demanar una dada, esperar una tecla. Les funcions que calculen valen pel resultat que lliuren a qui les ha cridat, perquè aquest decideixi què en fa. Avui n'escriurem només del primer tipus; el return que fa possible el segon arriba a la lliçó següent. Però hi ha un detall que has de veure ja:
def mostrar_capcalera():
print("TascaFacil - Estudi Alba")
resultat = mostrar_capcalera()
print(resultat, type(resultat))Llegeix-ho amb atenció. La funció ha imprès el seu text, però el que ha retornat ha estat None. En Python, tota funció que acaba sense retornar res explícitament retorna None, el valor «absència de valor» que vas conèixer a 02-01. No hi ha funcions que no retornin res: hi ha funcions que retornen None. D'aquí surt un error molt freqüent:
print mostra el text; no el retorna, així que desar-ne el resultat no serveix de res. Imprimir i retornar són operacions diferents, i confondre-les és de les coses que més costa desaprendre.
- Docstring i convencions de nom
Si la primera línia del cos d'una funció és una cadena solta, Python la tracta de manera especial: la desa com a documentació. Es diu docstring i s'escriu amb cometes triples.
def mostrar_capcalera():
"""Pinta el titol de l'aplicacio a la pantalla."""
print("=" * 46)
print(f"{'TascaFacil - Estudi Alba':^46}")
print("=" * 46)Amb això hi guanyes dues coses: l'editor et mostra aquesta frase en escriure la crida, i la funció queda autoexplicada per a qui la llegeixi d'aquí a sis mesos (que gairebé sempre ets tu). Des del REPL, help(mostrar_capcalera) imprimeix la capçalera seguida del seu docstring. Per ara, una regla mínima: una sola línia, començant per un verb, que digui què fa la funció, no com ho fa. El format complet és el tema de Documentació i comentaris. I compte amb la diferència respecte del comentari # de 02-01: el comentari explica una línia concreta a qui llegeix el codi; el docstring descriu la funció sencera a qui la va a fer servir sense mirar a dins.
El nom, per la seva banda, és la meitat del valor d'una funció. Les convencions de la comunitat Python són aquestes:
| Regla | Bé | Malament |
|---|---|---|
snake_case, minúscules i guions baixos |
mostrar_menu |
MostrarMenu, mostrarMenu |
| Comença per un verb en infinitiu | demanar_prioritat |
prioritat, dades |
| Descriu què fa, no com | pausar |
fer_input_buit |
| Sense accents ni caràcters estranys | mostrar_capcalera |
mostrar_capçalera |
| Una responsabilitat, sense «i» al nom | validar_titol |
validar_i_desar_titol |
La raó del verb és que una funció és una acció: les variables desen coses (substantius), les funcions fan coses (verbs). En llegir pausar() entens a l'instant que allà hi passa alguna cosa; si hi llegissis pausa, dubtaries entre una variable i una crida. Dos prefixos que veuràs pertot arreu: mostrar_* per al que escriu a la pantalla i demanar_* per al que consulta a l'usuari. Adoptar-los fa el teu codi previsible, que és més elogi del que sembla.
- TascaFàcil: les tres primeres funcions
De moment només podem extreure els trossos que no necessiten dades d'entrada ni retornen res, perquè els paràmetres són la lliçó següent. Tot i així hi ha tres candidats evidents: la capçalera, el menú i la pausa.
# tascafacil.py - Estudi Alba
# Versio 0.6.1: primeres funcions sense parametres
AMPLE = 46
PRIORITATS = ("alta", "mitjana", "baixa")
EQUIP = ("Marta", "Luis", "Nuria")
OPCIONS = ("1", "2", "3", "4", "5")
def mostrar_capcalera():
"""Pinta el titol emmarcat de l'aplicacio."""
print("\n" + "=" * AMPLE)
print(f"{'TascaFacil - Estudi Alba':^{AMPLE}}")
print("=" * AMPLE)
def mostrar_menu():
"""Mostra la capcalera i les cinc opcions disponibles."""
mostrar_capcalera()
print(" 1. Registrar / substituir la tasca")
print(" 2. Veure la fitxa de la tasca")
print(" 3. Canviar la prioritat")
print(" 4. Marcar com a completada")
print(" 5. Sortir")
print("-" * AMPLE)
def pausar():
"""Atura l'execucio fins que l'usuari prem Intro."""
input("\nPrem Intro per tornar al menu...")I el bucle principal, que ara es llegeix d'una altra manera:
hi_ha_tasca = False
titol = ""
responsable = ""
prioritat = ""
dies = 0
completada = False
while True:
mostrar_menu()
opcio = input("Tria una opcio (1-5): ").strip()
while opcio not in OPCIONS:
opcio = input("Opcio no valida. Tria 1-5: ").strip()
if opcio == "1":
pass # registre de la tasca (encara sense extreure)
elif opcio == "5":
confirmar = input("Segur que vols sortir? (s/n): ").strip().lower()
if confirmar == "s":
print("Fins despres. TascaFacil es tanca.")
break
pausar()Quatre observacions sobre el que acaba de passar:
- Dotze línies de
prints'han convertit enmostrar_menu(). El bucle principal ja es llegeix com el que és: mostrar, llegir, despatxar, pausar. La mecànica de pintar continua existint, però ha deixat de fer nosa. mostrar_menu()cridamostrar_capcalera(). Una funció es pot recolzar en una altra, i així es construeixen nivells d'abstracció.- Les funcions llegeixen les constants
AMPLE,OPCIONS… encara que no els les passem. Funciona perquè són globals; a Àmbit de variables veuràs per què, i per què les constants són l'única excepció acceptable a la regla de no dependre de globals. pausar()ignora deliberadament el que retornainput(). El seu valor és en l'efecte: esperar. És un procediment de manual.
Fixa't també en les dues línies en blanc entre funcions: és la convenció d'estil de Python (la guia PEP 8, que veurem a 08-05) i ajuda molt a distingir blocs d'una ullada. El que encara no podem extreure són els bucles de validació repetits, perquè cadascun necessita el seu missatge i els seus valors vàlids, i ha de retornar la dada llegida.
Errors Comuns i Consells
Cridar sense parèntesis. pausar avalua l'objecte funció i el llença; pausar() l'executa. No hi ha error ni avís: senzillament no passa res. Si el teu programa «se salta» un pas, mira primer els parèntesis. I si el crides abans de definir-lo, NameError: name 'x' is not defined: col·loca totes les definicions a dalt i el codi que arrenca el programa al final del fitxer.
Oblidar els dos punts o la indentació. def mostrar_menu() sense : dona SyntaxError; el cos sense indentar dona IndentationError, el mateix de 03-01. Una funció necessita almenys una línia indentada: si encara no saps què hi has de posar, escriu pass.
Definir una funció dins del bucle. És legal, però es redefineix a cada volta sense cap benefici i despista en llegir. Les definicions van fora. I no esperis que una funció s'executi sola: definir no és executar.
Creure que print retorna el text. text = print("Hola") deixa text valent None. Mostrar i retornar són coses diferents.
Consell: extreu quan el codi es repeteix dues vegades, no tres. La segona còpia ja és el senyal; la tercera acostuma a arribar amb una diferència subtil entre còpies, que és un error esperant el seu torn. I prova les teves funcions al REPL: enganxa-hi la definició, crida-la i observa. No necessites executar l'aplicació sencera per saber si una peça funciona, i aquest és justament el regal de les funcions.
Exercicis
Exercici 1: Predir la sortida
Sense executar-lo, escriu què imprimeix aquest programa i per què. Para atenció als parèntesis.
def anotar():
print("Anotat")
print("A")
anotar
print("B")
anotar()
resultat = anotar()
print(resultat)Exercici 2: Tres funcions per a l'estudi
Escriu panell_alba.py amb tres funcions sense paràmetres, cadascuna amb el seu docstring d'una línia: mostrar_benvinguda(), que imprimeix un marc de 40 guions, el text Panell Estudi Alba centrat i un altre marc; mostrar_equip(), que imprimeix els tres noms de l'equip, un per línia i precedits d'un guió; i acomiadar_se(), que imprimeix una línia en blanc i el missatge Panell tancat.. Després crida les tres en ordre. La constant EQUIP ha d'estar declarada a dalt i fer-se servir a mostrar_equip().
Exercici 3: Arreglar el fitxer trencat
Aquest fitxer té quatre errades relacionades amb funcions. Identifica-les i digues quin error produeix cadascuna, o si no en produeix cap.
mostrar_titol()
def mostrar_titol()
print("TascaFacil")
def mostrar_peu():
print("Fi de l'informe")
mostrar_peuSolucions
Solució 1. La sortida és A, B, Anotat, Anotat, None, cadascun a la seva línia. Repassem-ho: print("A") imprimeix A; la línia anotar sense parèntesis avalua l'objecte funció i descarta el resultat, sense imprimir ni fallar; print("B") imprimeix B; anotar() sí que executa el cos i imprimeix Anotat; l'última crida l'executa un altre cop (segon Anotat) i desa a resultat el que retorna, que en no haver-hi return és None.
Solució 2.
# panell_alba.py - Estudi Alba
AMPLE = 40
EQUIP = ("Marta", "Luis", "Nuria")
def mostrar_benvinguda():
"""Pinta el marc i el titol del panell."""
print("-" * AMPLE)
print(f"{'Panell Estudi Alba':^{AMPLE}}")
print("-" * AMPLE)
def mostrar_equip():
"""Llista els noms de l'equip, un per linia."""
for persona in EQUIP:
print(f" - {persona}")
def acomiadar_se():
"""Tanca el panell amb un missatge de comiat."""
print()
print("Panell tancat.")
mostrar_benvinguda()
mostrar_equip()
acomiadar_se()El for persona in EQUIP: és el recorregut de 03-02: dins d'una funció funciona exactament igual que fora. I observa l'ordre: les tres definicions a dalt, les tres crides al final. Si moguessis mostrar_benvinguda() a la primera línia del fitxer, tindries un NameError.
Solució 3.
| Errada | Conseqüència |
|---|---|
mostrar_titol() es crida a la línia 1, abans de definir-se |
NameError en executar |
def mostrar_titol() sense els dos punts |
SyntaxError |
El cos de mostrar_peu no està indentat |
IndentationError |
mostrar_peu al final, sense parèntesis |
Cap error: senzillament no s'executa |
Els dos errors de sintaxi es detecten abans d'executar res, així que el programa ni tan sols arriba a produir el NameError. La versió corregida afegeix els dos punts, indenta el print de mostrar_peu, i baixa mostrar_titol() al final del fitxer al costat de mostrar_peu(), aquesta vegada amb parèntesis. La quarta errada és la més perillosa precisament perquè no falla: un programa que calla i no fa res és més difícil de diagnosticar que un que es trenca amb un missatge clar.
Conclusió
Ja saps fabricar les teves pròpies eines. Una funció es defineix amb def nom(): i un cos indentat, es crida escrivint nom() —amb parèntesis, sempre—, i ha d'estar definida abans de la línia que la invoca o Python respondrà amb un NameError. En cridar-la, el programa s'atura, anota on era, executa el cos sencer i torna exactament al punt següent: això és la pila de crides. Les funcions que només produeixen un efecte s'anomenen procediments i retornen None de manera implícita, perquè en Python tota funció retorna alguna cosa. I un docstring d'una línia, començant per un verb, deixa la funció explicada per a qui la faci servir.
Amb això, TascaFàcil ha aprimat una mica: mostrar_capcalera(), mostrar_menu() i pausar() han tret del bucle principal la mecànica de pintar i esperar. Però el problema gros continua intacte: els bucles de validació repetits. I encara no els podem extreure perquè a una funció així cal dir-li quin missatge ha de mostrar i quins valors accepta, i aquesta funció ens ha de lliurar la dada ja validada. Dir dades a una funció són els paràmetres; que ens lliuri un resultat és el return. Aquestes dues peces, que són les que converteixen una funció en una cosa veritablement reutilitzable, són el tema de la lliçó vinent: Paràmetres i retorn de valors.
Fonaments de la Programació
Mòdul 1: Introducció a la Programació
- Què és la programació?
- Història de la programació
- Llenguatges de programació
- Entorns de desenvolupament
- Del problema a l'algorisme
Mòdul 2: Conceptes Bàsics
- Variables i tipus de dades
- Operadors i expressions
- Entrada i sortida de dades
- Conversió de tipus i validació de dades
Mòdul 3: Estructures de Control
Mòdul 4: Funcions i Procediments
- Definició i ús de funcions
- Paràmetres i retorn de valors
- Àmbit de variables
- Descompondre un programa en funcions
- Funcions com a valors: lambda i ordre superior
Mòdul 5: Estructures de Dades
- Llistes i arrays
- Cadenes de caràcters
- Diccionaris i conjunts
- Tuples i estructures imbricades
- Desar dades en fitxers: text, CSV i JSON
Mòdul 6: Algorismes Bàsics
Mòdul 7: Objectes i Organització del Codi
- De les dades als objectes: classes i instàncies
- Atributs, mètodes i constructor
- Col·leccions d'objectes
- Mòduls, paquets i importacions
Mòdul 8: Bones Pràctiques i Eines
- Documentació i comentaris
- Depuració i gestió d'errors
- Control de versions
- Proves automatitzades
- Estil, llegibilitat i refactorització
