*args, **kwargs y lambda en Python
En esta lección
- El error clásico: valores por defecto mutables
- *args: cualquier número de argumentos posicionales
- **kwargs: cualquier número de argumentos con nombre
- Desempaquetar al llamar
- Parámetros solo posicionales y solo con nombre
- Funciones lambda
- Las funciones son objetos
- Funciones anidadas y closures
- Ámbito: la regla LEGB
- Decoradores
- Recursividad
- Errores frecuentes
- Resumen
En la lección de funciones aprendiste a definirlas con def, devolver valores con return y usar parámetros con valor por defecto. Ahora vas a dar el siguiente paso: funciones que aceptan cualquier número de argumentos, funciones sin nombre (lambda), funciones que crean otras funciones, las reglas exactas del ámbito de las variables y los decoradores. Son herramientas que verás constantemente en código real y en librerías como Flask o Django.
El error clásico: valores por defecto mutables
Empecemos por una trampa en la que cae casi todo el mundo. Esta función añade una tarea a una lista, y si no le pasas lista, crea una nueva… o eso parece:
def agregar_tarea(tarea, lista=[]):
lista.append(tarea)
return lista
print(agregar_tarea("estudiar")) # ['estudiar']
print(agregar_tarea("comprar")) # ['estudiar', 'comprar'] ¡¿?!
La segunda llamada “recuerda” la tarea de la primera. ¿Por qué? Porque el valor por defecto se crea una sola vez, cuando Python lee el def, no en cada llamada. Todas las llamadas comparten la misma lista.
La solución estándar es usar None como valor por defecto y crear la lista dentro:
def agregar_tarea(tarea, lista=None):
if lista is None:
lista = []
lista.append(tarea)
return lista
print(agregar_tarea("estudiar")) # ['estudiar']
print(agregar_tarea("comprar")) # ['comprar']
Cuidado: nunca uses como valor por defecto una lista, un diccionario o un conjunto (
[],{},set()). Los números, cadenas, tuplas,True/FalseyNonesí son seguros porque no se pueden modificar.
*args: cualquier número de argumentos posicionales
A veces no sabes cuántos valores te van a pasar. Un asterisco delante del parámetro recoge todos los argumentos posicionales sobrantes en una tupla:
def sumar(*numeros):
print(numeros)
return sum(numeros)
print(sumar(1, 2, 3)) # (1, 2, 3) y luego 6
print(sumar()) # () y luego 0
El nombre args es solo una convención; lo importante es el *. Usa un nombre descriptivo cuando puedas. Se puede combinar con parámetros normales, que van antes:
def nota_media(alumno, *notas):
if not notas:
return f"{alumno}: sin notas"
return f"{alumno}: {sum(notas) / len(notas):.2f}"
print(nota_media("Ana", 7, 8.5, 9)) # Ana: 8.17
print(nota_media("Luis")) # Luis: sin notas
Así funciona el propio print, que admite tantos valores como quieras.
**kwargs: cualquier número de argumentos con nombre
Dos asteriscos recogen los argumentos con nombre sobrantes en un diccionario:
def crear_perfil(nombre, **datos):
print(datos)
perfil = {"nombre": nombre}
perfil.update(datos)
return perfil
print(crear_perfil("Marta", edad=21, ciudad="Bilbao"))
# {'edad': 21, 'ciudad': 'Bilbao'}
# {'nombre': 'Marta', 'edad': 21, 'ciudad': 'Bilbao'}
Puedes usar los dos a la vez. El orden obligatorio es: normales, *args, **kwargs.
def registrar(evento, *args, **kwargs):
print(evento, args, kwargs)
registrar("login", "ana", 3, ip="1.2.3.4", ok=True)
# login ('ana', 3) {'ip': '1.2.3.4', 'ok': True}
Desempaquetar al llamar
Los asteriscos también funcionan al revés, al llamar a una función: * reparte una lista o tupla en argumentos posicionales y ** reparte un diccionario en argumentos con nombre.
def area(base, altura):
return base * altura
medidas = [5, 3]
print(area(*medidas)) # 15 (igual que area(5, 3))
datos = {"altura": 2, "base": 10}
print(area(**datos)) # 20 (igual que area(base=10, altura=2))
print(*["a", "b", "c"], sep=" | ") # a | b | c
Es muy útil cuando los datos te llegan ya en una lista o un diccionario (por ejemplo, leídos de un fichero JSON).
Parámetros solo posicionales y solo con nombre
Puedes obligar a que ciertos parámetros se pasen de una forma concreta:
- Todo lo que va antes de
/solo se puede pasar por posición (desde Python 3.8). - Todo lo que va después de
*solo se puede pasar por nombre.
def potencia(base, exponente, /):
return base ** exponente
print(potencia(2, 10)) # 1024
potencia(base=2, exponente=10) # TypeError
def conectar(host, *, puerto=5432, ssl=False):
return f"{host}:{puerto} ssl={ssl}"
print(conectar("localhost", puerto=3306, ssl=True)) # localhost:3306 ssl=True
conectar("localhost", 3306) # TypeError: takes 1 positional argument but 2 were given
¿Para qué sirve? Los parámetros solo con nombre evitan llamadas confusas como conectar("localhost", 3306, True), donde nadie sabe qué es ese True. Los solo posicionales te permiten cambiar el nombre del parámetro más adelante sin romper el código de quien use la función.
Funciones lambda
Una lambda es una función anónima de una sola expresión. Su resultado se devuelve automáticamente, sin return:
doble = lambda x: x * 2
print(doble(5)) # 10
Asignar una lambda a una variable, como arriba, no tiene sentido: para eso está def. Las lambdas brillan cuando una función necesita recibir otra función pequeña y de usar y tirar. El caso más típico es el parámetro key de sorted, min y max, que indica por qué criterio comparar:
alumnos = [("Ana", 8.5), ("Luis", 6), ("Marta", 9.2), ("Pablo", 7)]
print(sorted(alumnos, key=lambda a: a[1]))
# [('Luis', 6), ('Pablo', 7), ('Ana', 8.5), ('Marta', 9.2)]
print(max(alumnos, key=lambda a: a[1])) # ('Marta', 9.2)
frutas = ["kiwi", "pera", "fresa", "uva"]
print(sorted(frutas, key=lambda p: (len(p), p)))
# ['uva', 'kiwi', 'pera', 'fresa'] (por longitud y, si empatan, alfabético)
key también admite funciones ya existentes, sin lambda:
nombres = ["marta", "Ana", "Luis", "bea"]
print(sorted(nombres)) # ['Ana', 'Luis', 'bea', 'marta']
print(sorted(nombres, key=str.lower)) # ['Ana', 'bea', 'Luis', 'marta']
Sin key, las mayúsculas van antes que las minúsculas; con str.lower se ordena sin distinguirlas.
map y filter
map aplica una función a cada elemento y filter se queda con los que devuelven True. Devuelven objetos perezosos, por eso los pasamos a list:
precios = [10, 25, 8]
print(list(map(lambda p: p * 1.21, precios))) # [12.1, 30.25, 9.68]
print(list(filter(lambda p: p > 9, precios))) # [10, 25]
En Python se prefieren las comprensiones, que se leen mejor: [p * 1.21 for p in precios] y [p for p in precios if p > 9]. Conviene conocer map y filter porque aparecen en mucho código.
Las funciones son objetos
En Python una función es un valor más, como un número o una lista. Puedes guardarla en una variable, pasarla como argumento o meterla en un diccionario:
def saludar(nombre):
return f"Hola, {nombre}"
decir = saludar # sin paréntesis: no la llamas, la referencias
print(decir("Eva")) # Hola, Eva
def aplicar(funcion, valores):
return [funcion(v) for v in valores]
print(aplicar(len, ["hola", "sol"])) # [4, 3]
operaciones = {
"+": lambda a, b: a + b,
"-": lambda a, b: a - b,
"*": lambda a, b: a * b,
}
print(operaciones["*"](6, 7)) # 42
El diccionario de operaciones sustituye a una larga cadena de if/elif: para añadir una operación nueva solo añades una entrada.
Funciones anidadas y closures
Una función puede definirse dentro de otra y devolverse. La función interna recuerda las variables de la externa aunque esta ya haya terminado. Eso se llama closure (clausura):
def crear_descuento(porcentaje):
def aplicar(precio):
return round(precio * (1 - porcentaje / 100), 2)
return aplicar
rebajas = crear_descuento(20)
black_friday = crear_descuento(50)
print(rebajas(50)) # 40.0
print(black_friday(50)) # 25.0
crear_descuento es una “fábrica de funciones”: cada llamada fabrica una función configurada con su propio porcentaje.
Ámbito: la regla LEGB
Cuando usas un nombre, Python lo busca en este orden y se queda con el primero que encuentra:
| Letra | Ámbito | Dónde |
|---|---|---|
| L | Local | Dentro de la función actual |
| E | Enclosing | En las funciones que la contienen |
| G | Global | En el nivel superior del módulo (el archivo) |
| B | Built-in | Los nombres propios de Python: len, print, sum… |
x = "global"
def exterior():
x = "enclosing"
def interior():
x = "local"
print(x) # local
interior()
print(x) # enclosing
exterior()
print(x) # global
global y nonlocal
Leer una variable de fuera funciona sin más. Pero si asignas un valor a un nombre dentro de una función, Python lo considera local:
contador = 0
def incrementar():
contador += 1 # UnboundLocalError
incrementar()
Para modificar una variable global hay que declararlo con global:
def incrementar():
global contador
contador += 1
incrementar()
incrementar()
print(contador) # 2
Y para modificar una variable de la función que te contiene, nonlocal:
def crear_contador():
cuenta = 0
def siguiente():
nonlocal cuenta
cuenta += 1
return cuenta
return siguiente
turno = crear_contador()
print(turno(), turno(), turno()) # 1 2 3
Consejo: evita
globalsiempre que puedas. Una función que cambia variables de fuera es difícil de probar y de entender. Lo limpio es recibir datos por parámetros y devolverlos conreturn. Si necesitas guardar estado, una clase suele ser mejor opción.
Decoradores
Un decorador es una función que recibe una función y devuelve otra que la envuelve, añadiéndole comportamiento sin tocar su código. Juntando todo lo anterior (funciones como objetos, closures y *args, **kwargs), podemos medir cuánto tarda cualquier función:
import time
def medir_tiempo(funcion):
def envoltura(*args, **kwargs):
inicio = time.perf_counter()
resultado = funcion(*args, **kwargs)
fin = time.perf_counter()
print(f"{funcion.__name__} tardó {fin - inicio:.4f} s")
return resultado
return envoltura
@medir_tiempo
def sumar_hasta(n):
return sum(range(n))
print(sumar_hasta(1_000_000))
# sumar_hasta tardó 0.0111 s (el tiempo varía en cada equipo)
# 499999500000
La línea @medir_tiempo es una forma abreviada de escribir sumar_hasta = medir_tiempo(sumar_hasta). Gracias a *args, **kwargs, la envoltura sirve para funciones con cualquier número de parámetros.
Consejo: en código real añade
@functools.wraps(funcion)encima dedef envoltura. Así la función decorada conserva su nombre y su docstring originales.
Verás decoradores por todas partes: @property y @classmethod en las clases, @app.route("/") en Flask, @pytest.fixture en los tests.
Recursividad
Una función recursiva se llama a sí misma. Necesita siempre un caso base que detenga las llamadas:
def factorial(n):
if n <= 1: # caso base
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120 (5 * 4 * 3 * 2 * 1)
Es natural para estructuras anidadas, como sumar listas dentro de listas:
def suma_anidada(datos):
total = 0
for d in datos:
if isinstance(d, list):
total += suma_anidada(d)
else:
total += d
return total
print(suma_anidada([1, [2, 3], [4, [5, 6]]])) # 21
Python limita la profundidad a unas 1000 llamadas. Si olvidas el caso base obtendrás RecursionError: maximum recursion depth exceeded. Para recorridos simples, un bucle es más eficiente.
Errores frecuentes
- Lista o diccionario como valor por defecto: se comparte entre llamadas. Usa
None. - Poner
**kwargsantes de*args: el orden es normales,*args,**kwargs. - Llamar a la función al pasarla:
sorted(datos, key=str.lower())falla; se pasa sin paréntesis:key=str.lower. - Lambdas demasiado largas: si no cabe en una línea clara, usa
def. - Asignar a una variable global sin
global: provocaUnboundLocalError. - Recursión sin caso base: termina en
RecursionError.
Resumen
| Herramienta | Para qué sirve | Ejemplo |
|---|---|---|
param=None | Valor por defecto mutable seguro | def f(lista=None) |
*args | Recoger posicionales en una tupla | def sumar(*nums) |
**kwargs | Recoger con nombre en un diccionario | def perfil(**datos) |
* y ** al llamar | Desempaquetar lista o diccionario | area(*medidas) |
/ y * en la firma | Solo posicionales / solo con nombre | def f(a, /, *, b) |
lambda | Función corta de usar y tirar | key=lambda a: a[1] |
| Closure | Función que recuerda su entorno | crear_descuento(20) |
global / nonlocal | Modificar variables de fuera | nonlocal cuenta |
| Decorador | Añadir comportamiento a una función | @medir_tiempo |
| Recursividad | Una función que se llama a sí misma | factorial(n - 1) |
En la siguiente lección verás cómo gestionar los errores con excepciones.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.