Curso de Python · Bloque 04 de 09

Funciones

Parámetros, alcance, cierres, decoradores, generadores y recursión.

75–105 min Descargar notebook ↓

Audiencia. Personas que ya han visto algo de programación y quieren repasar Python de forma práctica.
Prerrequisitos. Tipos, estructuras de datos y control del flujo.
Duración orientativa. 60–90 minutos, incluida la práctica.

Objetivos de aprendizaje

  • Definir funciones pequeñas con entradas y salidas claras.
  • Manejar parámetros posicionales, nombrados, variables y predeterminados.
  • Comprender alcance, anotaciones, funciones de orden superior y cierres.
  • Reconocer decoradores, generadores y recursión.

Itinerario

  1. Definición y retorno
  2. Parámetros
  3. Alcance
  4. Funciones como valores
  5. Decoradores
  6. Generadores
  7. Recursión
  8. Práctica

Cómo trabajar: ejecuta las celdas en orden, predice el resultado antes de verlo y modifica los ejemplos. Cada bloque termina con un ejercicio, un punto de partida y una solución posible.

1. Definir, llamar y devolver

Una función agrupa una responsabilidad. return entrega un resultado y termina la ejecución; sin return explícito, devuelve None. Una función puede devolver varios valores empaquetados en una tupla.

Python
def area_rectangulo(ancho, alto):
    return ancho * alto

area = area_rectangulo(4, 2.5)
print(area)

def cociente_y_resto(dividendo, divisor):
    return dividendo // divisor, dividendo % divisor

cociente, resto = cociente_y_resto(17, 5)
print(cociente, resto)
Resultado
10.0
3 2

2. Parámetros y argumentos

Los parámetros aparecen en la definición; los argumentos son los valores usados al llamar. Los argumentos con nombre mejoran la lectura. Los valores predeterminados deben ir tras los obligatorios.

Python
def presentar(nombre, saludo="Hola", puntuacion=None):
    texto = f"{saludo}, {nombre}"
    if puntuacion is not None:
        texto += f". Puntuación: {puntuacion}"
    return texto

print(presentar("Irene"))
print(presentar(nombre="Irene", saludo="Bienvenida", puntuacion=9))
Resultado
Hola, Irene
Bienvenida, Irene. Puntuación: 9

Trampa importante: un valor predeterminado mutable se crea una sola vez. Usa None y crea la colección dentro.

Python
def agregar_evento(evento, historial=None):
    if historial is None:
        historial = []
    historial.append(evento)
    return historial

print(agregar_evento("inicio"))
print(agregar_evento("fin"))  # lista independiente
Resultado
['inicio']
['fin']

/ marca parámetros solo posicionales y * marca los que deben escribirse por nombre. *args reúne argumentos posicionales extra en una tupla; **kwargs, argumentos nombrados extra en un diccionario.

Python
def convertir(valor, /, *, factor=1.0, desplazamiento=0.0):
    return valor * factor + desplazamiento

def resumen(*valores, etiqueta="datos", **metadatos):
    return {
        "etiqueta": etiqueta,
        "cantidad": len(valores),
        "suma": sum(valores),
        "metadatos": metadatos,
    }

print(convertir(20, factor=1.8, desplazamiento=32))
print(resumen(3, 5, 8, etiqueta="serie", unidad="kg"))
Resultado
68.0
{'etiqueta': 'serie', 'cantidad': 3, 'suma': 16, 'metadatos': {'unidad': 'kg'}}

3. Documentación, tipos y alcance

Una docstring explica el contrato. Las anotaciones de tipo ayudan a lectores y herramientas, pero Python no las valida automáticamente. El alcance sigue la regla LEGB: local, función envolvente, global y built-ins.

Python
TASA_IVA = 0.21

def precio_final(precio: float, descuento: float = 0.0) -> float:
    '''Calcula el precio tras descuento e IVA; los porcentajes van de 0 a 1.'''
    base = precio * (1 - descuento)
    return base * (1 + TASA_IVA)

print(f"{precio_final(100, 0.10):.2f} €")
print(precio_final.__annotations__)
Resultado
108.90 €
{'precio': <class 'float'>, 'descuento': <class 'float'>, 'return': <class 'float'>}

Evita modificar globales desde funciones. Un cierre puede conservar estado de la función envolvente; nonlocal permite actualizarlo de manera explícita.

Python
def crear_contador(inicio=0):
    valor = inicio

    def incrementar(paso=1):
        nonlocal valor
        valor += paso
        return valor

    return incrementar

contador = crear_contador(10)
print(contador(), contador(5), contador())
Resultado
11 16 17

4. Funciones como valores

Las funciones se pueden guardar, pasar y devolver. Esto permite separar una transformación de los datos sobre los que se aplica. Las lambda son funciones anónimas breves; para lógica compleja, usa def.

Python
def aplicar(operacion, valores):
    return [operacion(valor) for valor in valores]

def doble(numero):
    return numero * 2

print(aplicar(doble, [1, 2, 3]))
print(sorted([("Ana", 8), ("Leo", 10), ("Eva", 7)], key=lambda item: item[1], reverse=True))
Resultado
[2, 4, 6]
[('Leo', 10), ('Ana', 8), ('Eva', 7)]

5. Decoradores

Un decorador recibe una función y devuelve otra. Se usa para añadir comportamiento transversal —registro, autorización, caché— sin cambiar el cuerpo original. functools.wraps conserva nombre y documentación.

Python
from functools import wraps

def registrar_llamada(funcion):
    @wraps(funcion)
    def envoltorio(*args, **kwargs):
        print(f"Llamando a {funcion.__name__}")
        resultado = funcion(*args, **kwargs)
        print(f"Resultado: {resultado}")
        return resultado
    return envoltorio

@registrar_llamada
def sumar(a, b):
    return a + b

sumar(4, 7)
Resultado
Llamando a sumar
Resultado: 11
11

6. Generadores

Una función con yield produce valores de uno en uno y conserva su estado. Es útil para secuencias grandes o potencialmente infinitas porque no necesita almacenarlas completas.

Python
def cuenta_atras(desde):
    actual = desde
    while actual > 0:
        yield actual
        actual -= 1

generador = cuenta_atras(4)
print(next(generador))
print(list(generador))
Resultado
4
[3, 2, 1]

7. Recursión

Una función recursiva se llama a sí misma y necesita un caso base. En Python, una versión iterativa suele ser más eficiente y evita el límite de recursión, pero la recursión expresa bien árboles y problemas divisibles.

Python
def factorial(n):
    if n < 0:
        raise ValueError("n debe ser no negativo")
    if n in (0, 1):
        return 1
    return n * factorial(n - 1)

print(factorial(6))
Resultado
720

Errores habituales

  • Hacer que una función imprima cuando debería devolver un valor reutilizable.
  • Usar listas o diccionarios como valores predeterminados.
  • Crear funciones enormes con múltiples responsabilidades.
  • Confiar en anotaciones como si validaran datos en ejecución.

Extensión opcional: estudia functools.partial, functools.lru_cache y protocolos de tipado.

Práctica · Canalización de datos

Crea procesar(numeros, transformacion, filtro=None) que:

  1. conserve todos los números si no se proporciona filtro;
  2. filtre cuando reciba una función predicado;
  3. aplique la transformación a los valores restantes;
  4. devuelva una lista, sin imprimir.
Tu turno
def procesar(numeros, transformacion, filtro=None):
    # TODO: filtra opcionalmente y transforma.
    pass
Ver una solución posible
Solución
def procesar(numeros, transformacion, filtro=None):
    seleccionados = numeros if filtro is None else (n for n in numeros if filtro(n))
    return [transformacion(n) for n in seleccionados]

resultado = procesar(range(8), lambda n: n ** 2, lambda n: n % 2 == 0)
print(resultado)
assert resultado == [0, 4, 16, 36]
Resultado
[0, 4, 16, 36]

Comprobación rápida

  • [ ] Mis funciones reciben datos y devuelven resultados claros.
  • [ ] Sé evitar parámetros mutables compartidos.
  • [ ] Puedo explicar para qué sirven cierres, decoradores y generadores.

← Anterior · Volver al índice · Siguiente →