A la lliçó 04-01 va aparèixer un detall que vam deixar aparcat: escriure pausar sense parèntesis no dona error. Python respon amb una cosa com ara <function pausar at 0x7f3c9a1b2e50>, i type(pausar) retorna <class 'function'>. Aleshores ho vam tractar com un parany que calia evitar; ara li donarem la volta i el convertirem en una eina.
Perquè aquell detall significa una cosa important: en Python, una funció és un valor com qualsevol altre. Si és un valor, es pot desar en una variable, imprimir, passar com a argument a una altra funció i retornar des d'una funció. I això obre una manera d'escriure codi diferent: en comptes de decidir dins d'una funció què farà exactament, se li passa el comportament des de fora.
És l'última peça del mòdul, i la que permet rematar TascaFàcil: les tres funcions demanar_text, demanar_opcio i demanar_enter comparteixen el mateix esquelet —pregunta, comprova, insisteix— i només es diferencien en quina comprovació fan. Si la comprovació pot ser un argument, les tres es converteixen en una.
Contingut
- Una funció sense parèntesis és un valor
- Funcions d'ordre superior
- Funcions anònimes amb
lambda lambdaodef: quan cadascun- Retornar una funció: fàbriques
- El patró callback a TascaFàcil
- Ordre superior sobre col·leccions: una avançada
- Errors comuns i consells
- Exercicis
- Conclusió del mòdul
- Una funció sense parèntesis és un valor
Comencem per comprovar-ho al REPL, sense trucs.
def saludar():
print("Bon dia, equip d'Estudi Alba.")
print(saludar) # <function saludar at 0x7f3c9a1b2e50>
print(type(saludar)) # <class 'function'>
print(saludar()) # executa la funcio i despres imprimeix NoneLes dues primeres línies tracten saludar com a dada: l'imprimeixen i en consulten el tipus, igual que faries amb un número. La tercera el tracta com a acció: els parèntesis són l'operador de crida.
Si és un valor, es pot assignar a una altra variable:
def mostrar_capcalera():
print("TascaFacil - Estudi Alba")
pintar = mostrar_capcalera # sense parentesis: copiem la referencia
pintar() # TascaFacil - Estudi Alba
print(pintar is mostrar_capcalera) # TrueFixa't bé en el que ha passat: no s'ha copiat la funció, sinó que ara hi ha dos noms apuntant al mateix objecte funció, tal com vas veure amb les referències a 04-03. L'is ho confirma. A partir d'aquell moment, pintar() i mostrar_capcalera() són exactament la mateixa crida.
| Expressió | Què és | Què val |
|---|---|---|
mostrar_capcalera |
L'objecte funció | <function mostrar_capcalera at 0x…> |
mostrar_capcalera() |
Una crida | El que retorni (aquí, None) |
type(mostrar_capcalera) |
El tipus de l'objecte | <class 'function'> |
Això val també per a les funcions integrades i per als mètodes: str.upper, len o input sense parèntesis són valors que pots desar i passar per aquí. És la base de tot el que ve.
- Funcions d'ordre superior
Una funció d'ordre superior és aquella que rep una altra funció com a paràmetre o retorna una funció. Sona abstracte; l'exemple ho aclareix en tres línies.
def aplicar_format(text, transformacio):
"""Aplica al text la transformacio rebuda i retorna el resultat."""
return transformacio(text)
print(aplicar_format("cartell fira del llibre", str.upper))
print(aplicar_format("CARTELL FIRA DEL LLIBRE", str.lower))
print(aplicar_format("cartell fira del llibre", str.title))Dins d'aplicar_format, el paràmetre transformacio és un nom local que apunta a la funció que li hagin passat; en escriure transformacio(text) es crida allò que hagi arribat. La funció no sap ni li importa què és: el comportament ve de fora.
I funciona igual amb funcions pròpies:
def marcar_urgent(text):
"""Retorna el text envoltat de marques d'urgencia."""
return f">>> {text.upper()} <<<"
def amb_longitud(text):
"""Retorna el text seguit del seu nombre de caracters."""
return f"{text} ({len(text)} caracters)"
print(aplicar_format("cartell fira del llibre", marcar_urgent))
print(aplicar_format("cartell fira del llibre", amb_longitud))Observa el detall decisiu: a aplicar_format(text, marcar_urgent), marcar_urgent va sense parèntesis. Estem passant la funció, no el seu resultat. Si escrivissis marcar_urgent(...) l'estaries cridant allà mateix i passant la cadena que retorna, que és una altra cosa completament diferent —i la font número u d'errors en aquesta lliçó.
L'avantatge d'aquest patró és que separa el que es repeteix del que canvia. aplicar_format és un esquelet; la transformació concreta és un buit que s'omple a cada crida. Veuràs que és exactament el problema de les nostres tres funcions demanar_*.
- Funcions anònimes amb
lambda
lambdaDe vegades la funció que vols passar és tan petita que donar-li nom i tres línies de def resulta desproporcionat. Per a això hi ha lambda: una expressió que crea una funció sense nom.
Les tres parts: la paraula lambda, els paràmetres separats per comes (sense parèntesis) i, després dels dos punts, una única expressió el valor de la qual es retorna automàticament. No s'escriu return: està implícit.
Aquesta lambda és equivalent, punt per punt, a def doble(n): return n * 2. El seu tipus és el mateix, function, i es crida igual. On de veritat brilla és dins d'una altra crida, sense donar-li nom:
print(aplicar_format("cartell fira", lambda t: t.strip().capitalize()))
print(aplicar_format("cartell fira", lambda t: f"[{t}]"))
print(aplicar_format("cartell fira", lambda t: t.replace(" ", "_")))Tres comportaments diferents sense escriure tres def. La limitació, que és forta, ja l'has vista: una lambda només pot contenir una expressió. Res de if/elif en diverses línies, res de bucles, res d'assignacions, res de diverses sentències. Sí que hi cap l'operador ternari de 03-01, perquè és una expressió:
etiqueta = lambda dies: "llarga" if dies > 5 else "curta"
print(etiqueta(9)) # llarga
print(etiqueta(2)) # curtaI un advertiment d'estil que convé interioritzar ja: doble = lambda n: n * 2 és correcte però desaconsellat. Si li has de posar nom a la funció, fes servir def: et dona docstring, un nom visible als missatges d'error i lloc per créixer. La lambda està pensada per a funcions sense nom, fetes servir al mateix lloc.
lambda o def: quan cadascun
lambda o def: quan cadascun| Criteri | lambda |
def |
|---|---|---|
| Nom | Anònima | Té nom propi |
| Cos | Una sola expressió | Tantes sentències com calgui |
return |
Implícit | Explícit |
| Docstring | No n'admet | Sí |
| En un traceback apareix com | <lambda> |
El nom real de la funció |
| Ús típic | Argument d'una altra funció, d'usar i llençar | Tota la resta |
| Reutilitzable des de diversos llocs | Malament | Bé |
La regla de decisió pràctica cap en tres preguntes. La faràs servir més d'una vegada? Doncs def. Necessita més d'una línia, o mereix una explicació? Doncs def. És una condició o una transformació trivial que es llegeix millor dins de la crida que com una definició a part, tres pantalles més amunt? Doncs lambda.
Aquest últim cas és real i no és rar: quan el criteri és evident, escriure'l al mateix lloc on es fa servir estalvia al lector un viatge a una altra banda del fitxer. Però tan bon punt la lambda comença a tenir parèntesis imbricats o un ternari dins d'un altre, ha deixat d'aclarir i toca ascendir-la a def.
- Retornar una funció: fàbriques
L'altra meitat de l'ordre superior és retornar una funció. Una funció que fabrica funcions s'anomena, informalment, una fàbrica, i es recolza en les funcions imbricades i l'àmbit envolupant que vas veure a 04-03.
def crear_salutacio(nom_estudi):
"""Retorna una funcio que saluda en nom de l'estudi indicat."""
def saludar(persona):
return f"Hola {persona}, benvinguda a {nom_estudi}."
return saludar # sense parentesis: retornem la funcio
saludar_alba = crear_salutacio("Estudi Alba")
saludar_sole = crear_salutacio("Forn Sole")
print(saludar_alba("Marta"))
print(saludar_alba("Luis"))
print(saludar_sole("Nuria"))Hola Marta, benvinguda a Estudi Alba. Hola Luis, benvinguda a Estudi Alba. Hola Nuria, benvinguda a Forn Sole.
Analitza el que passa, perquè és més subtil del que sembla. crear_salutacio("Estudi Alba") s'executa, defineix la funció interna saludar i la retorna; en aquell moment crear_salutacio acaba, i segons el que vas aprendre a 04-03 la seva variable local nom_estudi hauria de desaparèixer. Però no desapareix: la funció retornada se'n porta amb ella el valor que tenia. Per això saludar_alba i saludar_sole són dues funcions diferents, cadascuna amb el seu estudi a dins.
Aquest mecanisme té nom —clausura, o closure— i no cal que el dominis avui. N'hi ha prou de quedar-te amb la idea: una fàbrica et permet crear versions especialitzades d'una funció a partir d'uns paràmetres de configuració, en comptes de repetir aquests paràmetres a totes les crides.
def crear_validador_de_rang(minim, maxim):
"""Retorna una funcio que comprova si un text es un enter del rang."""
def es_valid(text):
return text.isdigit() and minim <= int(text) <= maxim
return es_valid
es_dia_valid = crear_validador_de_rang(1, 365)
es_hora_valida = crear_validador_de_rang(0, 23)
print(es_dia_valid("400")) # False
print(es_dia_valid("4")) # True
print(es_hora_valida("22")) # TrueDos validadors complets a partir d'una sola definició. Guarda aquesta idea: la farem servir ara mateix.
- El patró callback a TascaFàcil
Mira les tres funcions de 04-02 traient-los la carn:
valor = input(missatge).strip()
while <la comprovacio que sigui>:
valor = input(<l'avis que sigui>).strip()
return valorLes tres tenen el mateix esquelet i difereixen només en la comprovació. Ja saps què cal fer: convertir aquesta comprovació en un paràmetre. A la funció que es passa perquè una altra la cridi se l'anomena callback —«funció de retorn de crida»—, perquè qui la rep la crida de tornada quan la necessita.
def demanar_dada(missatge, validador, avis="Valor no valid."):
"""Demana una dada i la retorna quan el validador la dona per bona."""
valor = input(missatge).strip()
while not validador(valor):
print(avis)
valor = input(missatge).strip()
return valorVuit línies que substitueixen les tres funcions anteriors. El contracte és explícit: validador ha de ser una funció que rebi un text i retorni True o False. I ara els validadors, uns amb def i altres amb lambda, segons el que aconsella la taula de la secció 4:
PRIORITATS = ("alta", "mitjana", "baixa")
EQUIP = ("marta", "luis", "nuria")
def es_dies_valid(text):
"""Comprova que el text sigui un enter de dies entre 1 i 365."""
return text.isdigit() and 1 <= int(text) <= 365
titol = demanar_dada("Titol : ",
lambda t: t != "",
"El titol no pot estar buit.")
responsable = demanar_dada("Responsable : ",
lambda t: t.lower() in EQUIP,
f"Ha de ser un de: {', '.join(EQUIP)}.")
prioritat = demanar_dada("Prioritat : ",
lambda t: t.lower() in PRIORITATS,
"Prioritat: alta, mitjana o baixa.")
dies = int(demanar_dada("Dies (1-365) : ", es_dies_valid,
"Ha de ser un enter entre 1 i 365."))Quatre observacions sobre aquest codi:
- Les tres primeres validacions són trivials i van en
lambda, escrites just on es fan servir: es llegeix d'una ullada què es considera vàlid per a cada camp. La dels dies porta dues comprovacions encadenades i mereix undefamb nom i docstring. es_dies_validva sense parèntesis. Estem passant la funció, no cridant-la. És l'error que més es comet aquí.- L'avís és un paràmetre amb valor per defecte (04-02), així que cada camp pot explicar el seu propi criteri sense obligar a fer-ho sempre.
demanar_dadaretorna sempre text, i la conversió a enter es fa fora, a la crida. És coherent amb 04-04: la funció d'entrada valida, i qui la fa servir decideix quin tipus vol.
Ha millorat el programa? Depèn de quins valors. La taula honesta:
Tres funcions demanar_* (04-02) |
Una demanar_dada amb callback |
|
|---|---|---|
| Funcions a mantenir | 3 | 1 |
| Flexibilitat | Només els tres casos previstos | Qualsevol criteri imaginable |
| Llegibilitat de la crida | Molt alta (demanar_enter(m, 1, 365)) |
Mitjana: cal llegir la lambda |
| Dificultat per a un principiant | Baixa | Mitjana |
No hi ha un guanyador absolut, i convé dir-ho clar: l'ordre superior és una eina, no una millora automàtica. Per a un programa petit amb tres tipus de dada, les tres funcions específiques es llegeixen millor. Quan els criteris de validació es multipliquen i canvien sovint, demanar_dada guanya de pallissa. L'important és que ara pots triar, i que reconeixes el patró quan el vegis en codi aliè, on és habitual.
- Ordre superior sobre col·leccions: una avançada
El lloc on l'ordre superior es fa servir diàriament és sobre col·leccions de dades: ordenar una llista de tasques per prioritat, quedar-se amb les de la Nuria, transformar-les totes de cop. Les eines clàssiques són sorted amb el seu paràmetre key, i map i filter.
Com que encara no coneixes les llistes, vegem-ho amb el que sí que tens: una cadena, que és una seqüència de caràcters. (sorted retorna una llista de caràcters, així que els tornem a unir amb "".join(...) per poder imprimir-los com a text; les llistes arriben al mòdul 5.)
print("".join(sorted("nuria"))) # ainru
print("".join(sorted("Nuria"))) # Nairu
print("".join(sorted("Nuria", key=str.lower))) # aiNru
print(max("Marta", "Luis", key=len)) # MartaFixa't en key=str.lower: és un argument per nom el valor del qual és una funció. sorted la crida una vegada per cada caràcter i ordena segons el que retorni, no segons el caràcter original. Per això la tercera línia ignora majúscules i minúscules mentre que la segona posa la N al principi —les majúscules van abans en l'ordre dels caràcters. I max(..., key=len) tria la cadena més llarga en comptes de la major alfabèticament: el mateix max de sempre, amb un criteri diferent injectat des de fora.
Aquest és el patró sencer. sorted no sap ordenar tasques per prioritat, però sí que sap ordenar qualsevol cosa si li dius amb una funció què ha de mirar de cada element. Quan arribis a Llistes i arrays tindràs col·leccions de veritat sobre les quals aplicar-ho, i a Algorismes d'ordenació veuràs com funciona l'ordenació per dins. map i filter, que apliquen una funció a tots els elements o seleccionen els que la compleixen, s'estudien també amb col·leccions; de moment n'hi ha prou que en reconeguis la forma quan la trobis.
Errors Comuns i Consells
Passar la crida en comptes de la funció. aplicar_format(text, marcar_urgent()) executa marcar_urgent allà mateix —a més sense arguments, amb la qual cosa dona TypeError— i passa el seu resultat. Per passar la funció, escriu-la sense parèntesis.
Oblidar els parèntesis dins de la funció d'ordre superior. L'error simètric: si a dins escrius return transformacio en comptes de transformacio(text), retornes l'objecte funció. Símptoma: s'imprimeix <function … at 0x…> on esperaves un text.
Ficar massa coses en una lambda. Si necessites un if de diverses branques, un bucle o una assignació, no hi cap: SyntaxError. Fes servir def.
Posar nom a una lambda. doble = lambda n: n * 2 funciona, però perds docstring i un nom útil als errors. Si porta nom, que sigui def.
Confondre el paràmetre amb la funció que arriba. Dins de demanar_dada, validador és un nom local: pot rebre avui una lambda i demà un def. La funció només exigeix que sigui cridable amb un text i retorni un booleà.
Consell: documenta el contracte del callback. Al docstring, digues què ha de rebre i què ha de retornar la funció que et passen. Sense això, qui faci servir la teva funció s'haurà de llegir el cos.
Consell: no facis servir ordre superior perquè quedi sofisticat. Si dos if resolen el problema amb més claredat, dos if. L'abstracció es paga en esforç de lectura, i només compensa quan estalvia repetició real.
Exercicis
Exercici 1: Predir la sortida
Digues què imprimeix cada línia, i si escau quin error es produeix i per què.
def emmarcar(text):
return f"<<{text}>>"
f = emmarcar
print(f("Forn Sole"))
print(f)
print(emmarcar is f)
def aplicar(valor, funcio):
return funcio(valor)
print(aplicar("Forn Sole", emmarcar))
print(aplicar("Forn Sole", emmarcar("Vidal")))Exercici 2: Fàbrica d'etiquetadors
Escriu crear_etiquetador(prefix, ample), una funció que retorni una altra funció. La funció retornada rep un text i retorna el prefix alineat a l'esquerra en 10 caràcters seguit del text alineat a la dreta en ample caràcters, fent servir el formatatge d'f-strings de 02-03. Crea amb ella dos etiquetadors —un per a tasques urgents amb prefix [URGENT] i ample 30, i un altre per a notes amb prefix [nota] i ample 24— i prova'ls amb «Cartell fira del llibre».
Exercici 3: Validadors per a TascaFàcil
Fent servir demanar_dada(missatge, validador, avis) de la secció 6, escriu les expressions de crida necessàries per demanar aquestes tres dades, decidint en cada cas si el validador ha de ser lambda o def i justificant-ho:
- Un codi de client: exactament quatre dígits.
- Una resposta de sí o no:
son, sense distingir majúscules. - Un percentatge d'avanç: enter entre 0 i 100, i a més múltiple de 5.
Solucions
Solució 1.
<<Forn Sole>> <function emmarcar at 0x...> True <<Forn Sole>> TypeError: 'str' object is not callable
f = emmarcar crea un segon nom per al mateix objecte, així que f("Forn Sole") emmarca igual i emmarcar is f és True. print(f) mostra la representació de l'objecte funció. La primera crida a aplicar passa la funció i funciona. L'última passa emmarcar("Vidal"), que s'avalua abans de la crida i val "<<Vidal>>": dins d'aplicar, funcio és una cadena, i en intentar funcio(valor) Python protesta amb 'str' object is not callable. És el mateix missatge que veies en ombrejar un built-in a 04-03, i pel mateix motiu: s'està cridant una cosa que no és una funció.
Solució 2.
def crear_etiquetador(prefix, ample):
"""Retorna una funcio que etiqueta textos amb el prefix i ample donats."""
def etiquetar(text):
return f"{prefix:<10}{text:>{ample}}"
return etiquetar
etiquetar_urgent = crear_etiquetador("[URGENT]", 30)
etiquetar_nota = crear_etiquetador("[nota]", 24)
print(etiquetar_urgent("Cartell fira del llibre"))
print(etiquetar_nota("Cartell fira del llibre"))La funció interna etiquetar llegeix prefix i ample de l'àmbit envolupant (la E de LEGB, 04-03) i continua recordant-los després que crear_etiquetador hagi acabat. Compara aquest enfocament amb passar prefix i ample a cada crida: la fàbrica t'estalvia repetir-los, i de passada deixa clar que un etiquetador d'urgències és una cosa i un de notes és una altra.
Solució 3.
codi = demanar_dada("Codi de client: ",
lambda t: t.isdigit() and len(t) == 4,
"Han de ser exactament quatre digits.")
resposta = demanar_dada("Confirmes? (s/n): ",
lambda t: t.lower() in ("s", "n"),
"Respon s o n.")
def es_avanc_valid(text):
"""Comprova que sigui un enter de 0 a 100 multiple de 5."""
if not text.isdigit():
return False
valor = int(text)
return 0 <= valor <= 100 and valor % 5 == 0
avanc = int(demanar_dada("Avanc (0-100): ", es_avanc_valid,
"Enter de 0 a 100, multiple de 5."))Els dos primers criteris caben folgadament en una expressió i es llegeixen millor dins de la crida, així que lambda. El tercer necessita convertir el text a número després de comprovar que es pot convertir, i això són dos passos amb una variable intermèdia: impossible en una lambda llegible, i a més mereix un docstring que expliqui la regla del múltiple de 5. Aquell if not text.isdigit(): return False és, per cert, la clàusula de guarda de 04-02 fent la seva feina: sense ella, int(text) rebentaria amb un ValueError davant de qualsevol lletra.
Conclusió del mòdul
Les funcions també són dades. Sense parèntesis, un nom de funció és un valor de tipus function que es pot imprimir, assignar a una altra variable i passar per aquí; amb parèntesis, és una crida. D'aquí surten les funcions d'ordre superior: les que reben una funció com a paràmetre —l'esquelet el poses tu, el comportament concret el posa qui crida— i les que retornen una funció, les fàbriques, capaces de crear versions especialitzades que recorden la seva configuració. Per a les funcions diminutes d'usar i llençar hi ha lambda, limitada a una sola expressió i sense return explícit; per a tota la resta, def. I el patró callback —passar la comprovació com a argument— ha unificat les tres funcions demanar_* de TascaFàcil en una sola demanar_dada.
Amb això es tanca el mòdul 4. Saps definir i cridar funcions, i distingir la definició de la crida (04-01). Saps donar-los paràmetres posicionals, per nom i amb valors per defecte, i fer que retornin un o diversos valors amb return, entenent per què retornar gairebé sempre guanya a imprimir (04-02). Saps on viu cada nom gràcies a la regla LEGB, per què les globals canviants són un problema i per què les constants no ho són (04-03). Saps descompondre un programa sencer amb criteris comprovables: una responsabilitat per funció, nivells d'abstracció homogenis, entrada i sortida separades de la lògica, DRY sense sobrefragmentar, i un main() protegit per if __name__ == "__main__": (04-04). I saps que les funcions són valors que es passen i es retornen (04-05).
tascafacil.py ha arribat a la versió 0.7: constants a dalt, funcions agrupades per famílies, un main() de vint-i-dues línies que es llegeix com un índex, zero duplicació i vuit peces reutilitzables en qualsevol altre programa. És un salt enorme respecte al mur de la v0.6.
I tanmateix, continua gestionant una sola tasca. La Marta no pot tenir alhora el cartell de la fira del llibre, el menú del Forn Solé i l'encàrrec del client Vidal: registrar-ne una destrueix l'anterior. Les sis variables soltes —titol, responsable, prioritat, dies, completada, hi_ha_tasca— viatgen juntes de funció en funció perquè són, en realitat, una sola cosa sense nom. I mostrar_fitxa(titol, responsable, prioritat, dies, completada) amb els seus cinc paràmetres n'és el símptoma més visible.
Les dues coses —desar moltes tasques i agrupar les dades de cadascuna— es resolen amb el mateix: estructures de dades. És el mòdul 5, que comença amb Llistes i arrays: l'eina que permetrà per fi que TascaFàcil gestioni la feina real d'Estudi Alba, recórrer-la amb un for, cercar-hi, ordenar-la per prioritat i, quan arribem als fitxers, que continuï allà demà al matí.
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ó
