Funciones en Python 🪄✨

Aprende a organizar tu código, reutilizar lógica y construir programas más complejos y elegantes definiendo tus propias funciones.

1. ¿Qué son las Funciones y por qué son tus mejores amigas?

Imagina que construyes un robot complejo 🤖; en vez de forjar cada tornillo desde cero, diseñas piezas reutilizables. Las funciones son exactamente eso en programación: componentes auto‑contenidos que realizan una tarea específica y que puedes invocar una y otra vez.

🚀 Ventajas de usar Funciones:

Reutilización

Escribe una vez y úsalo mil.

Modularidad

Divide y vencerás.

Legibilidad

Más fácil de depurar 👓.

Abstracción

Usa la función sin conocer sus componentes internos.

Analogía: la fórmula de energía cinética E_k = ½·m·v² se convierte en energia_cinetica(m,v).

2. Sintaxis Básica: def iniendo tu Primera Función

Para definir una función usa def + nombre + paréntesis y dos puntos. Todo el cuerpo va indentado cuatro espacios.

Código 2.1 — Función sin parámetros
def saludo_laboratorio():
    print("🔬 ¡Bienvenido al laboratorio virtual!")
    print("Por favor, sigue los protocolos de seguridad.")

saludo_laboratorio()
print("--- Iniciando experimento 1 ---")
saludo_laboratorio()

3. Parámetros & Argumentos

Los parámetros son los nombres declarados en la definición de la función; los argumentos son los valores reales que envías al invocarla.

Aspecto Parámetro Argumento
ContextoDefinición (def)Llamada
NaturalezaNombreValor concreto
Código 3.1 — Parámetros vs. argumentos
def calcular_area_rectangulo(base, altura):
    return base * altura

print(calcular_area_rectangulo(10, 5))
print(calcular_area_rectangulo(altura=3, base=7))

Cuidado: los parámetros viven sólo dentro de la función. Cambiarlos no modifica la variable original (salvo que sean mutables y compartidos).

4. La palabra clave return

return detiene la ejecución de la función y envía un resultado al punto de llamada. Si se omite, Python devuelve None.

Cierre inmediato

Todo lo que esté después de return no se ejecuta.

Valor enviado

Puedes devolver uno o varios valores (en forma de tupla implícita).

5. Argumentos variables *args

*args (de variable arguments) permite a una función recibir un número arbitrario de argumentos posicionales. Internamente, todo lo que sobrepase los parámetros fijos se almacena en una tuple. Gracias a ello, tu función puede ser tan flexible como un plot twist en una serie 📺.

Anatomía de *args

Elemento Significado
* Operador de unpacking; atrapa el “resto” de los argumentos.
args Convención de nombre – pero podrías usar *nums, *files, etc.
Tipo resultante tuple inmutable.
Posición en la firma Después de los parámetros posicionales “normales” y antes de **kwargs.
Código 5.1 — Sumar cualquier cantidad de números
def sumar(*numeros):
    """Devuelve la suma de todos los argumentos."""
    return sum(numeros)

print(sumar(2, 4, 6))                # → 12
print(sumar(1, 2, 3, 4, 5))    # → 15
Código 5.2 — Encabezado fijo + datos variables
def registrar_evento(etiqueta, *detalles):
    texto = f"[{etiqueta}]" + ": " + ", ".join(map(str, detalles))
    print(texto)

registrar_evento("LOGIN", 42, "usuario", "OK")
# [LOGIN]: 42, usuario, OK
Código 5.3 — Re-empacar argumentos para otra función
def promedio(*vals):
    return sum(vals) / len(vals) if vals else 0

def promedio_redondeado(*datos):
    return round(promedio(*datos), 2)

print(promedio_redondeado(7.5, 8.1, 9.3))

Buenas prácticas & trampas frecuentes

Nombres expressivos

Usa *nombres, *puntos, etc., cuando el contexto mejore la legibilidad.

Orden correcto

def f(a, b=0, *args, **kwargs). Nada puede ir después de *args salvo **kwargs.

Validación temprana

Comprueba tipos o cantidad mínima: if len(args) < 2: raise ValueError.

No abuses

Si tu función siempre espera 3 valores, declara 3 parámetros. *args es para casos realmente variables.

Dominar *args multiplica la versatilidad de tus APIs y facilita la creación de wrapper-functions y logging detallado.

6. Argumentos de palabra clave **kwargs

Si *args captura “todos los sobrantes posicionales”, **kwargs (de “keyword arguments”) hace lo propio con los argumentos nombrados. El operador ** empaqueta cada par clave=valor adicional en un dict, ideal para funciones con configuraciones opcionales, objetos heterogéneos o parámetros evolutivos.

Anatomía de **kwargs

Elemento Significado
** Operador de double-unpacking; captura pares nombre=valor.
kwargs Convención de nombre (“kw” = keyword). Puedes usar **opciones, **params, etc.
Tipo resultante dict mutable.
Posición en la firma Siempre el último parámetro de la lista.
Código 6.1 — Crear un perfil de usuario flexible
def crear_perfil(nombre, **extra):
    perfil = {"nombre": nombre}
    perfil.update(extra)
    return perfil

print(crear_perfil("Ana",
                   edad=17,
                   ciudad="Bogotá",
                   pasatiempos=["ajedrez", "música"]))
Código 6.2 — Función “todo-en-uno” con verificación
def procesar_datos(accion, *datos, **config):
    modo = config.get("modo", "debug")
    if accion == "suma":
        res = sum(datos)
    elif accion == "promedio":
        res = sum(datos)/len(datos) if datos else 0
    else:
        raise ValueError("Acción no soportada")
    return round(res, config.get("decimales", 2)) if modo=="prod" else res

print(procesar_datos("suma", 2, 4, 8, decimales=0, modo="prod"))
Código 6.3 — Redirigir configuraciones a otra función
def imprimir_tabla(datos, **op):
    ancho = op.get("ancho", 10)
    for fila in datos:
        print("|".join(str(c).ljust(ancho) for c in fila))

def mostrar_personas(*filas, **estilo):
    imprimir_tabla(filas, **estilo)   # forward

mostrar_personas(
    ("ID", "Nombre"),
    (1, "Luisa"),
    (2, "Carlos"),
    ancho=12)

Buenas prácticas & peligros comunes

Documenta lo esperado

Lista las claves aceptadas en el docstring o lanza TypeError si llega algo inesperado.

Sanitiza valores

Convierte tipos (ej. int()) o establece límites antes de usar las opciones.

Descompón si crece

Si **kwargs se vuelve gigantesco, crea sub-diccionarios (db_opts, ui_opts…) y pásalos explícitamente.

Evita opacidad

El poder de **kwargs trae consigo la tentación de esconder demasiada lógica. Mantén transparencia ⚖️.

Controlar **kwargs te brinda APIs evolutivas sin romper compatibilidad. Úsalo estratégicamente y tus funciones crecerán con las necesidades de tu proyecto.

7. Parámetros opcionales con valor por defecto

Un parámetro con valor por defecto le dice a Python: “si el llamador no envía nada, usa este plan B”. Esto simplifica firmas, evita sobrecarga de None-checks y crea API más amistosas.

📏 Orden canónico: posicionales / obligatorios → opcionales → *args → **kwargs. Alterarlo provoca SyntaxError.

Código 7.1 — Saludo con parámetro opcional
def saludar(nombre, saludo="Hola"):
    print(f"{saludo}, {nombre}!")

saludar("María")                 # Hola, María!
saludar("Luis", saludo="Buenos días")  # Buenos días, Luis!

El clásico error del mutable default

😱 Problema

Los objetos mutables (listas, diccionarios, conjuntos) se evalúan una sola vez, en tiempo de definición de la función. Todos los llamadores compartirán la misma instancia.

✅ Solución

Usa None como centinela y crea un nuevo objeto dentro. Patrón conocido como “None factory”.

Código 7.2 — Efecto colateral vs. patrón seguro
def agregar_item_peligroso(item, lista=[]):  # 👎 NO HAGAS ESTO
    lista.append(item)
    return lista

print(agregar_item_peligroso(1))  # [1]
print(agregar_item_peligroso(2))  # [1, 2]  ¡Sorpresa!

def agregar_item_seguro(item, lista=None):
    if lista is None:
        lista = []
    lista.append(item)
    return lista

print(agregar_item_seguro(1))  # [1]
print(agregar_item_seguro(2))  # [2]

Estrategias & buenas costumbres

Dominar los valores por defecto te permite diseñar APIs limpias y evolutivas sin sobrecargar al usuario con parámetros obligatorios. ¡Dale a tu código esa dosis de ergonomía!

8. Funciones anónimas lambda

Una expresión lambda define una función pequeña, sin nombre y de una sola línea. Se usa donde escribir def completo sería excesivo (callbacks, ordenamientos, mapeos rápidos, etc.).

Sintaxis esencial

Componente Descripción
lambda Palabra clave que inicia la expresión.
parámetros Cero o más, separados por comas (pueden usar *args/**kwargs).
: Separador entre parámetros y cuerpo.
expresión Debe ser una única expresión (no return).
Código 8.1 — Ordenar una lista por la longitud de sus elementos
palabras = ["árbol", "sol", "enciclopedia", "luz"]
palabras.sort(key=lambda w: len(w))
print(palabras)         # ['sol', 'luz', 'árbol', 'enciclopedia']
Código 8.2 — Función de primer orden para map
numeros = [1, 2, 3, 4]
cuadrados = list(map(lambda x: x**2, numeros))
print(cuadrados)        # [1, 4, 9, 16]

Pros / Contras de lambda

⚡ Brevedad

Ideal para funciones de paso rápido — evita un def innecesario.

🔍 Legibilidad

Úsalas con moderación; expresiones largas o lógicas complejas merecen un def.

🚫 Sin docstring

No puedes documentarlas directamente; para APIs públicas prefiere funciones nombradas.

🐞 Difícil de depurar

Las lambdas anidadas crean tracebacks menos claros; nómbralas si algo puede fallar.

Dominar lambda te permitirá escribir código conciso en operaciones de alto orden (map, filter, GUI callbacks, ordenamientos complejos). Úsalas como condimento, no como plato principal.

9. Lambdas al servicio de la ciencia & error-handling elegante

Las expresiones lambda brillan cuando encapsulan fórmulas cortas y repetitivas. Combinadas con bloques try / except logras prototipos robustos en una sola pasada 🏎️.

Código 9.1 — Área y circunferencia de un círculo
import math
area_circ = lambda r: math.pi * r**2          # A = π·r²
perim_circ = lambda r: 2 * math.pi * r        # P = 2πr

radio = 3
print(f"Área: {area_circ(radio):.2f}")
print(f"Perímetro: {perim_circ(radio):.2f}")
Código 9.2 — Energía potencial gravitatoria (m·g·h)
E_p = lambda m, h, g=9.81: m * g * h
datos = [(1.5, 2), (0.8, 5), (10, 0.3)]   # (kg, m)
energias = list(map(lambda p: round(E_p(*p), 2), datos))
print(energias)  # [29.43, 39.24, 29.43]
Código 9.3 — Pendiente de recta con protección ÷0
m = lambda x1, y1, x2, y2: (y2 - y1) / (x2 - x1)

def pendiente_segura(p1, p2):
    try:
        return m(*p1, *p2)
    except ZeroDivisionError:
        return math.inf   # Recta vertical

print(pendiente_segura((0, 0), (2, 4)))   # 2.0
print(pendiente_segura((3, 1), (3, 10)))  # inf
Código 9.4 — Ley de Ohm con captura de división por cero
R = lambda V, I: V / I

def resistencia_segura(volts, amps):
    try:
        return round(R(volts, amps), 2)
    except ZeroDivisionError:
        return "∞ (aislamiento)"

print(resistencia_segura(12, 0.5), "Ω")   # 24.0 Ω
print(resistencia_segura(5, 0), "Ω")      # ∞ (aislamiento) Ω

Micro-tips para lambdas científicas

Mezclar lambdas minimalistas con un sólido try / except te permite pasar de la idea al experimento en segundos, sin sacrificar estabilidad. ¡Aplícalo y acelera tu próximo proyecto científico!