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, nofoprocesar). - Parámetros entre paréntesis — los datos que la función recibe (pueden ser cero).
:y bloque indentado.return— devuelve un resultado. Sinreturn, la función devuelveNone.
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)—ayb. - Argumentos son los valores que pasás al llamar:
sumar(3, 4)—3y4.
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:
- Local — variables de la función actual.
- Enclosing — funciones que la rodean (cuando hay funciones dentro de funciones).
- Global — el archivo entero.
- 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 hints — subtotal: 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:
- Detectar errores de tipos antes de ejecutar.
- Dejar tu código auto-documentado.
- 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:
- Cambiás algo (ej: pasar
.strip().title()a.strip().upper()) en un solo lugar. - El programa principal se vuelve un resumen legible: "leo 3 clientes".
- Cada función puede testearse aparte.
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:
- Cada función hace una cosa y tiene un nombre descriptivo. El programa principal cuenta una historia: "leer pedido → calcular → imprimir → acumular".
calcular_totales testeable. Podés verificarla sin involucrarinputoprint:d = calcular_total("queso", 50) assert d["descuento"] == 50 * 0.50 * 0.15- Cambiar el menú = editar un solo diccionario.
- 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 . Probala con 0 (debe dar 32) y 100 (212).
Solución
def celsius_a_fahrenheit(c: float) -> float:
"""Convierte temperatura de Celsius a Fahrenheit."""
return c * 9 / 5 + 32
print(celsius_a_fahrenheit(0)) # 32.0
print(celsius_a_fahrenheit(100)) # 212.0
print(celsius_a_fahrenheit(36.6)) # 97.88
✏️ 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.
Solución
def estadisticas(notas):
return min(notas), max(notas), sum(notas) / len(notas)
mn, mx, p = estadisticas([6, 7, 9, 8, 5])
print(f"min={mn}, max={mx}, promedio={p:.2f}")
min, max y sum son funciones built-in. Reusalas — no reinvientes la rueda.
✏️ Ejercicio 7.3 — IMC y clasificación
El IMC es (peso en kg, altura en metros).
Escribí dos funciones:
imc(peso, altura)que devuelva el IMC.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.
Solución
def imc(peso: float, altura: float) -> float:
return peso / altura ** 2
def clasificar_imc(valor: float) -> str:
if valor < 18.5:
return "bajo peso"
if valor < 25:
return "normal"
if valor < 30:
return "sobrepeso"
return "obesidad"
def main():
peso = float(input("Peso (kg): "))
altura = float(input("Altura (m): "))
valor = imc(peso, altura)
print(f"IMC: {valor:.1f} - {clasificar_imc(valor)}")
main()
Notá cómo cada función tiene una sola responsabilidad: imc calcula, clasificar_imc etiqueta, main orquesta. Eso es lo que vas a buscar en todo refactor.
✏️ Ejercicio 7.4 — Función recursiva (vistazo)
El factorial de se define así:
Escribí dos versiones de factorial(n):
- Iterativa (con un bucle).
- Recursiva (la función se llama a sí misma).
Solución
Iterativa:
def factorial(n: int) -> int:
resultado = 1
for i in range(2, n + 1):
resultado *= i
return resultado
Recursiva:
def factorial(n: int) -> int:
if n == 0:
return 1
return n * factorial(n - 1)
Las dos dan el mismo resultado: factorial(5) = 120.
La recursión — una función que se llama a sí misma — es uno de los temas de Programación II. Por ahora, contemplala con respeto: que un problema se resuelva diciendo "el factorial de n es n por el factorial de n-1, y el factorial de 0 es 1" es mágica matemática que también funciona como código.
✏️ 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")
Solución
def evaluar_nota(numero: int) -> None:
nota = float(input(f"Nota {numero}: "))
estado = "aprobado" if nota >= 6 else "reprobado"
print(f"Nota {numero}: {estado}")
for i in range(1, 4):
evaluar_nota(i)
Pasamos de 12 líneas duplicadas a 6 líneas claras. Ahora si querés evaluar 100 notas, cambiás un range. Si cambia la regla de aprobar, modificás un único if.
7.12 Para profundizar
- PEP 257 — convención de docstrings: https://peps.python.org/pep-0257/
- PEP 484 — type hints: https://peps.python.org/pep-0484/
- Libro recomendado: Clean Code de Robert C. Martin — capítulo 3 ("Functions") es de los mejores textos sobre cómo escribir funciones que valgan la pena leer.
- Próximo curso: Programación II (PRO215) — recursión, listas, diccionarios, archivos, módulos, excepciones, primer contacto con orientación a objetos.
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:
- Pensar algorítmicamente — descomponer un problema en pasos sin ambigüedad.
- Instalar Python, escribir un archivo
.py, ejecutarlo desde la terminal. - Guardar datos en variables y manipular números, texto y booleanos.
- Calcular con precisión, manejando los caprichos de los
floaty la división entera. - Hacer que el programa decida con
if/elif/else. - Hacer que el programa repita con
forywhile. - 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.