Funciones en Python — teoría con la caja de la cafetería, print frente a return y la pila de llamadas (y la función que enseñaba el total pero no lo devolvía)
En la primera práctica con funciones de FP1 me tocó programar la caja de la cafetería de la facultad. Escribí una función para el total del pedido, la probé con un café y un bocadillo y en pantalla salió Total: 3.7. Perfecto, pensé. Le añadí el descuento del bono de estudiante… y el programa explotó en la línea siguiente:
# La caja de la cafetería: una función que "calcula" el total del pedido
def total_pedido(cafe, bocadillo):
print("Total:", cafe + bocadillo) # lo ENSEÑA en pantalla... pero no lo devuelve
total = total_pedido(1.20, 2.50)
con_bono = total * 0.90 # el bono de estudiante descuenta un 10 %
print("Con bono:", con_bono)
Lo que salió:
Total: 3.7
Traceback (most recent call last):
File "gancho.py", line 7, in <module>
con_bono = total * 0.90 # el bono de estudiante descuenta un 10 %
~~~~~~^~~~~~
TypeError: unsupported operand type(s) for *: 'NoneType' and 'float'
¿Cómo que NoneType? ¡Si el 3.7 estaba ahí, en la pantalla! Pues ese es justo el malentendido que más cuesta en este tema: enseñar un valor no es lo mismo que devolverlo. La función hacía print del total, pero a la variable total le llegaba None, y None * 0.90 no se puede calcular. Al final del artículo lo entenderás tan bien que lo verás venir antes de ejecutar.
Hoy aprendes a escribir funciones en Python: qué son y por qué existen, cómo se definen con def, la diferencia entre parámetros y argumentos (por posición, por nombre y con valores por defecto), qué hace de verdad return y qué pasa cuando no lo pones, cómo se llaman unas funciones a otras (la pila de llamadas) y dónde vive cada variable (ámbito local y global). Vienes de los bucles en Python y por fin vamos a cumplir una promesa de sus ejercicios: la búsqueda en dos bucles que se arregla con un solo return. Cuando acabes, tienes la práctica de funciones con tres programas completos.
Tabla de Contenidos
¿Qué son las funciones en Python y por qué usarlas?
Una función es un bloque de código con nombre que hace una tarea concreta. Se escribe una vez y se ejecuta cada vez que la llamas por su nombre, desde donde quieras y tantas veces como quieras.
En realidad llevas usando funciones desde el primer día: print(), len(), round(), int() o range() son funciones. Alguien las escribió, les puso nombre y tú solo las llamas. La diferencia es que a partir de hoy vas a crear las tuyas.
¿Para qué? Mira la caja de la cafetería atendiendo tres pedidos. Sin funciones, la misma cuenta del cambio aparece copiada tres veces:
# Tres pedidos en la caja SIN funciones: la misma cuenta, copiada tres veces
pago1 = 5.00
total1 = 1.20 + 2.50 # café + bocadillo
print("Pedido 1 · cambio:", round(pago1 - total1, 2))
pago2 = 10.00
total2 = 1.20 + 1.20 + 3.10 # dos cafés + ensalada
print("Pedido 2 · cambio:", round(pago2 - total2, 2))
pago3 = 2.00
total3 = 1.50 # un zumo
print("Pedido 3 · cambio:", round(pago3 - total3, 2))
Salida:
Pedido 1 · cambio: 1.3 Pedido 2 · cambio: 4.5 Pedido 3 · cambio: 0.5
Funciona, pero imagina que mañana quieres redondear el cambio de otra forma, o avisar si el cliente paga de menos. Tendrías que cambiar tres líneas (o treinta, si el programa crece) y acordarte de todas. Ahora con una función:
# Los mismos tres pedidos CON una función: la cuenta se escribe una sola vez
def cobrar(total, pago):
"""Devuelve el cambio que hay que dar, redondeado a céntimos."""
return round(pago - total, 2)
print("Pedido 1 · cambio:", cobrar(1.20 + 2.50, 5.00))
print("Pedido 2 · cambio:", cobrar(1.20 + 1.20 + 3.10, 10.00))
print("Pedido 3 · cambio:", cobrar(1.50, 2.00))
Salida:
Pedido 1 · cambio: 1.3 Pedido 2 · cambio: 4.5 Pedido 3 · cambio: 0.5
La salida es idéntica, pero la cuenta vive en un solo sitio: cobrar. Si cambias la regla del cambio, la cambias ahí y los tres pedidos se enteran solos. A esto se le llama principio DRY (Don’t Repeat Yourself, no te repitas), y las funciones son la herramienta para cumplirlo.
Una regla práctica para FP1: si te pillas copiando y pegando el mismo trozo de código por segunda vez, para y conviértelo en una función.
Además de evitar repeticiones, las funciones hacen el código más legible (cobrar(3.70, 5.00) dice lo que hace mejor que una cuenta suelta), más fácil de probar (pruebas la función sola, con datos que tú eliges) y más fácil de depurar (si el cambio sale mal, sabes en qué función buscar).
Cómo definir una función
Una función tiene dos partes: la cabecera y el cuerpo.
def saludar_cliente():
"""Saluda a quien llega a la caja.""" # docstring: qué hace la función
print("¡Buenos días! ¿Qué te pongo?")
print("1. Antes de llamar a la función")
saludar_cliente() # llamada: Python salta al cuerpo de la función...
print("2. Después de llamarla") # ...y al terminar vuelve justo aquí
saludar_cliente() # se puede llamar tantas veces como quieras
print(saludar_cliente.__doc__) # la docstring también se puede leer
- La cabecera empieza con la palabra clave
def, sigue con el nombre de la función (ensnake_case, como las variables), los paréntesis (obligatorios aunque estén vacíos) y termina con dos puntos:. - El cuerpo va debajo, con sangría de 4 espacios. Todo lo que tenga esa sangría pertenece a la función.
- La primera línea del cuerpo suele ser una docstring: un texto entre comillas triples que explica qué hace la función. No cambia lo que hace, pero los editores la enseñan como ayuda cuando escribes el nombre, y se puede leer con
__doc__.
Salida:
1. Antes de llamar a la función ¡Buenos días! ¿Qué te pongo? 2. Después de llamarla ¡Buenos días! ¿Qué te pongo? Saluda a quien llega a la caja.
Fíjate en el orden. Al ejecutar def, Python no ejecuta el cuerpo: solo apunta «existe una función llamada saludar_cliente«. El cuerpo se ejecuta cuando la llamas con saludar_cliente(). En ese momento el programa salta dentro de la función, ejecuta su cuerpo y, al terminar, vuelve justo detrás de la llamada. Por eso el saludo sale entre la línea 1 y la 2.
Escribir saludar_cliente sin paréntesis. No da error, pero tampoco llama a la función: solo nombra el objeto función y no pasa nada. Para llamarla hacen falta los paréntesis, aunque estén vacíos.
El dibujo resume las piezas de una función y lo que pasa en su llamada. Arriba está la definición de cobrar; abajo, la llamada que reparte los valores entre los parámetros y recoge lo que devuelve el return:
Pistas de tipo en la cabecera
En los apuntes de FP1 verás cabeceras como esta, con anotaciones de tipo (type hints): después de cada parámetro, dos puntos y el tipo que se espera; y antes de los dos puntos finales, una flecha -> con el tipo de lo que devuelve.
def precio_con_bono(precio: float, descuento: float = 0.10) -> float:
"""Precio tras aplicar el descuento del bono (10 % si no se indica otro)."""
return round(precio * (1 - descuento), 2)
print(precio_con_bono(2.50)) # 2.50 menos el 10 %
print(precio_con_bono(2.50, 0.20)) # con un 20 %
print(precio_con_bono("2.50")) # las pistas de tipo NO se comprueban al ejecutar
Salida:
2.25
2.0
Traceback (most recent call last):
File "anotaciones.py", line 8, in <module>
print(precio_con_bono("2.50")) # las pistas de tipo NO se comprueban al ejecutar
~~~~~~~~~~~~~~~^^^^^^^^
File "anotaciones.py", line 3, in precio_con_bono
return round(precio * (1 - descuento), 2)
~~~~~~~^~~~~~~~~~~~~~~~~
TypeError: can't multiply sequence by non-int of type 'float'
Las dos primeras llamadas funcionan: 2.50 − 10 % = 2.25 y 2.50 − 20 % = 2.0. La tercera es la lección: Python no comprueba las anotaciones al ejecutar. Son información para quien lee el código (y para herramientas que analizan el código antes de ejecutarlo), no una protección. Le pasamos el texto "2.50" y Python intentó multiplicar un texto por un decimal, que es el TypeError que ves.
Fíjate también en el traceback: tiene dos bloques File. El primero es la línea de la llamada (8) y el segundo, la línea de dentro de la función que falló (3). Volveremos a esto en la pila de llamadas.
Parámetros y argumentos: la diferencia que nadie explica
Son dos palabras que se usan como si fueran lo mismo, pero no lo son:
- Parámetro (o parámetro formal): la variable que pones en la definición, entre los paréntesis. Es un hueco con nombre que espera un valor.
- Argumento (o parámetro real): el valor que pasas en la llamada. Es lo que rellena el hueco en esa llamada concreta.
def precio_menu(bocadillo, bebida):
"""bocadillo y bebida son PARÁMETROS: huecos que se rellenan en cada llamada."""
return round(bocadillo + bebida, 2)
# 2.50 y 1.50 son ARGUMENTOS: los valores reales de esta llamada
print(precio_menu(2.50, 1.50)) # por posición: el 1.º va al 1.º
print(precio_menu(bebida=1.50, bocadillo=3.20)) # por nombre: el orden da igual
bocadillo = 9.99 # una variable de fuera con el mismo nombre...
print(precio_menu(2.50, 1.20)) # ...no tiene nada que ver con el parámetro
print(bocadillo) # sigue valiendo 9.99
Salida:
4.0 4.7 3.7 9.99
Vamos línea a línea:
| Llamada | Cómo se reparten los valores | Resultado |
|---|---|---|
precio_menu(2.50, 1.50) | por posición: bocadillo = 2.50, bebida = 1.50 | 2.50 + 1.50 = 4.0 |
precio_menu(bebida=1.50, bocadillo=3.20) | por nombre: el orden da igual | 3.20 + 1.50 = 4.7 |
precio_menu(2.50, 1.20) | por posición | 2.50 + 1.20 = 3.7 |
La última línea es la más interesante: antes de la tercera llamada creamos fuera una variable que también se llama bocadillo y vale 9.99. La función no la ve ni la toca: su parámetro bocadillo es otra variable, interna, que en esa llamada vale 2.50. Por eso print(bocadillo) sigue dando 9.99. Que se llamen igual es pura coincidencia.
En Python los parámetros funcionan como variables locales de la función: se crean al llamarla con el valor del argumento y desaparecen cuando termina. Lo vemos a fondo en el apartado de ámbito.
Cuántos y en qué orden
Si la función tiene dos parámetros sin valor por defecto, hay que darle dos argumentos. Si falta uno, Python no adivina:
def precio_menu(bocadillo, bebida):
return round(bocadillo + bebida, 2)
print(precio_menu(2.50)) # falta el segundo argumento
Traceback (most recent call last):
File "falta_argumento.py", line 5, in <module>
print(precio_menu(2.50)) # falta el segundo argumento
~~~~~~~~~~~^^^^^^
TypeError: precio_menu() missing 1 required positional argument: 'bebida'
El mensaje es de los más claros que vas a ver: missing 1 required positional argument: 'bebida', te falta el argumento posicional obligatorio bebida.
Más traicionero es pasar el número correcto de argumentos en el orden equivocado, porque ahí no hay error:
def cobrar(total, pago):
return round(pago - total, 2)
print(cobrar(3.70, 5.00)) # bien: primero el total, después lo que paga
print(cobrar(5.00, 3.70)) # al revés: ¡cambio negativo y ningún error!
print(cobrar(pago=5.00, total=3.70)) # por nombre no hay confusión posible
Salida:
1.3 -1.3 1.3
cobrar(5.00, 3.70) mete 5.00 en total y 3.70 en pago, así que calcula 3.70 − 5.00 = −1.3. Para Python es una resta perfectamente válida; para la caja, un desastre. Cuando una función tiene varios parámetros del mismo tipo y es fácil confundirlos, pasarlos por nombre (pago=5.00, total=3.70) elimina el problema.
Los argumentos con nombre tienen que ir después de los posicionales en la llamada: cobrar(3.70, pago=5.00) vale; cobrar(total=3.70, 5.00) es un SyntaxError.
Parámetros con valor por defecto
Un parámetro puede tener un valor por defecto (o por omisión): si en la llamada no le das valor, usa ese. Así el parámetro se vuelve opcional.
def cobrar(total, pago, propina=0):
"""Devuelve el cambio; la propina es opcional (0 si no se indica)."""
return round(pago - total - propina, 2)
print(cobrar(3.70, 5.00)) # propina = 0, el valor por defecto
print(cobrar(3.70, 5.00, 0.30)) # propina = 0.30, por posición
print(cobrar(3.70, 5.00, propina=1)) # propina = 1, por nombre (se lee mejor)
Salida:
1.3 1.0 0.3
Comprobamos las cuentas: 5.00 − 3.70 − 0 = 1.3; 5.00 − 3.70 − 0.30 = 1.0; 5.00 − 3.70 − 1 = 0.3. Cuando la propina no se indica, vale 0 y es como si no existiera.
La regla de oro: los parámetros con valor por defecto van al final. Si pones uno opcional delante de uno obligatorio, Python ni siquiera empieza a ejecutar:
# Un parámetro con valor por defecto ANTES de uno obligatorio
def cobrar(propina=0, total, pago):
return round(pago - total - propina, 2)
File "orden_mal.py", line 2
def cobrar(propina=0, total, pago):
^^^^^
SyntaxError: parameter without a default follows parameter with a default
El mensaje lo dice literal: un parámetro sin valor por defecto va detrás de uno con valor por defecto. ¿Por qué esa regla? Porque en cobrar(3.70, 5.00) Python no podría saber si 3.70 es la propina o el total.
Usar una lista o un diccionario vacío como valor por defecto (def anotar(pedido, lista=[])). El valor por defecto se crea una sola vez, al definir la función, y todas las llamadas comparten la misma lista. Cuando lleguemos a las listas lo verás con detalle; por ahora, usa valores por defecto de tipos simples: números, textos, True/False o None.
El return: lo que más confunde
return hace dos cosas a la vez:
- Devuelve un valor a quien llamó a la función. Ese valor sustituye a la llamada, como si en su lugar hubieras escrito el número.
- Termina la función en el acto. Lo que haya debajo del
returnen esa misma rama no se ejecuta nunca.
def precio_con_bono(precio):
"""Devuelve el precio con el 10 % de descuento del bono."""
return round(precio * 0.90, 2)
print("Esta línea nunca se ejecuta") # código muerto: el return ya ha salido
cafe = precio_con_bono(1.20) # el valor devuelto se guarda...
print(cafe)
print(round(precio_con_bono(2.50) + cafe, 2)) # ...se mete en una expresión...
print(precio_con_bono(precio_con_bono(10.00))) # ...o en otra llamada
Salida:
1.08 3.33 8.1
Cuentas: 1.20 × 0.90 = 1.08; 2.50 × 0.90 = 2.25 y 2.25 + 1.08 = 3.33; 10.00 × 0.90 = 9.0 y 9.0 × 0.90 = 8.1. El print("Esta línea nunca se ejecuta") cumple su palabra: no aparece en la salida porque está detrás del return. Los editores suelen ponerlo en gris (código muerto).
Mira las tres formas de usar lo que devuelve una función: guardarlo en una variable, meterlo dentro de una expresión y pasárselo como argumento a otra llamada (precio_con_bono(precio_con_bono(10.00)) aplica el bono dos veces). Las tres son posibles solo porque hay return.
Varios return y varios valores
Una función puede tener varios return en sitios distintos; sale por el primero que alcance. Y un return puede devolver varios valores separados por comas:
def tipo_de_pedido(total):
"""Clasifica el pedido según lo que cuesta: cada rama tiene su propio return."""
if total < 2:
return "pedido pequeño"
if total < 6:
return "pedido normal" # si llega aquí, la línea de abajo ya no se mira
return "pedido grande"
def cambio_en_monedas(cambio):
"""Devuelve DOS valores a la vez: los euros enteros y los céntimos."""
centimos = round(cambio * 100)
return centimos // 100, centimos % 100
print(tipo_de_pedido(1.50), "/", tipo_de_pedido(3.70), "/", tipo_de_pedido(7.80))
euros, cents = cambio_en_monedas(6.30) # los dos valores, cada uno a su variable
print(euros, "euros y", cents, "céntimos")
Salida:
pedido pequeño / pedido normal / pedido grande 6 euros y 30 céntimos
En tipo_de_pedido(3.70), la primera condición (3.70 < 2) es falsa, la segunda (3.70 < 6) es verdadera y la función sale con "pedido normal": la tercera línea ni se mira. Ni siquiera hace falta elif, porque cada return ya corta la función.
cambio_en_monedas(6.30) pasa el cambio a céntimos (630, con round para evitar sorpresas de los decimales), y devuelve 630 // 100 = 6 y 630 % 100 = 30. Python empaqueta los dos valores en una tupla (6, 30), y euros, cents = … la desempaqueta, cada valor a su variable. Las tuplas las verás con calma en su tema; de momento quédate con que una función puede devolver varias cosas a la vez.
¿Qué pasa cuando no pones return?
Toda función en Python devuelve algo. Si no escribes return (o escribes return sin nada detrás), devuelve None, el valor que significa «nada». Aquí está el gancho del principio, desmontado:
def total_con_print(cafe, bocadillo):
print(cafe + bocadillo) # ENSEÑA el número en pantalla
def total_con_return(cafe, bocadillo):
return cafe + bocadillo # DEVUELVE el número a quien la llamó
a = total_con_print(1.20, 2.50) # en pantalla sale 3.7...
b = total_con_return(1.20, 2.50) # ...y aquí no sale nada
print("a vale", a)
print("b vale", b)
print(type(a), type(b))
Salida:
3.7 a vale None b vale 3.7 <class 'NoneType'> <class 'float'>
Las dos funciones «calculan» 3.7, pero solo una lo entrega:
total_con_printescribe 3.7 en la pantalla (la primera línea de la salida) y, como no tienereturn, devuelveNone. La pantalla es un callejón sin salida: lo que va ahí lo ves tú, pero el programa ya no lo puede usar.total_con_returnno escribe nada, pero devuelve 3.7 a quien la llama, que lo guarda enb.
Fíjate en el dibujo: la columna oscura es lo que ves en pantalla y la de la derecha, lo que vuelve a la variable. El gancho fallaba porque total recibía lo de la derecha (None), aunque en la pantalla estuviera el 3.7.
Confiar en lo que ves en la pantalla para saber qué devuelve una función. Si vas a usar el resultado en otro sitio (guardarlo, sumarlo, compararlo, pasarlo a otra función), la función necesita return. print solo sirve para enseñar.
Una pregunta de test que circula mucho es «¿cuál de estas funciones devuelve el número 10?». La respondemos ejecutándolas:
def opcion_a():
10 # calcula 10... y lo tira
def opcion_b():
print(10) # lo enseña, pero devuelve None
def opcion_c():
return 10 # lo devuelve
print("a:", opcion_a())
print("b:", opcion_b())
print("c:", opcion_c())
Salida:
a: None 10 b: None c: 10
opcion_acalcula el 10 en una línea suelta y lo tira: devuelveNone.opcion_blo imprime (por eso sale un10suelto antes deb: None: Python ejecuta la llamada para saber qué imprimir, y la llamada hace suprinten ese momento), pero devuelveNone.opcion_ces la única conreturn 10.
En ese test suele aparecer una cuarta opción, def my_function(10): return, que ni siquiera se puede definir: un parámetro tiene que ser un nombre, no un número, y Python da SyntaxError antes de ejecutar nada.
Funciones que llaman a otras funciones
Aquí empieza lo bueno. Una función puede llamar a otra, y esa a otra. Cada una hace una cosa pequeña y bien, y juntas resuelven el problema grande. En la caja de la cafetería:
def precio_con_bono(precio):
"""Nivel 3: aplica el 10 % de descuento a un precio."""
return round(precio * 0.90, 2)
def total_pedido(cafe, bocadillo, con_bono):
"""Nivel 2: suma el pedido; con bono, descuenta cada producto."""
if con_bono:
cafe = precio_con_bono(cafe) # llama al nivel 3...
bocadillo = precio_con_bono(bocadillo) # ...dos veces
return round(cafe + bocadillo, 2)
def cobrar_pedido(cafe, bocadillo, con_bono, pago):
"""Nivel 1: calcula el total y el cambio y prepara el mensaje."""
total = total_pedido(cafe, bocadillo, con_bono) # llama al nivel 2
cambio = round(pago - total, 2)
return f"Total {total} € · cambio {cambio} €"
print(cobrar_pedido(1.20, 2.50, False, 5.00))
print(cobrar_pedido(1.20, 2.50, True, 5.00))
Salida:
Total 3.7 € · cambio 1.3 € Total 3.33 € · cambio 1.67 €
Seguimos la segunda llamada, cobrar_pedido(1.20, 2.50, True, 5.00):
| Paso | Quién trabaja | Qué hace | Devuelve |
|---|---|---|---|
| 1 | cobrar_pedido | llama a total_pedido(1.20, 2.50, True) y espera | — |
| 2 | total_pedido | como con_bono es True, llama a precio_con_bono(1.20) | — |
| 3 | precio_con_bono | 1.20 × 0.90 | 1.08 |
| 4 | total_pedido | llama a precio_con_bono(2.50) | — |
| 5 | precio_con_bono | 2.50 × 0.90 | 2.25 |
| 6 | total_pedido | 1.08 + 2.25 | 3.33 |
| 7 | cobrar_pedido | 5.00 − 3.33 y prepara el texto | "Total 3.33 € · cambio 1.67 €" |
Cada función espera a que la que ha llamado termine y le devuelva su resultado; entonces sigue por donde iba. Python lleva la cuenta de «quién espera a quién» en una estructura que se llama pila de llamadas (call stack): cada llamada pone una caja encima de la anterior, con sus propias variables, y al terminar la quita.
Dentro de total_pedido reasignamos los parámetros (cafe = precio_con_bono(cafe)). No pasa nada: son variables locales de esa llamada. El 1.20 de fuera no cambia.
La pila de llamadas cuando algo falla
¿Dónde ves la pila? En los tracebacks. Mira qué pasa cuando la mesa 3 pide la cuenta sin decir cuántos son:
def parte_de_cada_uno(total, personas):
"""Nivel 3: reparte el total entre las personas de la mesa."""
return round(total / personas, 2)
def total_mesa(cafes, bocadillos):
"""Nivel 2: suma lo que ha pedido la mesa."""
return round(cafes * 1.20 + bocadillos * 2.50, 2)
def cuenta_mesa(cafes, bocadillos, personas):
"""Nivel 1: total de la mesa y lo que paga cada persona."""
total = total_mesa(cafes, bocadillos)
return parte_de_cada_uno(total, personas)
print(cuenta_mesa(3, 2, 3)) # tres amigos
print(cuenta_mesa(2, 1, 0)) # la mesa 3 no dijo cuántos eran
Salida:
2.87
Traceback (most recent call last):
File "pila_error.py", line 18, in <module>
print(cuenta_mesa(2, 1, 0)) # la mesa 3 no dijo cuántos eran
~~~~~~~~~~~^^^^^^^^^
File "pila_error.py", line 14, in cuenta_mesa
return parte_de_cada_uno(total, personas)
File "pila_error.py", line 3, in parte_de_cada_uno
return round(total / personas, 2)
~~~~~~^~~~~~~~~~
ZeroDivisionError: float division by zero
La primera llamada funciona: 3 × 1.20 + 2 × 2.50 = 3.60 + 5.00 = 8.60, y 8.60 / 3 = 2.866… que se redondea a 2.87. La segunda divide entre cero en lo más profundo, y el traceback te enseña la pila entera en ese momento:
Cómo se lee, de abajo arriba:
- La última línea dice qué pasó:
ZeroDivisionError: float division by zero. - El último
Filedice dónde explotó: línea 3, dentro departe_de_cada_uno. - Los
Filede encima dicen cómo llegaste ahí: desde la línea 14 (encuenta_mesa), que a su vez venía de la línea 18 (el programa principal,<module>).
total_mesa no aparece porque ya había terminado (devolvió 4.9) cuando llegó el error: el traceback solo enseña las funciones que estaban esperando. Esta forma de leer tracebacks la desarrollamos en depurar en Python, y te va a ahorrar horas.
El error suele estar en la función de abajo del todo, pero la causa puede estar más arriba. Aquí parte_de_cada_uno hace bien su trabajo; quien le pasó un 0 fue la llamada de la línea 18.
La búsqueda en las bandejas, ahora con return
En los ejercicios de bucles buscábamos el primer croissant de chocolate en las bandejas de la vitrina y nos encontramos con un problema: break solo sale del bucle de dentro, y hacía falta una bandera (encontrado = True) para salir también del de fuera. Prometimos que con funciones habría una forma más limpia. Aquí está:
# La búsqueda de los ejercicios de bucles, ahora dentro de una función
def buscar_chocolate(bandejas):
"""Devuelve (bandeja, hueco) del PRIMER croissant de chocolate, o None si no hay."""
num = 0
for bandeja in bandejas:
num += 1
hueco = 0
for pieza in bandeja:
hueco += 1
if pieza == "C":
return num, hueco # sale de LOS DOS bucles (y de la función) de golpe
return None # solo se llega aquí si no hay ninguno
print(buscar_chocolate(["NNC", "CNC", "NNN"]))
print(buscar_chocolate(["NNN", "NNN"]))
b, h = buscar_chocolate(["NNN", "NCN"])
print("Chocolate en la bandeja", b, "hueco", h)
Salida:
(1, 3) None Chocolate en la bandeja 2 hueco 2
return num, hueco no sale de un bucle: sale de la función, y con ella de todos los bucles que hubiera dentro, de golpe. Sin bandera y sin break. Con ["NNC", "CNC", "NNN"] el primer chocolate está en la bandeja 1, hueco 3 y la función ni mira la bandeja 2 (que también tiene chocolate). Si recorre todo sin encontrar nada, llega al return None del final.
| Vuelta | num | pieza | hueco | ¿Es "C"? |
|---|---|---|---|---|
| 1 | 1 | "N" | 1 | no |
| 2 | 1 | "N" | 2 | no |
| 3 | 1 | "C" | 3 | sí → return 1, 3 |
Poner el return None dentro del bucle (por ejemplo, en un else). Entonces la función se rinde en la primera pieza que no es de chocolate. En los ejercicios de este tema hay un «¿Qué falla aquí?» con exactamente ese fallo.
Alcance de las variables: local frente a global
El ámbito (scope) de una variable es la zona del programa donde existe. Hay dos que te importan en FP1:
- Local: las variables creadas dentro de una función (incluidos sus parámetros). Nacen al llamarla y desaparecen cuando termina. Cada llamada tiene las suyas.
- Global: las variables creadas fuera de cualquier función, en el programa principal. Existen desde que se crean hasta que acaba el programa.
def cobrar_cafe():
precio = 1.20 # variable LOCAL: nace al llamar a la función...
print("Dentro:", precio)
cobrar_cafe() # ...y desaparece cuando la función termina
print("Fuera:", precio)
Salida:
Dentro: 1.2
Traceback (most recent call last):
File "local.py", line 7, in <module>
print("Fuera:", precio)
^^^^^^
NameError: name 'precio' is not defined
Dentro de la función, precio existe y vale 1.2. Fuera, Python no la encuentra: se borró al terminar cobrar_cafe(). Por eso el NameError.
Al revés sí funciona: desde dentro de una función se puede leer una variable global.
PRECIO_CAFE = 1.20 # constante GLOBAL (en mayúsculas): se puede LEER desde dentro
def precio_cafes(cantidad):
return round(cantidad * PRECIO_CAFE, 2)
print(precio_cafes(3))
3.6
3 × 1.20 = 3.6. Es lo normal con las constantes: valores que no cambian, escritos en mayúsculas (PRECIO_CAFE, IVA) por convención, para que se vea a simple vista que no se tocan.
El error que confunde a todo el mundo: UnboundLocalError
Leer una global desde dentro funciona. Pero ¿y si intentas cambiarla?
cafes_vendidos = 0 # contador global de la caja
def vender_cafe():
cafes_vendidos = cafes_vendidos + 1 # al ASIGNAR, Python decide que es local...
vender_cafe() # ...y la intenta leer antes de que tenga valor
Salida:
Traceback (most recent call last):
File "unbound.py", line 8, in <module>
vender_cafe() # ...y la intenta leer antes de que tenga valor
~~~~~~~~~~~^^
File "unbound.py", line 5, in vender_cafe
cafes_vendidos = cafes_vendidos + 1 # al ASIGNAR, Python decide que es local...
^^^^^^^^^^^^^^
UnboundLocalError: cannot access local variable 'cafes_vendidos' where it is not associated with a value
La regla de Python es esta: si una función asigna una variable (=, +=…) en cualquier parte de su cuerpo, esa variable es local en toda la función. Como en vender_cafe hay un cafes_vendidos = …, Python decide que cafes_vendidos es local; y al calcular cafes_vendidos + 1 intenta leer esa local, que todavía no tiene valor. De ahí el mensaje: no se puede acceder a la variable local cafes_vendidos porque no tiene valor asociado.
Hay dos arreglos, y uno es mucho mejor que el otro:
def vender_cafe(cafes_vendidos):
"""Recibe el contador y DEVUELVE el valor nuevo, sin tocar nada de fuera."""
return cafes_vendidos + 1
cafes = 0
cafes = vender_cafe(cafes) # lo que devuelve el return se guarda en la variable
cafes = vender_cafe(cafes)
print("Cafés vendidos:", cafes)
# La otra salida, la palabra global (funciona, pero úsala poco)
contador = 0
def vender_con_global():
global contador # "contador" es la de fuera, no una local nueva
contador += 1
vender_con_global()
vender_con_global()
print("Con global:", contador)
Salida:
Cafés vendidos: 2 Con global: 2
- El bueno: la función recibe el valor por parámetro y devuelve el nuevo con
return. Quien llama decide dónde guardarlo (cafes = vender_cafe(cafes)). Todo lo que entra y sale de la función está a la vista en la llamada. - El que existe: la palabra
globalle dice a Python «esta variable es la de fuera». Funciona, pero cualquier función puede cambiar la variable sin que lo veas en la llamada, y cuando algo sale mal no sabes quién la tocó.
Entrada por parámetros, salida por return. Si sigues esa regla, casi nunca necesitarás global, y tus funciones se podrán probar solas, con los datos que tú quieras.
¿Cuándo uso cada cosa?
| Quiero… | Uso… | Ejemplo |
|---|---|---|
| Reutilizar un cálculo | una función con return | cobrar(total, pago) |
| Que un dato sea opcional | un parámetro con valor por defecto, al final | propina=0 |
| Que no se confundan los argumentos | argumentos por nombre | cobrar(pago=5.00, total=3.70) |
| Enseñar algo al usuario | print (dentro de una función de presentación) | mostrar_ticket(...) |
| Usar el resultado en otro sitio | return, nunca solo print | total = total_pedido(...) |
| Devolver dos datos | return a, b y desempaquetar | euros, cents = cambio_en_monedas(6.30) |
| Salir de varios bucles a la vez | meter la búsqueda en una función y return | buscar_chocolate(bandejas) |
| Cambiar un dato de fuera | recibirlo, devolver el nuevo y reasignar | cafes = vender_cafe(cafes) |
| Un valor fijo para todo el programa | constante global en mayúsculas (solo se lee) | PRECIO_CAFE = 1.20 |
Errores típicos con funciones
Usar print donde hacía falta return. Síntoma: TypeError con 'NoneType' en el mensaje, o una variable que vale None «sin motivo».
Olvidar los paréntesis al llamar (saludar_cliente en lugar de saludar_cliente()): no da error, pero la función no se ejecuta.
Llamar a la función antes de definirla. Python lee de arriba abajo: si la llamada está en la línea 2 y el def en la línea 10, da NameError. Define primero, llama después.
Argumentos en el orden equivocado. No da error y el resultado sale mal (el cambio negativo). Usa nombres cuando haya dudas.
Intentar cambiar una variable global sin declararla (UnboundLocalError). Mejor recibirla y devolverla.
Un return dentro de un bucle que sale demasiado pronto (normalmente un else: return False). El «no lo he encontrado» va después del bucle.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta tu código línea a línea y dibuja las variables. Con funciones es donde más brilla, porque dibuja cada llamada como una caja (un frame) con sus variables locales. Elige Python 3, pega este programa y avanza con Next:
PRECIO_CAFE = 1.20
def precio_con_bono(precio):
descuento = precio * 0.10
return round(precio - descuento, 2)
def total_pedido(cafes, con_bono):
total = cafes * PRECIO_CAFE
if con_bono:
total = precio_con_bono(total)
return round(total, 2)
def avisar(total):
print("A pagar:", total)
t = total_pedido(3, True)
r = avisar(t)
print(t, r)
Lo que debería salir al final:
A pagar: 3.24 3.24 None
Qué observar paso a paso:
- Al principio, Python recorre los tres
defsin entrar en ellos: en la zona Global frame aparecenPRECIO_CAFEy tres «function», pero ninguna caja nueva. Definir no es ejecutar. - En la llamada
total_pedido(3, True)aparece una caja nueva concafes = 3ycon_bono = True. Cuando se calculatotal, vale3.5999999999999996: es la imprecisión de los decimales que viste en variables y tipos de datos, y elroundfinal la arregla. - Dentro, la llamada a
precio_con_bonoapila otra caja encima, con suprecioy sudescuento. Esa es la pila de llamadas en directo. Cuando llega a sureturn, verás un Return value de 3.24 y la caja desaparece. - En
r = avisar(t)mira el Return value deavisar: esNone. En la consola saleA pagar: 3.24, perorrecibeNone, el mismo efecto del gancho del principio. - Fíjate en que
descuentoototalnunca aparecen en el Global frame: son locales y mueren con su caja.
Resumen
| Concepto | Lo esencial |
|---|---|
def nombre(parametros): | define la función; no la ejecuta |
nombre(argumentos) | la llama: salta al cuerpo y vuelve detrás de la llamada |
| Parámetro / argumento | hueco en la definición / valor en la llamada |
| Por posición / por nombre | f(2.50, 1.50) / f(bebida=1.50, bocadillo=2.50) |
| Valor por defecto | def f(a, b=0): los opcionales, al final |
return valor | devuelve el valor y termina la función |
Sin return | la función devuelve None |
print frente a return | enseñar frente a entregar |
return a, b | devuelve varios valores (una tupla) |
| Pila de llamadas | cada llamada apila su caja; el traceback la enseña |
| Local / global | dentro de la función / fuera; las locales mueren al terminar |
UnboundLocalError | asignaste dentro una variable de fuera: recíbela y devuélvela |
Tres ideas para llevarte:
printenseña,returnentrega. Si el resultado se va a usar en otro sitio, necesitasreturn.- Entrada por parámetros, salida por
return. Así cada función se entiende y se prueba sola. - Un traceback es la pila de llamadas: léelo de abajo arriba.
Ahora toca practicar: en la práctica de funciones en Python construimos un conversor de temperaturas, una calculadora modular y un piedra, papel o tijera, separando lo que calcula de lo que se ve. Y cuando tengas muchas funciones y quieras usarlas en varios programas sin copiarlas, el siguiente paso es guardarlas en archivos propios: los módulos en Python.
Siguiente paso: prácticaFunciones en Python — práctica con un conversor de temperaturas, una calculadora modular y piedra, papel o tijera (y el menú que dejó de pedir datos)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →




Qué es un sistema operativo — teoría con tu portátil en una tarde de estudio, una llamada a write() y la CPU que nunca descansa (y los tres programas que no se ejecutaban a la vez)