Parámetros args y kwargs
Los parámetros variables en Python nos permiten crear funciones que acepten un número flexible de argumentos, lo que resulta extremadamente útil cuando no sabemos de antemano cuántos valores recibirá nuestra función. Python proporciona dos mecanismos principales para esto: *args
para argumentos posicionales y **kwargs
para argumentos con nombre.
Parámetros *args
El parámetro *args
permite que una función reciba cualquier cantidad de argumentos posicionales. El nombre args
es una convención, pero el asterisco (*
) es lo que realmente importa. Internamente, Python empaqueta todos los argumentos adicionales en una tupla.
def saludar(*nombres):
for nombre in nombres:
print(f"Hola, {nombre}!")
# Podemos llamarla con diferentes cantidades de argumentos
saludar("Ana")
saludar("Carlos", "María", "José")
Un ejemplo más práctico sería una función que calcule el promedio de una cantidad variable de números:
def calcular_promedio(*numeros):
if len(numeros) == 0:
return 0
return sum(numeros) / len(numeros)
# Uso flexible
print(calcular_promedio(8, 9, 7)) # Resultado: 8.0
print(calcular_promedio(10, 8, 9, 7, 6)) # Resultado: 8.0
print(calcular_promedio(5)) # Resultado: 5.0
Combinando parámetros fijos con *args
Podemos combinar parámetros normales con *args
, pero los parámetros fijos deben aparecer siempre antes que *args
:
def procesar_pedido(cliente, *productos):
print(f"Cliente: {cliente}")
print("Productos solicitados:")
total = 0
for producto, precio in productos:
print(f"- {producto}: ${precio}")
total += precio
print(f"Total: ${total}")
# Uso
procesar_pedido(
"María García",
("Laptop", 800),
("Mouse", 25),
("Teclado", 60)
)
Parámetros **kwargs
El parámetro **kwargs
maneja argumentos con nombre (keyword arguments). Al igual que con *args
, el nombre kwargs
es convencional, pero los dos asteriscos (**
) son obligatorios. Python empaqueta estos argumentos en un diccionario.
def crear_perfil(nombre, **datos_extra):
perfil = {"nombre": nombre}
perfil.update(datos_extra)
return perfil
# Podemos agregar cualquier información adicional
usuario1 = crear_perfil("Ana", edad=28, ciudad="Madrid", profesion="Ingeniera")
usuario2 = crear_perfil("Carlos", edad=35, pais="España")
print(usuario1) # {'nombre': 'Ana', 'edad': 28, 'ciudad': 'Madrid', 'profesion': 'Ingeniera'}
Un caso práctico común es configurar conexiones o sistemas donde necesitamos flexibilidad:
def conectar_base_datos(host, puerto, **opciones):
print(f"Conectando a {host}:{puerto}")
# Configuraciones opcionales
if "timeout" in opciones:
print(f"Timeout configurado: {opciones['timeout']} segundos")
if "ssl" in opciones:
print(f"SSL habilitado: {opciones['ssl']}")
if "pool_size" in opciones:
print(f"Tamaño del pool: {opciones['pool_size']}")
# Diferentes formas de uso
conectar_base_datos("localhost", 5432)
conectar_base_datos("localhost", 5432, timeout=30, ssl=True)
conectar_base_datos("remoto.com", 3306, pool_size=10, timeout=60)
Combinando args y kwargs
La combinación más flexible se logra usando tanto *args
como **kwargs
en la misma función. El orden de los parámetros debe seguir esta secuencia: parámetros normales, *args
, **kwargs
.
def registrar_evento(nivel, mensaje, *detalles, **metadatos):
# Mensaje principal
print(f"[{nivel.upper()}] {mensaje}")
# Detalles adicionales (args)
if detalles:
print("Detalles:", " | ".join(detalles))
# Metadatos (kwargs)
if metadatos:
for clave, valor in metadatos.items():
print(f"{clave}: {valor}")
print("-" * 40)
# Ejemplos de uso
registrar_evento("info", "Usuario conectado")
registrar_evento(
"warning",
"Memoria baja",
"RAM al 85%",
"Swap activo",
usuario="admin",
servidor="web-01",
timestamp="2025-01-10 14:30"
)
Desempaquetado de argumentos
Python también permite desempaquetar listas y diccionarios al llamar funciones usando los mismos operadores *
y **
:
def calcular_area_rectangulo(ancho, alto):
return ancho * alto
# Desempaquetando una lista
dimensiones = [5, 3]
area = calcular_area_rectangulo(*dimensiones)
print(f"Área: {area}") # Área: 15
# Desempaquetando un diccionario
def presentar_persona(nombre, edad, ciudad):
return f"{nombre}, {edad} años, vive en {ciudad}"
datos = {"nombre": "Laura", "edad": 30, "ciudad": "Barcelona"}
presentacion = presentar_persona(**datos)
print(presentacion) # Laura, 30 años, vive en Barcelona
Esta técnica es especialmente útil cuando trabajamos con APIs o funciones que esperan múltiples parámetros pero tenemos los datos organizados en estructuras:
def enviar_email(destinatario, asunto, cuerpo, **opciones):
print(f"Para: {destinatario}")
print(f"Asunto: {asunto}")
print(f"Mensaje: {cuerpo}")
# Opciones adicionales
if opciones.get("copia"):
print(f"CC: {opciones['copia']}")
if opciones.get("prioridad"):
print(f"Prioridad: {opciones['prioridad']}")
# Datos del email
config_email = {
"destinatario": "cliente@empresa.com",
"asunto": "Confirmación de pedido",
"cuerpo": "Su pedido ha sido procesado",
"copia": "ventas@empresa.com",
"prioridad": "alta"
}
# Enviamos desempaquetando el diccionario
enviar_email(**config_email)
Los parámetros *args
y **kwargs
proporcionan flexibilidad sin sacrificar la claridad del código, permitiendo crear funciones adaptables que pueden evolucionar con las necesidades del proyecto sin romper el código existente.
Funciones lambda
Las funciones lambda son una forma concisa de crear funciones pequeñas y anónimas en Python. También conocidas como funciones anónimas, están diseñadas para situaciones donde necesitamos una función simple de una sola línea sin la necesidad de definir una función completa con def
.
La sintaxis básica de una función lambda sigue este patrón:
lambda argumentos: expresión
Una función lambda puede tener múltiples argumentos pero solo una expresión, que se evalúa y retorna automáticamente. Veamos un ejemplo simple:
# Función tradicional
def cuadrado(x):
return x ** 2
# Equivalente con lambda
cuadrado_lambda = lambda x: x ** 2
print(cuadrado(5)) # 25
print(cuadrado_lambda(5)) # 25
Casos prácticos de uso
Las funciones lambda brillan especialmente cuando se usan con funciones de orden superior como map()
, filter()
y sorted()
. Estas situaciones son donde realmente muestran su utilidad práctica.
Transformación de datos con map()
La función map()
aplica una función a cada elemento de una secuencia:
numeros = [1, 2, 3, 4, 5]
# Convertir a cuadrados
cuadrados = list(map(lambda x: x ** 2, numeros))
print(cuadrados) # [1, 4, 9, 16, 25]
# Convertir temperaturas de Celsius a Fahrenheit
celsius = [0, 20, 30, 40]
fahrenheit = list(map(lambda c: (c * 9/5) + 32, celsius))
print(fahrenheit) # [32.0, 68.0, 86.0, 104.0]
Filtrado de datos con filter()
La función filter()
crea una nueva secuencia con elementos que cumplen una condición:
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Filtrar números pares
pares = list(filter(lambda x: x % 2 == 0, numeros))
print(pares) # [2, 4, 6, 8, 10]
# Filtrar palabras largas
palabras = ["Python", "es", "un", "lenguaje", "genial"]
palabras_largas = list(filter(lambda palabra: len(palabra) > 3, palabras))
print(palabras_largas) # ['Python', 'lenguaje', 'genial']
Ordenamiento personalizado
Las lambdas son especialmente útiles para personalizar criterios de ordenamiento con sorted()
y el método .sort()
:
estudiantes = [
{"nombre": "Ana", "nota": 8.5},
{"nombre": "Carlos", "nota": 7.2},
{"nombre": "María", "nota": 9.1},
{"nombre": "José", "nota": 6.8}
]
# Ordenar por nota (descendente)
por_nota = sorted(estudiantes, key=lambda estudiante: estudiante["nota"], reverse=True)
for est in por_nota:
print(f"{est['nombre']}: {est['nota']}")
# Ordenar por longitud del nombre
por_nombre = sorted(estudiantes, key=lambda est: len(est["nombre"]))
print([est["nombre"] for est in por_nombre]) # ['Ana', 'José', 'María', 'Carlos']
Un ejemplo más complejo con ordenamiento múltiple:
productos = [
{"nombre": "Laptop", "precio": 800, "categoria": "Electrónicos"},
{"nombre": "Libro", "precio": 15, "categoria": "Educación"},
{"nombre": "Mouse", "precio": 25, "categoria": "Electrónicos"},
{"nombre": "Cuaderno", "precio": 5, "categoria": "Educación"}
]
# Ordenar por categoría y luego por precio
ordenados = sorted(productos, key=lambda p: (p["categoria"], p["precio"]))
for producto in ordenados:
print(f"{producto['categoria']}: {producto['nombre']} - ${producto['precio']}")
Lambdas con múltiples argumentos
Las funciones lambda pueden recibir múltiples parámetros, lo que las hace versátiles para operaciones más complejas:
# Operaciones matemáticas
suma = lambda x, y: x + y
producto = lambda x, y, z: x * y * z
print(suma(5, 3)) # 8
print(producto(2, 3, 4)) # 24
# Aplicación práctica: calcular descuentos
calcular_precio_final = lambda precio, descuento: precio * (1 - descuento/100)
precio_original = 100
descuento_aplicado = 20
precio_final = calcular_precio_final(precio_original, descuento_aplicado)
print(f"Precio final: ${precio_final}") # Precio final: $80.0
Uso con funciones integradas
Las lambdas se integran perfectamente con muchas funciones integradas de Python:
from functools import reduce
numeros = [1, 2, 3, 4, 5]
# Encontrar el máximo con reduce
maximo = reduce(lambda x, y: x if x > y else y, numeros)
print(f"Máximo: {maximo}") # Máximo: 5
# Calcular factorial con reduce
factorial_5 = reduce(lambda x, y: x * y, range(1, 6))
print(f"5! = {factorial_5}") # 5! = 120
Limitaciones importantes
Es crucial entender que las funciones lambda tienen limitaciones deliberadas. Solo pueden contener expresiones, no declaraciones, lo que significa que no pueden incluir:
# ❌ Esto NO funciona en lambda
# lambda x: print(x) # print es una declaración
# lambda x: x = 5 # asignación no permitida
# lambda x: if x > 0: return x # if statement no permitido
# ✅ Alternativas correctas
imprimir = lambda x: print(x) if x > 0 else None # expresión condicional
procesar = lambda x: x if x > 0 else 0 # operador ternario
Cuándo usar y cuándo evitar lambdas
Usa lambdas cuando:
- Necesites una función simple de una sola expresión
- Trabajes con
map()
,filter()
,sorted()
, etc. - La función sea tan simple que nombrarla sería redundante
# Caso ideal para lambda
precios = [19.99, 25.50, 12.30, 45.00]
precios_con_iva = list(map(lambda p: p * 1.21, precios))
Evita lambdas cuando:
- La lógica sea compleja o requiera múltiples líneas
- Necesites reutilizar la función en varios lugares
- La expresión sea difícil de entender de un vistazo
# ❌ Lambda compleja - mejor usar función normal
procesar = lambda datos: sum(x["valor"] for x in datos if x["activo"] and x["valor"] > 100)
# ✅ Función normal más clara
def procesar_datos_activos(datos):
return sum(x["valor"] for x in datos if x["activo"] and x["valor"] > 100)
Las funciones lambda son una herramienta específica en Python que, usada apropiadamente, puede hacer que tu código sea más conciso y expresivo, especialmente en operaciones de transformación y filtrado de datos.
Decoradores básicos
Los decoradores son una característica elegante de Python que nos permite modificar o extender el comportamiento de funciones sin alterar su código original. Puedes pensar en un decorador como una "envoltura" que rodea una función, añadiendo funcionalidad extra antes, después o alrededor de su ejecución normal.
En esencia, un decorador es una función que toma otra función como argumento y devuelve una versión modificada de esa función. Esta capacidad nos permite implementar funcionalidades transversales como logging, medición de tiempo, autenticación o validación de manera limpia y reutilizable.
Creando nuestro primer decorador
La forma más simple de entender los decoradores es creando uno desde cero. Comenzaremos con un decorador que mide el tiempo que tarda una función en ejecutarse:
import time
def medir_tiempo(func):
def envoltura(*args, **kwargs):
inicio = time.time()
resultado = func(*args, **kwargs)
fin = time.time()
print(f"{func.__name__} tardó {fin - inicio:.4f} segundos")
return resultado
return envoltura
# Función que queremos decorar
def operacion_lenta():
time.sleep(1)
return "Operación completada"
# Aplicamos el decorador manualmente
operacion_decorada = medir_tiempo(operacion_lenta)
operacion_decorada() # operacion_lenta tardó 1.0045 segundos
Usando la sintaxis @ (azúcar sintáctico)
Python proporciona una sintaxis más elegante para aplicar decoradores usando el símbolo @
. Esta forma es equivalente a la aplicación manual pero mucho más legible:
@medir_tiempo
def calcular_factorial(n):
if n <= 1:
return 1
resultado = 1
for i in range(2, n + 1):
resultado *= i
return resultado
# Al llamar la función, el decorador se ejecuta automáticamente
print(calcular_factorial(10)) # calcular_factorial tardó 0.0001 segundos
Decorador de logging
Un caso de uso muy común es registrar información sobre las llamadas a funciones. Creemos un decorador que registre cuándo se llama una función y con qué argumentos:
def registrar_llamadas(func):
def envoltura(*args, **kwargs):
argumentos = []
if args:
argumentos.extend([str(arg) for arg in args])
if kwargs:
argumentos.extend([f"{k}={v}" for k, v in kwargs.items()])
args_str = ", ".join(argumentos) if argumentos else "sin argumentos"
print(f"Llamando a {func.__name__}({args_str})")
resultado = func(*args, **kwargs)
print(f"{func.__name__} devolvió: {resultado}")
return resultado
return envoltura
@registrar_llamadas
def dividir(a, b):
return a / b
@registrar_llamadas
def saludar(nombre, saludo="Hola"):
return f"{saludo}, {nombre}!"
# Probamos las funciones
print(dividir(10, 2))
print(saludar("Ana", saludo="Buenos días"))
Decorador de validación
Los decoradores son excelentes para validar argumentos antes de que la función principal se ejecute:
def validar_positivo(func):
def envoltura(*args, **kwargs):
# Validamos que todos los argumentos numéricos sean positivos
for arg in args:
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(f"Todos los argumentos deben ser positivos, recibido: {arg}")
for valor in kwargs.values():
if isinstance(valor, (int, float)) and valor <= 0:
raise ValueError(f"Todos los argumentos deben ser positivos, recibido: {valor}")
return func(*args, **kwargs)
return envoltura
@validar_positivo
def calcular_area_rectangulo(ancho, alto):
return ancho * alto
@validar_positivo
def calcular_interes(capital, tasa, tiempo):
return capital * (tasa / 100) * tiempo
# Uso correcto
print(calcular_area_rectangulo(5, 3)) # 15
# Esto lanzará una excepción
try:
print(calcular_interes(1000, -5, 2))
except ValueError as e:
print(f"Error: {e}")
Aplicando múltiples decoradores
Una función puede tener múltiples decoradores aplicados. Los decoradores se aplican de abajo hacia arriba, creando capas de funcionalidad:
def decorador_superior(func):
def envoltura(*args, **kwargs):
print("Antes del decorador superior")
resultado = func(*args, **kwargs)
print("Después del decorador superior")
return resultado
return envoltura
def decorador_inferior(func):
def envoltura(*args, **kwargs):
print("Antes del decorador inferior")
resultado = func(*args, **kwargs)
print("Después del decorador inferior")
return resultado
return envoltura
@decorador_superior
@decorador_inferior
def funcion_ejemplo():
print("Ejecutando función original")
return "Resultado"
resultado = funcion_ejemplo()
Decorador para cache simple
Un decorador práctico es implementar un cache básico para funciones que realizan cálculos costosos:
def cache_simple(func):
cache = {}
def envoltura(*args, **kwargs):
# Creamos una clave única para los argumentos
clave = str(args) + str(sorted(kwargs.items()))
if clave in cache:
print(f"Cache hit para {func.__name__}")
return cache[clave]
print(f"Calculando {func.__name__}")
resultado = func(*args, **kwargs)
cache[clave] = resultado
return resultado
return envoltura
@cache_simple
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Primera llamada: calcula todo
print(fibonacci(10))
# Segunda llamada: usa cache
print(fibonacci(10))
Preservando metadatos de la función
Cuando usamos decoradores, perdemos información importante sobre la función original como su nombre y documentación. Python proporciona functools.wraps
para preservar estos metadatos:
from functools import wraps
def decorador_mejorado(func):
@wraps(func) # Preserva metadatos de la función original
def envoltura(*args, **kwargs):
print(f"Ejecutando {func.__name__}")
return func(*args, **kwargs)
return envoltura
@decorador_mejorado
def funcion_documentada(x, y):
"""Esta función suma dos números."""
return x + y
# Sin @wraps, esto mostraría 'envoltura'
print(funcion_documentada.__name__) # funcion_documentada
print(funcion_documentada.__doc__) # Esta función suma dos números.
Caso práctico: decorador de retry
Un decorador muy útil en aplicaciones reales es uno que reintenta una operación si falla:
from functools import wraps
import random
def reintentar(max_intentos=3):
def decorador(func):
@wraps(func)
def envoltura(*args, **kwargs):
for intento in range(max_intentos):
try:
return func(*args, **kwargs)
except Exception as e:
if intento == max_intentos - 1:
print(f"Falló después de {max_intentos} intentos")
raise e
print(f"Intento {intento + 1} falló: {e}. Reintentando...")
return envoltura
return decorador
@reintentar(max_intentos=3)
def operacion_inestable():
if random.random() < 0.7: # 70% de probabilidad de fallo
raise ConnectionError("Conexión fallida")
return "Operación exitosa"
# Esta función se reintentará automáticamente si falla
try:
resultado = operacion_inestable()
print(resultado)
except ConnectionError:
print("La operación falló definitivamente")
Los decoradores básicos nos permiten añadir funcionalidad a nuestras funciones de manera elegante y reutilizable. Son especialmente valiosos para implementar aspectos transversales como logging, timing, validación y manejo de errores, manteniendo el código principal limpio y enfocado en su responsabilidad principal.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en Python
Documentación oficial de Python
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Python es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Python
Explora más contenido relacionado con Python y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender el uso de parámetros variables *args y **kwargs para funciones flexibles.
- Aprender a crear y aplicar funciones lambda para operaciones simples y transformaciones.
- Entender cómo funcionan los decoradores para modificar el comportamiento de funciones.
- Saber combinar múltiples decoradores y preservar metadatos con functools.wraps.
- Aplicar decoradores en casos prácticos como medición de tiempo, logging, validación y reintentos.