En esta página

Funciones y scope

14 min lectura TextoCap. 2 — Funciones y datos

Definiendo funciones con def

Las funciones son los bloques de construcción fundamentales de cualquier programa Python. Se definen con la palabra clave def, seguida del nombre, los parámetros entre paréntesis y dos puntos. El cuerpo de la función se indenta:

def saludar(nombre: str) -> str:
    """Devuelve un saludo personalizado."""
    return f"¡Hola, {nombre}!"

# Llamar a la función
mensaje = saludar("María")
print(mensaje)  # ¡Hola, María!

# Las funciones son objetos de primera clase en Python
print(type(saludar))  # <class 'function'>
print(saludar)        # <function saludar at 0x...>

Docstrings: documentando tus funciones

Un docstring es una cadena de texto que documenta una función, clase o módulo. Va inmediatamente después de la definición y se accede con .__doc__ o la función help():

def calcular_imc(peso_kg: float, altura_m: float) -> float:
    """
    Calcula el Índice de Masa Corporal (IMC).

    El IMC se calcula dividiendo el peso en kilogramos
    entre el cuadrado de la altura en metros.

    Args:
        peso_kg: Peso de la persona en kilogramos.
        altura_m: Altura de la persona en metros.

    Returns:
        El valor del IMC redondeado a 2 decimales.

    Raises:
        ValueError: Si el peso o la altura son negativos o cero.

    Example:
        >>> calcular_imc(70, 1.75)
        22.86
    """
    if peso_kg <= 0 or altura_m <= 0:
        raise ValueError("El peso y la altura deben ser positivos")
    return round(peso_kg / (altura_m ** 2), 2)

# Acceder a la documentación
help(calcular_imc)
print(calcular_imc.__doc__)

Parámetros por defecto

Los parámetros pueden tener valores por defecto. Los parámetros con valores por defecto deben ir después de los parámetros sin valor por defecto:

def crear_usuario(
    nombre: str,
    edad: int,
    rol: str = "usuario",
    activo: bool = True
) -> dict:
    """Crea un diccionario con los datos del usuario."""
    return {
        "nombre": nombre,
        "edad": edad,
        "rol": rol,
        "activo": activo
    }

# Llamadas válidas
u1 = crear_usuario("Ana", 25)
u2 = crear_usuario("Bruno", 32, "admin")
u3 = crear_usuario("Carmen", 28, "moderador", False)
u4 = crear_usuario("Diego", 35, activo=False)  # Argumento keyword

print(u1)  # {'nombre': 'Ana', 'edad': 25, 'rol': 'usuario', 'activo': True}
print(u4)  # {'nombre': 'Diego', 'edad': 35, 'rol': 'usuario', 'activo': False}

La trampa del valor por defecto mutable

Este es uno de los errores más comunes para programadores nuevos en Python:

# ¡MAL! El defecto mutable se comparte entre llamadas
def agregar_elemento_mal(elemento: str, lista: list = []) -> list:
    lista.append(elemento)
    return lista

print(agregar_elemento_mal("a"))  # ['a']
print(agregar_elemento_mal("b"))  # ['a', 'b'] — ¡debería ser ['b']!
print(agregar_elemento_mal("c"))  # ['a', 'b', 'c'] — ¡definitivamente mal!

# BIEN — usa None como centinela
def agregar_elemento_bien(elemento: str, lista: list | None = None) -> list:
    if lista is None:
        lista = []
    lista.append(elemento)
    return lista

print(agregar_elemento_bien("a"))  # ['a']
print(agregar_elemento_bien("b"))  # ['b'] — correcto

*args — Argumentos posicionales variables

El parámetro *args recibe cualquier cantidad de argumentos posicionales como una tupla:

def suma(*numeros: float) -> float:
    """Suma cualquier cantidad de números."""
    total = 0.0
    for n in numeros:
        total += n
    return total

print(suma(1, 2, 3))           # 6.0
print(suma(10, 20, 30, 40))    # 100.0
print(suma())                  # 0.0

# También con la función integrada sum()
def suma_v2(*numeros: float) -> float:
    return sum(numeros)

# Desempaquetar una lista como argumentos posicionales
numeros = [1, 2, 3, 4, 5]
print(suma(*numeros))  # 15.0 — el * desempaqueta la lista

**kwargs — Argumentos keyword variables

El parámetro **kwargs recibe cualquier cantidad de argumentos keyword como un diccionario:

def mostrar_info(**datos: str) -> None:
    """Muestra pares clave-valor de información."""
    for clave, valor in datos.items():
        print(f"  {clave.capitalize()}: {valor}")

mostrar_info(nombre="Elena", ciudad="Madrid", profesión="Desarrolladora")
# Nombre: Elena
# Ciudad: Madrid
# Profesión: Desarrolladora

# Combinar *args y **kwargs
def todo_junto(*args: str, **kwargs: str) -> None:
    print(f"Posicionales: {args}")
    print(f"Keyword: {kwargs}")

todo_junto("a", "b", "c", x="1", y="2")
# Posicionales: ('a', 'b', 'c')
# Keyword: {'x': '1', 'y': '2'}

# El orden es: posicionales, *args, keyword, **kwargs
def configurar(host: str, puerto: int = 8080, *args: str,
               debug: bool = False, **opciones: str) -> None:
    print(f"host={host}, puerto={puerto}, debug={debug}")
    print(f"extra args: {args}")
    print(f"opciones: {opciones}")

Funciones lambda

Las funciones lambda son funciones anónimas de una sola expresión. Son útiles como argumentos de otras funciones:

# Función normal equivalente
def cuadrado(x: float) -> float:
    return x ** 2

# Lambda equivalente
cuadrado_lambda = lambda x: x ** 2

print(cuadrado(5))       # 25
print(cuadrado_lambda(5)) # 25

# Usos comunes: como argumento de sorted(), map(), filter()
personas = [
    {"nombre": "Carlos", "edad": 30},
    {"nombre": "Ana", "edad": 25},
    {"nombre": "Bruno", "edad": 35},
]

# Ordenar por edad
por_edad = sorted(personas, key=lambda p: p["edad"])
print(por_edad[0]["nombre"])  # Ana

# Ordenar por nombre (descendente)
por_nombre_desc = sorted(personas, key=lambda p: p["nombre"], reverse=True)
print(por_nombre_desc[0]["nombre"])  # Carlos

# map() — aplica función a cada elemento
numeros = [1, 2, 3, 4, 5]
cuadrados = list(map(lambda x: x ** 2, numeros))
print(cuadrados)  # [1, 4, 9, 16, 25]

# filter() — filtra elementos según condición
pares = list(filter(lambda x: x % 2 == 0, numeros))
print(pares)  # [2, 4]

Scope y la regla LEGB

Python determina en qué ámbito buscar un nombre de variable siguiendo la regla LEGB:

  1. Local: dentro de la función actual
  2. Enclosing: en funciones contenedoras (para closures)
  3. Global: en el módulo (nivel de script)
  4. Built-in: nombres integrados como print, len, range
x = "global"  # Ámbito global

def exterior():
    x = "enclosing"  # Ámbito enclosing

    def interior():
        x = "local"  # Ámbito local
        print(x)     # Busca en L primero → "local"

    interior()
    print(x)  # Ámbito enclosing → "enclosing"

exterior()
print(x)  # Ámbito global → "global"

Las palabras clave global y nonlocal

contador = 0  # Variable global

def incrementar() -> None:
    global contador  # Declara que usamos la variable global
    contador += 1

incrementar()
incrementar()
print(contador)  # 2


def hacer_contador(inicio: int = 0):
    """Devuelve funciones para manipular un contador local."""
    cuenta = inicio  # Variable en el ámbito enclosing

    def incrementar_local(paso: int = 1) -> None:
        nonlocal cuenta  # Accede a la variable del ámbito enclosing
        cuenta += paso

    def obtener() -> int:
        return cuenta

    return incrementar_local, obtener

inc, get = hacer_contador(10)
inc()
inc(5)
print(get())  # 16

Closures

Una closure (clausura) es una función que "recuerda" el entorno en el que fue creada, incluso después de que la función exterior haya terminado de ejecutarse:

def multiplicador(factor: float):
    """Fábrica de funciones multiplicadoras."""
    def multiplicar(numero: float) -> float:
        return numero * factor  # 'factor' viene del ámbito enclosing
    return multiplicar

doble = multiplicador(2)
triple = multiplicador(3)
decima_parte = multiplicador(0.1)

print(doble(5))          # 10.0
print(triple(5))         # 15.0
print(decima_parte(50))  # 5.0

# Puedes ver las variables capturadas por una closure
print(doble.__closure__)  # (<cell at 0x...>)
print(doble.__closure__[0].cell_contents)  # 2


def crear_validador(minimo: float, maximo: float):
    """Crea una función validadora de rango."""
    def validar(valor: float) -> bool:
        return minimo <= valor <= maximo
    return validar

es_porcentaje = crear_validador(0, 100)
es_temperatura = crear_validador(-273.15, 1_000_000)

print(es_porcentaje(75))     # True
print(es_porcentaje(150))    # False
print(es_temperatura(-300))  # False
print(es_temperatura(100))   # True

Funciones como objetos de primera clase

En Python, las funciones son objetos de primera clase. Pueden almacenarse en variables, pasarse como argumentos y devolverse como valores de retorno:

from typing import Callable

def aplicar(func: Callable[[int], int], valor: int) -> int:
    """Aplica una función a un valor."""
    return func(valor)

def cuadrado(x: int) -> int:
    return x ** 2

def cubo(x: int) -> int:
    return x ** 3

print(aplicar(cuadrado, 5))  # 25
print(aplicar(cubo, 3))      # 27

# Lista de funciones
operaciones: list[Callable[[int], int]] = [cuadrado, cubo, lambda x: x + 1]
numero = 4
for op in operaciones:
    print(op(numero))  # 16, 64, 5

Resumen

Las funciones en Python son extremadamente flexibles: *args y **kwargs permiten una firma variádica, las lambdas son ideales para expresiones cortas como argumentos, y las closures permiten crear funciones que mantienen estado. La regla LEGB explica cómo Python resuelve los nombres de variables. En la próxima lección exploraremos las listas y tuplas, las estructuras de datos secuenciales más utilizadas.

La regla LEGB de scope
Python busca variables en este orden: Local → Enclosing (función contenedora) → Global → Built-in. Si no encuentra el nombre en ningún ámbito, lanza un NameError.
Nunca uses listas o dicts como valores por defecto de parámetros
Los valores por defecto mutables se crean UNA sola vez y se comparten entre todas las llamadas. Usa None como valor por defecto y crea el objeto mutable dentro de la función.
Las lambdas son para expresiones simples
Usa lambda solo para funciones de una sola expresión, principalmente como argumento de funciones como sorted(), map() o filter(). Para cualquier lógica más compleja, define una función normal con def.