1.8 · Funciones¶
Objetivos. Al terminar este capítulo podrás: definir, llamar y documentar
funciones con parámetros, valores por defecto y retorno; reconocer
*args/**kwargs, lambda y los ámbitos de las variables; y comprender, como
ampliación, el funcionamiento de decoradores, recursión y generadores.
Evidencia de logro. Escribirás un módulo con funciones reutilizables, docstrings y pruebas manuales de sus contratos. Como ampliación aplicarás al menos una de estas técnicas: un decorador, un generador o una función recursiva.
Contexto y motivación¶
Repetir código aumenta la probabilidad de cometer errores. Las funciones
permiten definir la lógica una vez y reutilizarla mediante parámetros. Son
una herramienta fundamental para organizar programas y librerías: cuando usas
len() o print(), estás llamando funciones.
Además de lo básico, este capítulo introduce dos herramientas muy usadas en IA y en el resto del módulo:
- Decoradores: "envolver" una función para añadirle comportamiento (medir tiempo, registrar, validar). Los verás en FastAPI (UD2).
- Generadores: producir resultados de uno en uno sin guardarlos todos en memoria (esencial para datos grandes).
Vocabulario¶
| Término | Significado |
|---|---|
| Función | Bloque con nombre que recibe entradas y devuelve salidas |
| Parámetro | Variable que la función espera al llamarla |
| Argumento | Valor que le pasas al llamarla |
Retorno (return) |
Valor que la función devuelve |
| Ámbito (scope) | Dónde existe una variable |
| Decorador | Función que envuelve otra para extenderla |
| Generador | Función que produce valores de uno en uno con yield |
lambda |
Función anónima de una línea |
Prerrequisitos¶
1.5 · Bucles, 1.6 · Listas y tuplas y 1.7 · Diccionarios y conjuntos, además de saber manejar variables.
Ruta recomendada¶
Este capítulo reúne más herramientas de las que conviene abordar en una sola sesión:
- Núcleo obligatorio: secciones 1–3 y 7 (definir, llamar, devolver y documentar funciones).
- Ampliación guiada: secciones 4–6 (
*args,**kwargs, ámbitos ylambda). - Profundización: secciones 8–10 (recursión, decoradores y generadores).
La práctica debe demostrar primero el núcleo. Después elige una profundización según el ritmo del grupo; el resto puede quedar como lectura o demostración del docente. En FastAPI y en el trabajo con datos volverás a estas ideas cuando sean necesarias.
1. Definir y llamar funciones¶
Salida esperada:
def nombre(): define la función; el cuerpo va indentado, igual que en un if.
Definir no ejecuta nada: solo le da nombre a un bloque de código. El código
se ejecuta cuando llamas a la función escribiendo su nombre con paréntesis,
saludar(), y puedes llamarla tantas veces como quieras. Por eso la definición
tiene que aparecer antes que la primera llamada.
2. Parámetros y retorno¶
Salida esperada:
a y b son los parámetros: variables que reciben los valores con los que
se llama (3 y 4, los argumentos). La diferencia con imprimir dentro es el
return: la función devuelve un valor que quien la llama puede guardar y
seguir usando. print muestra; return comunica.
resultado = suma(3, 4) # el valor devuelto se guarda en una variable
doble = suma(resultado, resultado)
print(resultado, doble)
Salida esperada:
return no imprime
Si escribes la función sin return, devuelve None y la suma se pierde:
Salida esperada:
3. Valores por defecto y argumentos con nombre¶
Salida esperada:
def info(nombre, edad):
return f"{nombre} ({edad})"
print(info(edad=20, nombre="Ana")) # por nombre: orden indiferente
Salida esperada:
Los argumentos con nombre (edad=20) hacen la llamada más clara y no dependen
del orden. Los valores por defecto permiten omitir parámetros.
Nunca uses una lista mutable como valor por defecto
El valor por defecto se crea una sola vez, al definir la función. Si es una lista, todas las llamadas comparten la misma lista y acumulan datos:
def agregar_mal(elemento, items=[]):
items.append(elemento)
return items
print(agregar_mal("a"))
print(agregar_mal("b")) # esperabas ['b']
Salida esperada:
La solución es usar None como valor por defecto y crear la lista dentro:
def agregar(elemento, items=None):
if items is None:
items = []
items.append(elemento)
return items
print(agregar("a"))
print(agregar("b"))
Salida esperada:
4. Ampliación: *args y **kwargs¶
Antes de nada, recuerda que hay dos maneras de pasar argumentos a una función:
- Posicionales: en el orden que define la función, sin nombre.
f(1, 2). - Con nombre (keyword): indicando
nombre=valor. En el apartado 3 ya lo usaste:info(edad=20, nombre="Ana").
Con *args y **kwargs una función recibe una cantidad variable de
argumentos. La diferencia es qué tipo captura cada uno:
4.1 *args: posicionales sobrantes → una tupla¶
Salida esperada:
*args empaqueta todos los argumentos posicionales (los "sin nombre")
que queden sin asignar en una tupla. Aquí args = (1, 2, 3), y sum() los
suma.
4.2 **kwargs: con nombre sobrantes → un diccionario¶
Salida esperada:
**kwargs empaqueta todos los argumentos con nombre que queden sin
asignar en un diccionario. Aquí kwargs = {"a": 1, "b": 2}.
Los nombres args y kwargs son convención
Lo que importa son los asteriscos, no el nombre. Podrían ser *valores y
**opciones:
*algo→ recoge posicionales en tupla.**algo→ recoge con nombre en dict.
4.3 Combinar ambos¶
Puedes definir parámetros fijos, *args y **kwargs a la vez:
Salida esperada:
Analiza la llamada f(1, 2, 3, 4, x=5) paso a paso:
1y2son posicionales → van aayb(los primeros parámetros).3y4sobran como posicionales → aargs = (3, 4)(tupla).x=5es un argumento con nombre no previsto → akwargs = {"x": 5}.
El resultado es la tupla (a, b, args, kwargs) = (1, 2, (3, 4), {"x": 5}).
Lo usarás al escribir decoradores y funciones wrapper, que reciben argumentos cualesquiera y los reenvían a otra función.
4.4 Regla de orden (importante)¶
Al combinar, Python exige un orden fijo en la firma. Si lo incumples, da
SyntaxError:
Es decir, de izquierda a derecha:
- Parámetros normales (posicionales o con nombre):
a,b. *args(posicionales sobrantes → tupla).- Parámetros solo-por-clave (keyword-only), que van detrás de
*args. **kwargsal final (con nombre sobrantes → dict).
Salida esperada:
Fíjate en c: al ir después de *args, solo puede pasarse por nombre
(c=4). Si escribes j(1, 2, 3, 4, x=5), ese 4 NO va a c: por ser
posicional, acaba en args.
def k(a, *args, **kwargs): ... # OK
def j(a, *args, c, **kwargs): ... # OK
def h(a, **kwargs, b): ... # ✗ SyntaxError: un parámetro tras **kwargs
Y hay una segunda regla en la llamada: los argumentos posicionales van antes que los con nombre:
Clave para recordarlo
Orden de definición: normales → *args → solo-por-clave → **kwargs.
**kwargs siempre al final; tras *args, todo lo que viene es
solo-por-clave. Y un * solo, sin nombre (p. ej. def f(a, *, c)), también
obliga a pasar c por nombre. En la llamada, primero posicionales, luego con nombre.
5. Ampliación: ámbito de variables (scope)¶
Salida esperada:
La x dentro de f es local: solo existe mientras se ejecuta la función y
no toca la de fuera. Asignar dentro de una función crea una variable local, no
modifica la global. Una función puede leer una variable global, pero para
modificarla necesita declararla con global x, y eso es mala práctica.
Regla: no dependas de variables globales dentro de las funciones; pásalas como parámetro y devuelve resultados. Hace el código predecible y testeable.
5.1 ¿Y si quiero modificar una global? global¶
Si dentro de una función escribes x = 99, Python crea una variable local y
no toca la global x:
Salida esperada:
Y si la función lee la variable y luego la asigna, Python la considera local en toda la función, así que la lectura falla porque aún no tiene valor:
contador = 0
def incrementar_mal():
contador = contador + 1 # la lee antes de haberla creado como local
return contador
incrementar_mal()
Salida esperada (error):
Para modificar la variable global existe global x:
Salida esperada:
Entiéndelo, pero evítalo
global funciona, pero es mala práctica: hace que el resultado de la
función dependa de un estado externo oculto, y es difícil de depurar y de
probar. La alternativa limpia es pasar el valor como parámetro y devolver
el resultado:
5.2 Ampliación: nonlocal en funciones anidadas¶
Si tienes una función dentro de otra y quieres modificar una variable del
ámbito intermedio (ni local, ni global), usa nonlocal:
def contador():
n = 0
def subir():
nonlocal n
n += 1
return n
return subir
c = contador()
print(c(), c(), c())
Salida esperada:
nonlocal busca la variable en la función envolvente (no en la global). Es
útil en closures (funciones que "recuerdan" estado) y en decoradores.
6. Ampliación: funciones lambda (anónimas)¶
Una lambda es una función anónima de una sola expresión, sin nombre
asociado. La guardas en una variable y la llamas como a cualquier función:
Salida esperada:
Una lambda no permite hacer nada que no pudieras expresar con
def cuad(x): return x**2; resulta útil cuando necesitas una función puntual
sin definirla con un nombre. Es habitual usarla con sorted, map y filter,
donde la función se necesita solo para esa operación:
personas = [("Ana", 20), ("Luis", 25), ("Marta", 18)]
print(sorted(personas, key=lambda p: p[1])) # por edad
print(sorted(personas, key=lambda p: p[0], reverse=True)) # por nombre, desc
print(list(map(lambda x: x ** 2, [1, 2, 3])))
print(list(filter(lambda x: x % 2 == 0, range(10))))
Salida esperada:
[('Marta', 18), ('Ana', 20), ('Luis', 25)]
[('Marta', 18), ('Luis', 25), ('Ana', 20)]
[1, 4, 9]
[0, 2, 4, 6, 8]
key=lambda p: p[1] dice "ordena por el segundo elemento". La lambda recibe un
elemento y devuelve la clave por la que ordenar.
Puede recibir varios argumentos:
Salida esperada:
Cuándo NO usar lambda
Una lambda solo admite una expresión (no sentencias como if/for ni
asignaciones). Si la lógica necesita más de una línea, usa def, que además
se puede documentar y probar. Regla: lambda para una función diminuta y de
un solo uso; def para todo lo demás.
7. Documentación con docstrings¶
Los docstrings son las cadenas que van justo después de def y documentan la
función. Se consultan con help(f) o f.__doc__:
def promedio(notas):
"""Calcula el promedio de una lista de notas.
Args:
notas: lista de números.
Returns:
float: la media.
"""
return sum(notas) / len(notas)
help(promedio)
Salida esperada:
Help on function promedio in module __main__:
promedio(notas)
Calcula el promedio de una lista de notas.
Args:
notas: lista de números.
Returns:
float: la media.
Un docstring no es un comentario: es legible desde el código (help, el
autocompletado del editor) y aparece donde se documenta. Es buena práctica
escribirlo desde el principio, sobre todo en funciones que reutilizarás.
Una línea o varias
Para funciones cortas basta una línea: """Devuelve la suma de a y b.""".
Para funciones importantes, documenta los args y el return. Lo
verás al escribir tus propios módulos (capítulo 1.12).
8. Profundización: recursividad¶
Una función recursiva se llama a sí misma. El ejemplo clásico, el factorial, tiene dos partes esenciales:
def factorial(n):
if n <= 1: # caso base
return 1
return n * factorial(n - 1) # caso recursivo (se acerca al base)
print(factorial(5))
Salida esperada:
Sin el caso base (if n <= 1) la función se llamaría para siempre y
terminaría en RecursionError. El caso recursivo reduce el problema hasta
llegar al base.
La recursión brilla en estructuras anidadas (listas de listas, árboles, ficheros), donde el problema se repite dentro de sí mismo:
def suma_anidada(lista):
total = 0
for elem in lista:
if isinstance(elem, list):
total += suma_anidada(elem) # bajamos un nivel
else:
total += elem
return total
print(suma_anidada([1, [2, [3, 4]], 5]))
Salida esperada:
suma_anidada recorre la lista; si un elemento es otra lista, se llama a sí
misma. Es la misma idea que recorrer carpetas dentro de carpetas.
Límite de recursión
Python limita la profundidad de recursión (por defecto 1000) para proteger la
memoria: import sys; sys.getrecursionlimit() → 1000. Si te acercas al
límite con datos grandes, revisa si un bucle es mejor.
¿Recursión o bucle?
Para recorridos lineales, un bucle suele ser más legible y sin límite de profundidad. Usa recursión cuando la estructura sea naturalmente anidada (árboles, JSON, ficheros). Es común que la recursión se pueda reescribir con un bucle explícito.
9. Profundización: decoradores¶
Un decorador envuelve una función para añadirle comportamiento sin modificar su código. En términos sencillos, es una función que recibe una función y devuelve otra que la envuelve.
import time
def crono(func):
def envoltura(*args, **kwargs):
t0 = time.perf_counter()
resultado = func(*args, **kwargs)
t1 = time.perf_counter()
print(f"[{func.__name__}] {t1 - t0:.6f}s")
return resultado
return envoltura
@crono
def trabajo():
return sum(range(1000))
print("Resultado:", trabajo())
Salida esperada:
El bloque es de solo sintaxis porque el tiempo cambia según el equipo, la
carga de la CPU y la versión de Python: no hay nada que comparar. Lo que sí es
fiable es Resultado: 499500, la suma de los números del 0 al 999.
Qué hace el decorador: @crono es azúcar sintáctico de
trabajo = crono(trabajo). Sustituye trabajo por envoltura, que mide, llama
a la original y devuelve su resultado. Por eso el decorador debe devolver
el valor de func: si se olvidara, la función devolvería None.
9.1 functools.wraps: conservar nombre y docstring¶
Por defecto, envoltura "oculta" el nombre y el docstring de la función original.
Para conservarlos, decora la envoltura con @functools.wraps:
import functools
import time
def crono(func):
@functools.wraps(func)
def envoltura(*args, **kwargs):
t0 = time.perf_counter()
r = func(*args, **kwargs)
t1 = time.perf_counter()
print(f"[{func.__name__}] {t1 - t0:.6f}s")
return r
return envoltura
@crono
def trabajo():
return sum(range(1000))
print(trabajo.__name__) # sigue llamándose "trabajo"
Salida esperada:
9.2 Decorador de registro (logging)¶
import functools
def registrar(func):
@functools.wraps(func)
def envoltura(*args, **kwargs):
print(f"Llamando a {func.__name__}{args}{kwargs}")
return func(*args, **kwargs)
return envoltura
@registrar
def suma(a, b):
return a + b
print("suma:", suma(3, 4))
Salida esperada:
Este patrón (medir, registrar, validar) es exactamente lo que usarán FastAPI y
el logging en la UD2. Suele acompañarse de *args/**kwargs para que la
envoltura acepte cualquier firma.
9.3 Ampliación: functools.lru_cache (caché)¶
Para funciones costosas y repetidas, lru_cache guarda los resultados:
import functools
@functools.lru_cache(maxsize=None)
def fib(n):
return n if n <= 1 else fib(n - 1) + fib(n - 2)
print("fib(30):", fib(30))
Salida esperada:
Sin caché, fib(30) haría millones de llamadas recursivas; con ella, cada
resultado se calcula una vez. Este decorador de la biblioteca estándar muestra
cómo una función puede añadir un comportamiento reutilizable a otra.
10. Profundización: generadores (yield)¶
Un generador produce valores de uno en uno y no guarda todos en memoria. La
diferencia con return es que yield pausa la función y conserva su estado;
la siguiente llamada continúa donde se quedó.
def contar_hasta(n):
for i in range(1, n + 1):
print("genero", i)
yield i
g = contar_hasta(3)
print("Primer next:", next(g))
print("Segundo next:", next(g))
Salida esperada:
Fíjate en el orden: justo antes de cada next, se ejecuta la línea
print("genero", i). Eso demuestra que el generador no corre todo de golpe: se pausa en
cada yield y retoma al pedir el siguiente valor. Es el comportamiento lazy
(perezoso).
10.1 Expresión generadora¶
Como las comprehensions, pero sin memoria (con () en vez de []):
cuadrados = (x ** 2 for x in range(5))
print("Suma:", sum(cuadrados))
print(list(x * x for x in range(4)))
Salida esperada:
sum(), max() y min() consumen el generador. A diferencia de una lista, solo
se puede recorrer una vez: tras sum(cuadrados), el generador queda agotado.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
gen = fibonacci()
print([next(gen) for _ in range(8)])
Salida esperada:
Este generador es infinito (en teoría), pero solo construye los 8 primeros: produce bajo demanda y siempre "queda" para el siguiente.
Generador vs lista
list(...) materializa todo y ocupa memoria; el generador produce de uno en
uno. Esto es la diferencia entre procesar un fichero de millones de líneas
(generador, línea a línea) y quedarte sin memoria al cargarlo entero. Usa el
generador cuando no necesites todos los valores a la vez.
Errores frecuentes¶
- Olvidar
returny que la función devuelvaNone. - Usar una lista/dict mutable como valor por defecto (se comparte).
- Confundir
print(muestra) conreturn(devuelve). - Modificar una variable global dentro de una función sin
globaly tener unUnboundLocalError. - Pasarse de
lambda: si hay que hacer mucho, usadef. - Creer que una
lambdaadmite varias líneas (solo acepta una expresión). - Recursión sin caso base, o con datos demasiado profundos →
RecursionError. - No entender que
yieldpausa yreturntermina. - Reutilizar un generador después de consumirlo (queda agotado; hay que crear uno nuevo).
Práctica de transferencia¶
- Retoma tu caja registradora del proyecto
1.7b. Extrae a funciones las
cuatro partes que ahora están en el
while:mostrar_menu(),anadir_al_carrito(),calcular_total()yaplicar_descuento(). Cada una con su docstring y sureturncuando toque. El bucle principal debe quedar en unas pocas líneas: esta es la prueba de que la división sirve para algo. - Escribe
area_circulo(radio)que devuelva el área (3.14159 * r**2). - Escribe
promedio(*notas)que devuelva la media (usasum/len). - Crea un decorador
registrarque imprima"Llamando a <nombre>"antes de cada llamada y lo pruebes en una función. - Escribe un generador
pares(limite)que produzca números pares hastalimite. (Evita llamarmaxal parámetro: taparía la funciónmax().) - Reescribe el factorial (apartado 8) con un bucle en lugar de recursión y compara.
Qué debes comprobar: en el punto 1, que la caja produce exactamente el mismo
ticket que antes de extraer las funciones (misma sesión, misma salida); en el 3,
qué ocurre si llamas a promedio() sin notas y cómo lo evitarías.
Producto evaluable¶
Cuaderno 06_funciones.py con:
- Funciones con
return, valores por defecto y docstrings (área, promedio, formato), cada una con su celda Markdown que interprete el resultado. - Una demostración de
*argso**kwargs, explicando qué valores se empaquetan y en qué estructura. - Una ampliación elegida: decorador
crono, generadorfibonaccio función recursiva. Elige una y explica por qué es adecuada; las otras pueden quedar como demostración opcional. - Una celda que demuestre el bug de la lista mutable por defecto y su
solución con
None.
Formato de entrega: cuaderno marimo ejecutado, con salida esperada, interpretación y conclusión.
Resumen y referencia rápida¶
| Herramienta | Uso | Ejemplo |
|---|---|---|
def f(a, b=1) |
Función con por defecto | def saludo(nombre="mundo") |
return |
Devolver un valor | return a + b |
*args / **kwargs |
Argumentos variables | def f(*a, **k) |
lambda |
Función anónima corta | lambda x: x**2 |
| Docstring | Documentar | def f(...): """...""" |
Decorador @ |
Envolver una función | @crono |
@functools.wraps |
Conservar nombre/doc del decorado | @functools.wraps(func) |
yield |
Generador (uno a uno) | def g(): yield x |
(x for x in ...) |
Expresión generadora (sin memoria) | sum(x**2 for x in r) |
Siguiente: 1.9 · Cadenas.