Funciones

"Programs must be written for people to read, and only incidentally for machines to execute." — Harold Abelson.

Qué vas a aprender en este capítulo

Las funciones son el tema del capítulo final del primer curso de programación, y por buenas razones: son el primer mecanismo que tenés para darle un nombre a un pedazo de lógica y reutilizarlo. Un programa sin funciones es como una receta donde cada paso lo escribís entero cada vez que repetís algo. Vas a aprender a definir tus propias funciones, a pasarles datos, a recibir resultados, y a entender ese demonio sutil que se llama scope (alcance de variables).

7.1 La idea: una caja con una receta

💡 Intuición

Imaginate que estás haciendo pupusas. La masa, el relleno, la cocción — son pasos repetidos para cada pupusa. Si tuvieras que escribir todo el procedimiento cada vez ("toma una porción de masa, aplástala, ponle queso, ciérrala, ponla al comal..."), tu hoja sería un caos.

En vez, le ponés un nombre al proceso entero: "hacer una pupusa". Una vez que existe el procedimiento, podés decir "hacé una pupusa de queso", "hacé una pupusa de chicharrón" — pasando como parámetro el relleno.

Una función en programación es exactamente eso: un nombre que le ponés a un procedimiento, con parámetros que ajustan su comportamiento, y que (opcionalmente) devuelve un resultado.

Sin funciones, repetís código. Con funciones, decís "hacé X" y la computadora ejecuta el bloque entero. Una sola línea de código activa muchas.

Toda la programación profesional consiste en construir funciones útiles, ponerles nombres claros y combinarlas. Si alguien te dice "soy buen programador", lo que está diciendo en realidad es "escribo funciones que valen la pena leer".

📜 Historia

La idea de subrutina — un bloque de código con nombre, llamable desde varios lugares — apareció con los primeros lenguajes de programación en los años 50. FORTRAN las llamó subroutines (no devolvían valor) y functions (sí devolvían).

John Backus, líder del equipo que creó FORTRAN, dijo más tarde que "la principal motivación de las subrutinas no fue ahorrar memoria, sino mantener la cabeza del programador en orden". Esa frase sigue siendo verdad.

Más adelante, Alonzo Church (1936) había formalizado matemáticamente el concepto de función pura con su cálculo lambda — la base teórica de los lenguajes funcionales como Lisp y Haskell. Python toma cosas de los dos mundos: tiene funciones imperativas (las que vas a aprender hoy) y soporta también funciones lambda y de orden superior. Eso lo vamos a ver en cursos siguientes.

7.2 def y return

📐 Fundamento

def nombre_funcion(parametros):
    """Docstring opcional explicando qué hace."""
    cuerpo
    return valor       # opcional

Las partes:

  • def — palabra clave para "define".
  • Nombre de la función — en snake_case, descriptivo (calcular_total, no f o procesar).
  • Parámetros entre paréntesis — los datos que la función recibe (pueden ser cero).
  • : y bloque indentado.
  • return — devuelve un resultado. Sin return, la función devuelve None.

Ejemplo: una función que suma dos números.

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

# Usar (llamar) la función
r = sumar(3, 4)
print(r)            # 7
print(sumar(10, 20)) # 30

Ejemplo más útil — calcular el total con IVA:

IVA = 0.13

def total_con_iva(subtotal):
    return subtotal * (1 + IVA)

print(total_con_iva(100))    # 113.0
print(total_con_iva(50.50))  # 57.065

Distinción importante: parámetros vs argumentos.

  • Parámetros son los nombres en la definición: def sumar(a, b)a y b.
  • Argumentos son los valores que pasás al llamar: sumar(3, 4)3 y 4.

En la práctica los términos se confunden y todo el mundo dice "argumentos" para ambas cosas. Saber la diferencia formal te ayuda en los exámenes.

7.3 return en detalle

def es_par(n):
    if n % 2 == 0:
        return True
    else:
        return False

print(es_par(4))     # True
print(es_par(7))     # False

return termina la función. Apenas se ejecuta, el bloque de la función no sigue. Eso permite el patrón early return:

def es_par(n):
    if n % 2 == 0:
        return True
    return False        # solo se llega acá si la condición fue False

O incluso más corto:

def es_par(n):
    return n % 2 == 0   # la comparación ya devuelve True o False

Las tres versiones son equivalentes. La última es la más "pythónica" porque dice exactamente lo que hace, sin redundancia.

Función sin return. Si tu función solo hace efectos (imprimir, modificar algo), no necesitás return:

def saludar(nombre):
    print(f"Hola, {nombre}")

saludar("Maria")     # Hola, Maria
r = saludar("Carlos")
print(r)             # None

Toda función sin return explícito devuelve None. Eso a veces sorprende:

total = print("Hola")    # imprime "Hola", pero total queda en None

print no devuelve nada útil. Si lo asignás a una variable, te quedás con None.

Devolver múltiples valores. Python permite devolver varios valores como una tupla, que después podés desempacar:

def estadisticas(notas):
    minimo = min(notas)
    maximo = max(notas)
    promedio = sum(notas) / len(notas)
    return minimo, maximo, promedio

mn, mx, prom = estadisticas([7, 8, 6, 9, 10])
print(mn, mx, prom)    # 6 10 8.0

Internamente Python devuelve una tupla (minimo, maximo, promedio). La asignación múltiple la desempaca.

7.4 Parámetros con valor por defecto

def saludar(nombre, idioma="es"):
    if idioma == "es":
        print(f"Hola, {nombre}")
    elif idioma == "en":
        print(f"Hello, {nombre}")
    else:
        print(f"??? {nombre}")

saludar("Maria")              # usa idioma="es" por defecto
saludar("Maria", "en")        # idioma="en"
saludar("Maria", idioma="en") # idéntico al anterior, pero más claro

Regla: los parámetros con valor por defecto van después de los obligatorios:

# OK
def f(a, b, c=10): ...

# ERROR
def f(a=10, b, c): ...
# SyntaxError: non-default argument follows default argument

⚠️ Trampa común

Valores mutables por defecto. El error sutil más famoso de Python:

def agregar_pupusa(carrito=[]):       # ¡NO HAGAS ESTO!
    carrito.append("pupusa")
    return carrito

print(agregar_pupusa())   # ["pupusa"]
print(agregar_pupusa())   # ["pupusa", "pupusa"]   ¡acumulado!

Las listas, diccionarios y otros mutables son objetos que se crean una sola vez, al definir la función. Cada llamada modifica el mismo objeto. Solución idiomática:

def agregar_pupusa(carrito=None):
    if carrito is None:
        carrito = []
    carrito.append("pupusa")
    return carrito

Esto te va a salvar de un bug horrible al menos una vez en tu carrera. Aprendelo desde ya.

7.5 Argumentos por nombre (keyword arguments)

Cuando una función tiene varios parámetros, podés pasarlos por nombre en lugar de por posición:

def crear_cliente(nombre, edad, ciudad="San Miguel", correo=""):
    print(f"{nombre}, {edad}, {ciudad}, {correo}")

# Por posición
crear_cliente("Maria", 19, "San Salvador", "m@x.com")

# Por nombre — más legible
crear_cliente(nombre="Maria", edad=19, ciudad="San Salvador", correo="m@x.com")

# Mezcla: posicionales primero, después por nombre
crear_cliente("Maria", 19, correo="m@x.com")

Cuándo usar nombres. Cuando hay varios parámetros y la posición no es obvia, los nombres convierten una llamada críptica en una llamada autoexplicada:

# ¿Qué significa cada True/False?
configurar_motor(True, False, True, 1, 200)

# Mucho más claro
configurar_motor(turbo=True, eco=False, abs=True, modo=1, rpm_max=200)

Regla práctica: si la función tiene 4+ parámetros, llamala con nombres.

7.6 Variables locales vs globales (scope)

📐 Fundamento

Cuando declarás una variable dentro de una función, esa variable existe solo durante la ejecución de esa función. Eso se llama alcance local.

def calcular(x):
    resultado = x * 2 + 5
    return resultado

print(calcular(3))         # 11
print(resultado)           # ¡ERROR! resultado no existe afuera

resultado vive dentro de calcular. Cuando la función termina, esa variable desaparece. Esa propiedad — el aislamiento — es buena: te garantiza que las funciones no se pisan unas a otras.

Variables globales. Son las que están definidas fuera de cualquier función. Las funciones pueden leerlas, pero modificarlas requiere una palabra mágica (global):

IVA = 0.13     # global

def total(subtotal):
    return subtotal * (1 + IVA)   # leer IVA: ok

print(total(100))   # 113.0
contador = 0

def incrementar():
    global contador      # sin esto, contador se crea como local
    contador += 1

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

Regla pragmática: evitá global. Las variables globales hacen tu código difícil de razonar — un cambio en una función puede romper otra parte sin avisar. La forma "limpia" es pasar y devolver valores:

def incrementar(c):
    return c + 1

contador = 0
contador = incrementar(contador)
contador = incrementar(contador)
print(contador)   # 2

LEGB: cómo Python busca un nombre. Cuando ves print(x), Python busca x en este orden:

  1. Local — variables de la función actual.
  2. Enclosing — funciones que la rodean (cuando hay funciones dentro de funciones).
  3. Global — el archivo entero.
  4. Built-in — nombres del propio Python (print, len, range, etc.).

Si no la encuentra en ninguno, da NameError.

Constantes. Convención: si tenés un valor fijo a nivel global, ponele nombre en MAYÚSCULAS:

PI = 3.14159265
IVA = 0.13
ANIO_ACTUAL = 2026

Python no las hace inmutables (no hay const real), pero la convención avisa al lector.

7.7 Docstrings y type hints

def total_con_iva(subtotal: float, iva: float = 0.13) -> float:
    """Calcula el total a pagar incluyendo IVA.

    Args:
        subtotal: monto antes de impuestos, en dólares.
        iva: tasa de IVA como fracción (0.13 = 13%). Default 0.13.

    Returns:
        El total con IVA aplicado.
    """
    return subtotal * (1 + iva)

"""docstring""" — la cadena entre triples comillas justo después del def es la documentación. Aparece cuando alguien hace help(total_con_iva) o cuando IDEs como VS Code te muestran el tooltip al pasar el mouse. Escribilas para todas las funciones públicas.

Type hintssubtotal: float y -> float son anotaciones. Le dicen al lector (y a herramientas como mypy, pyright, pylance) qué tipos espera y devuelve la función. Python los ignora en runtime — son solo informativos. Pero te ayudan muchísimo a:

  1. Detectar errores de tipos antes de ejecutar.
  2. Dejar tu código auto-documentado.
  3. Que el editor te autocomplete bien.

Por ahora, usalos en funciones pero no te obsesiones. Vamos a profundizar en Programación II.

7.8 El principio DRY

DRY = "Don't Repeat Yourself". Si copiás y pegás más de dos veces el mismo bloque de código, algo está mal — y la solución casi siempre es extraerlo a una función.

Antes (repetitivo):

nombre1 = input("Nombre cliente 1: ").strip().title()
edad1 = int(input("Edad cliente 1: "))

nombre2 = input("Nombre cliente 2: ").strip().title()
edad2 = int(input("Edad cliente 2: "))

nombre3 = input("Nombre cliente 3: ").strip().title()
edad3 = int(input("Edad cliente 3: "))

Después (DRY):

def leer_cliente(numero):
    nombre = input(f"Nombre cliente {numero}: ").strip().title()
    edad = int(input(f"Edad cliente {numero}: "))
    return nombre, edad

n1, e1 = leer_cliente(1)
n2, e2 = leer_cliente(2)
n3, e3 = leer_cliente(3)

O mejor todavía, combinado con un bucle:

clientes = []
for i in range(1, 4):
    clientes.append(leer_cliente(i))

Beneficios concretos:

Cuando NO aplicar DRY a la fuerza. Dos pedazos parecidos pero conceptualmente distintos no deben unirse. Si la "duplicación" es coincidencia, juntarla los acopla y empeora el diseño. Esto se conoce como WET (Write Everything Twice) y a veces es lo correcto.

7.9 Proyecto: pupusería refactorizada

🏗️ Avance del proyecto — Pupusería La Esquina

El programa de la pupusería ha crecido. Es hora de refactorizarlo con funciones. Esto deja el código listo para crecer en Programación II.

# Pupuseria La Esquina - Capitulo 7: refactor con funciones

PRECIOS = {
    "queso": 0.50,
    "revuelta": 0.60,
    "chicharron": 0.60,
    "frijol con queso": 0.55,
}
IVA = 0.13


def calcular_descuento(cantidad: int) -> float:
    """Devuelve el porcentaje de descuento según la cantidad."""
    if cantidad >= 50:
        return 0.15
    if cantidad >= 20:
        return 0.10
    if cantidad >= 10:
        return 0.05
    return 0.0


def calcular_total(sabor: str, cantidad: int) -> dict:
    """Calcula el desglose del cobro para un pedido.

    Devuelve un dict con keys:
      - subtotal, descuento, base, iva, total
    """
    if sabor not in PRECIOS:
        raise ValueError(f"Sabor desconocido: {sabor}")
    if cantidad <= 0:
        raise ValueError("La cantidad debe ser positiva")

    subtotal = cantidad * PRECIOS[sabor]
    porcentaje = calcular_descuento(cantidad)
    descuento = subtotal * porcentaje
    base = subtotal - descuento
    iva = base * IVA
    total = base + iva
    return {
        "subtotal": subtotal,
        "descuento": descuento,
        "base": base,
        "iva": iva,
        "total": total,
    }


def imprimir_recibo(sabor: str, cantidad: int, desglose: dict) -> None:
    """Imprime el recibo formateado."""
    print()
    print("=" * 38)
    print(" Pupuseria La Esquina")
    print("=" * 38)
    print(f"  {cantidad}x pupusa de {sabor}")
    print(f"  Subtotal           ${desglose['subtotal']:>7.2f}")
    print(f"  Descuento          ${desglose['descuento']:>7.2f}")
    print(f"  IVA (13%)          ${desglose['iva']:>7.2f}")
    print("-" * 38)
    print(f"  TOTAL              ${desglose['total']:>7.2f}")
    print("=" * 38)


def leer_pedido() -> tuple[str, int] | None:
    """Lee un pedido del usuario. Devuelve None si escribió 'fin'."""
    sabor = input("\nSabor (o 'fin'): ").strip().lower()
    if sabor == "fin":
        return None
    if sabor not in PRECIOS:
        print(f"  No tenemos '{sabor}'.")
        return ()           # tupla vacía = "intentá de nuevo"
    cantidad_str = input("Cantidad: ").strip()
    if not cantidad_str.isdigit():
        print("  Cantidad inválida.")
        return ()
    cantidad = int(cantidad_str)
    return sabor, cantidad


def main():
    print("=== Bienvenido a Pupuseria La Esquina ===")
    total_dia = 0.0
    clientes = 0

    while True:
        pedido = leer_pedido()
        if pedido is None:
            break
        if pedido == ():
            continue
        sabor, cantidad = pedido
        desglose = calcular_total(sabor, cantidad)
        imprimir_recibo(sabor, cantidad, desglose)
        total_dia += desglose["total"]
        clientes += 1

    print()
    print(f"Cierre: {clientes} clientes, ${total_dia:.2f}")


if __name__ == "__main__":
    main()

Ganancias del refactor:

  1. Cada función hace una cosa y tiene un nombre descriptivo. El programa principal cuenta una historia: "leer pedido → calcular → imprimir → acumular".
  2. calcular_total es testeable. Podés verificarla sin involucrar input o print:
    d = calcular_total("queso", 50)
    assert d["descuento"] == 50 * 0.50 * 0.15
    
  3. Cambiar el menú = editar un solo diccionario.
  4. El bloque if __name__ == "__main__": es una convención: el código adentro solo corre cuando ejecutás el archivo directamente, no cuando otro programa lo importa. Por ahora aceptalo como ritual; lo profundizamos en Programación II.

Lo que viene en Programación II: módulos, archivos, manejo de excepciones (try/except), estructuras de datos compuestas (listas, diccionarios), recursión, programación orientada a objetos.

7.10 Resumen visual

Construcción Para qué
def f(x): Define una función llamada f con parámetro x.
return valor Devuelve un resultado y termina la función.
def f(x=10): Parámetro con valor por defecto.
f(x=5) Argumento por nombre.
Variables dentro de def Locales, mueren al salir de la función.
global x Permitir modificar una variable global desde adentro.
"""docstring""" Documentación al inicio de la función.
x: int -> str Type hints, anotaciones (informativas).
DRY Si lo repetís 2 veces, considerá una función.

7.11 Ejercicios

✏️ Ejercicio 7.1 — Función para convertir

Escribí una función celsius_a_fahrenheit(c) que convierta una temperatura. La fórmula es F=95C+32F = \frac{9}{5}C + 32. Probala con 0 (debe dar 32) y 100 (212).

✏️ Ejercicio 7.2 — Mínimo, máximo y promedio

Escribí una función estadisticas(notas) que reciba una lista de notas y devuelva tres valores: mínimo, máximo y promedio.

✏️ Ejercicio 7.3 — IMC y clasificación

El IMC es IMC=pesoaltura2\text{IMC} = \frac{\text{peso}}{\text{altura}^2} (peso en kg, altura en metros).

Escribí dos funciones:

  1. imc(peso, altura) que devuelva el IMC.
  2. clasificar_imc(valor) que devuelva una de: "bajo peso", "normal", "sobrepeso", "obesidad". Los rangos típicos son < 18.5, [18.5, 25), [25, 30), >= 30.

Probalas en una función main() que pida los datos al usuario.

✏️ Ejercicio 7.4 — Función recursiva (vistazo)

El factorial de nn se define así:

n!={1si n=0n(n1)!si n>0n! = \begin{cases} 1 & \text{si } n = 0 \\ n \cdot (n-1)! & \text{si } n > 0 \end{cases}

Escribí dos versiones de factorial(n):

  1. Iterativa (con un bucle).
  2. Recursiva (la función se llama a sí misma).

✏️ Ejercicio 7.5 — Refactor

Tomá este programa repetitivo y refactorizá con funciones para eliminar la duplicación.

# Programa original
nota1 = float(input("Nota 1: "))
if nota1 >= 6:
    print("Nota 1: aprobado")
else:
    print("Nota 1: reprobado")

nota2 = float(input("Nota 2: "))
if nota2 >= 6:
    print("Nota 2: aprobado")
else:
    print("Nota 2: reprobado")

nota3 = float(input("Nota 3: "))
if nota3 >= 6:
    print("Nota 3: aprobado")
else:
    print("Nota 3: reprobado")

7.12 Para profundizar


Cierre del libro

Llegaste al final de Programación I. Hagamos un resumen de lo que sabés ahora y que no sabías hace 7 capítulos:

  1. Pensar algorítmicamente — descomponer un problema en pasos sin ambigüedad.
  2. Instalar Python, escribir un archivo .py, ejecutarlo desde la terminal.
  3. Guardar datos en variables y manipular números, texto y booleanos.
  4. Calcular con precisión, manejando los caprichos de los float y la división entera.
  5. Hacer que el programa decida con if/elif/else.
  6. Hacer que el programa repita con for y while.
  7. Empaquetar lógica en funciones y aplicar el principio DRY.

Eso es la base de toda la programación. Lenguajes como Java, JavaScript, C, C++, Rust, Go — todos los vas a poder leer porque comparten estos mismos conceptos. Lo único que cambia es la sintaxis y algunos detalles. Has aprendido a pensar, no a memorizar Python.

Buena suerte en Programación II, y si te trabás en algún tema, retroceder y releer un capítulo no es derrota, es estudio.


Definiciones nuevas: función, parámetro, argumento, def, return, None, valor por defecto, argumento por nombre, scope, local, global, LEGB, docstring, type hint, DRY.