Funciones, Modularización y Abstracción

Semana 4 — Descomposición de problemas, programación modular y pila de ejecución

1. ¿Por Qué Funciones?

En las semanas anteriores resolvimos problemas escribiendo todo el código en un solo bloque: la sección algorítmica contenía prólogo, resolución y epílogo de forma secuencial. Esta estrategia funciona para programas pequeños, pero presenta tres problemas que crecen junto con la complejidad del código:

  1. Repetición: si necesitamos el mismo cálculo en dos puntos distintos del programa, debemos copiar y pegar el código. Si luego descubrimos un error, debemos corregirlo en cada copia — una fuente prolífica de errores.
  2. Legibilidad: un programa de 200 líneas sin divisiones claras es tan difícil de leer como un libro sin capítulos. El lector pierde el hilo entre los detalles.
  3. Complejidad cognitiva: la memoria de trabajo humana maneja simultáneamente entre cinco y nueve elementos independientes (Miller, 1956). Un programa monolítico obliga a rastrear mentalmente todas las variables a la vez.

Las funciones resuelven estos tres problemas de una sola vez. Una función es un bloque de código con nombre propio que realiza una tarea específica, recibe datos de entrada (parámetros) y puede devolver un resultado. En lugar de repetir instrucciones, invocamos la función cada vez que necesitamos esa tarea.

Analogía: la receta y el chef

Pensemos en una cocina profesional. El chef principal no prepara personalmente cada ingrediente de cada plato: delega tareas a cocineros especializados. Uno prepara las salsas, otro las guarniciones, otro los postres. Cada cocinero tiene una responsabilidad definida (una tarea), recibe ingredientes (parámetros) y entrega un producto terminado (valor de retorno). El chef principal «invoca» a cada especialista cuando lo necesita, sin preocuparse por cómo se elabora internamente cada preparación — solo por qué necesita y qué recibe.

Así funcionan las funciones en un programa: cada una encapsula una tarea, recibe datos, devuelve un resultado, y el programa principal las invoca sin conocer (ni necesitar conocer) sus detalles internos.

2. Anatomía de una Función en Python

2.1. Definición y sintaxis

En Python, una función se define con la palabra reservada def, seguida de un nombre descriptivo, una lista de parámetros entre paréntesis y dos puntos. El cuerpo de la función está indentado y opcionalmente finaliza con una sentencia return.

Anatomía de una función en Python def calcular_area (base, altura): Palabra reservada Nombre (verbo) Parámetros Cuerpo '''Calcula el área de un rectángulo.''' Docstring return base * altura Valor de retorno
def calcular_area(base, altura):
    '''Calcula el área de un rectángulo.

    Parámetros:
      base (float): medida del lado horizontal.
      altura (float): medida del lado vertical.

    Retorna:
      float: el área calculada como base × altura.
    '''
    area = base * altura
    return area

Terminología precisa

Es fundamental distinguir dos términos que a menudo se confunden:

La relación es: al invocar una función, cada argumento se asocia con el parámetro correspondiente, en orden posicional.

2.2. Invocación y flujo de ejecución

Definir una función no ejecuta su código; solo lo registra en memoria con un nombre. La ejecución ocurre cuando el programa invoca (o «llama») a la función por su nombre, proporcionando los argumentos necesarios.

# Definición (se ejecuta una sola vez, al cargar el módulo)
def calcular_area(base, altura):
    '''Calcula el área de un rectángulo.'''
    area = base * altura
    return area

# --- Programa principal ---
# Prólogo
print('=== Calculadora de Rectángulo ===')

# Resolución
base_str = input('Base: ')
b = float(base_str)
altura_str = input('Altura: ')
h = float(altura_str)

resultado = calcular_area(b, h)    # Invocación: b→base, h→altura

# Epílogo
print('El área es:', resultado)

El flujo de ejecución sigue una secuencia precisa: cuando Python encuentra la invocación calcular_area(b, h), suspende la ejecución del programa principal, transfiere el control a la función (asociando b con base y h con altura), ejecuta el cuerpo de la función, y al encontrar return, devuelve el control al punto exacto donde se realizó la invocación, sustituyendo la llamada por el valor retornado.

Flujo de ejecución al invocar una función Programa principal b = float(input('Base: ')) h = float(input('Altura: ')) resultado = calcular_area(b,h) ⏸ ejecución suspendida print('El área es:', resultado) calcular_area(base, altura) base ← b | altura ← h area = base * altura return area Las variables locales se destruyen ① Invocación ② return → resultado

2.3. Funciones con y sin valor de retorno

No todas las funciones necesitan devolver un resultado. Según su propósito, podemos distinguir dos categorías:

TipoDescripciónEjemplo
Con retorno
return valor
Calcula y devuelve un resultado que el invocador necesita. La función termina al ejecutar return. Se invoca típicamente como parte de una asignación o expresión. area = calcular_area(5, 3)
Sin retorno
procedimiento
Realiza una acción (imprimir, modificar un estado) sin devolver un valor explícito. En Python, devuelve None implícitamente. Se invoca como sentencia independiente. mostrar_resultado(area)
# Función CON retorno: calcula y devuelve
def calcular_perimetro(base, altura):
    '''Retorna el perímetro de un rectángulo.'''
    perimetro = 2 * (base + altura)
    return perimetro

# Función SIN retorno (procedimiento): solo imprime
def mostrar_resultado(etiqueta, valor):
    '''Muestra un resultado con formato consistente.'''
    print(etiqueta + ':', valor)

⚠️ Error frecuente: print no es return

Un error muy común en principiantes es usar print() dentro de una función creyendo que eso «devuelve» el valor. No es así. print() muestra texto en pantalla; return entrega un valor al invocador. Si una función calcula un resultado que será usado después, debe usar return. Si una función solo necesita exhibir información, puede usar print(). Confundir ambas operaciones es el origen del error «mi función imprime el resultado correcto pero resultado vale None».

2.4. Retorno múltiple: tuplas implícitas

Python permite que una función devuelva varios valores simultáneamente, separados por comas. Internamente, Python empaqueta los valores en una tupla (un tipo de dato que estudiaremos en semanas posteriores). El invocador puede desempaquetar los valores en variables separadas:

def calcular_rectangulo(base, altura):
    '''Retorna el área y el perímetro de un rectángulo.'''
    area = base * altura
    perimetro = 2 * (base + altura)
    return area, perimetro

# Desempaquetado: cada variable recibe un valor
a, p = calcular_rectangulo(5.0, 3.0)
print('Área:', a)          # → Área: 15.0
print('Perímetro:', p)     # → Perímetro: 16.0

2.5. Anticipo: paso de argumentos por valor y por referencia

Cuando invocamos una función con argumentos, ¿qué recibe exactamente la función? La respuesta depende del mecanismo de paso de argumentos del lenguaje. Existen dos modelos fundamentales:

MecanismoDescripciónConsecuencia
Por valor La función recibe una copia del valor del argumento. Cualquier modificación dentro de la función afecta solo a la copia local; el argumento original no cambia. Seguro: la función no puede producir efectos secundarios sobre las variables del invocador. Es el comportamiento que observamos con int, float, str y bool.
Por referencia La función recibe una referencia (dirección) al objeto original. Las modificaciones dentro de la función sí afectan al objeto del invocador. Potente pero peligroso: permite que la función modifique datos compartidos. En Python, esto ocurre con objetos mutables como las listas (Semana 9).
# Ejemplo con tipo inmutable (int): paso por valor efectivo
def intentar_modificar(x):
    x = x + 10      # Modifica la copia local
    print('Dentro:', x) # → Dentro: 15

valor = 5
intentar_modificar(valor)
print('Fuera:', valor)   # → Fuera: 5 (¡no cambió!)

Nota para profundizar en la Semana 7

Técnicamente, Python utiliza un mecanismo denominado paso por asignación (pass by assignment): siempre se pasa una referencia al objeto, pero el comportamiento observable depende de si el objeto es mutable (listas, diccionarios) o inmutable (int, float, str, tuplas). Con los tipos inmutables que usamos en esta etapa del curso, el efecto práctico es idéntico al paso por valor: la función no puede modificar los datos del invocador. La Semana 7 abordará este tema en profundidad con objetos mutables.

3. Alcance de Variables (Scope)

Una de las propiedades más importantes de las funciones es el aislamiento: las variables creadas dentro de una función son invisibles desde fuera, y viceversa. Este mecanismo, denominado alcance de variables (en inglés, scope), evita interferencias accidentales entre distintas partes del programa.

3.1. Variables locales y globales

TipoDónde se creaDónde es visibleCuándo se destruye
Local Dentro del cuerpo de una función (incluidos los parámetros). Solo dentro de esa función. Al terminar la ejecución de la función (return o fin del cuerpo).
Global En el nivel principal del programa (fuera de toda función). En todo el programa, pero solo en modo lectura desde las funciones. Al terminar la ejecución del programa.
def duplicar(x):
    '''Retorna el doble de x.'''
    resultado = x * 2    # 'resultado' es LOCAL a esta función
    return resultado

# --- Programa principal ---
valor = 5               # 'valor' es GLOBAL
doble = duplicar(valor)   # 'doble' es GLOBAL
print(doble)              # → 10

# print(resultado) ← ERROR: 'resultado' no existe aquí

3.2. El problema del sombreado (shadowing)

Si una función define una variable local con el mismo nombre que una variable global, la variable local oculta (o «sombrea») a la global dentro del ámbito de la función. La variable global no se modifica.

# Ejemplo de shadowing
x = 100                # Variable global

def mi_funcion():
    x = 42              # Variable LOCAL: sombrea a la global
    print('Dentro:', x)   # → Dentro: 42

mi_funcion()
print('Fuera:', x)        # → Fuera: 100 (la global no cambió)

💡 Buena práctica: evitar nombres compartidos

El sombreado no es un error de Python, sino un comportamiento intencionado que protege al programa principal. Sin embargo, usar el mismo nombre para una variable local y una global genera confusión. La recomendación es simple: elegir nombres descriptivos y distintos para variables de distinto alcance. Si la variable global se llama total_acumulado, la variable local de una función no debería llamarse igual, a menos que se trate intencionalmente de una nueva variable independiente.

3.3. Regla LEGB: la cadena de búsqueda de Python

Cuando Python encuentra un nombre de variable en una expresión, busca su valor siguiendo un orden fijo conocido como la regla LEGB:

L Local E Enclosing G Global B Built-in Variables del cuerpo de la función actual Funciones que contienen a la actual (anidadas) Variables del módulo principal (nivel superior) Nombres predefinidos: print, len, int… En este curso trabajamos con los niveles L, G y B. El nivel E se usa con funciones anidadas (Semana 8).

4. Programación Modular: Descomposición de Problemas

Las funciones son la herramienta técnica; la programación modular es la estrategia de diseño que las aprovecha. Modularizar un programa consiste en descomponer un problema complejo en subproblemas más simples, implementar cada uno como una función independiente, y luego coordinarlos desde un programa principal.

4.1. Diseño top-down (descendente)

El diseño top-down es una estrategia donde se parte del problema completo y se lo descompone progresivamente en subproblemas más pequeños, hasta llegar a piezas lo suficientemente simples como para codificarse directamente. Es el enfoque natural de la metodología de Pólya: primero se analiza el problema general, se diseña la estructura de la solución, y luego se codifica cada parte.

Consideremos un programa que lee una temperatura en grados Celsius y genera un informe con las conversiones a Fahrenheit y Kelvin. La descomposición top-down produce el siguiente árbol:

Ejemplo de descomposición top-down Nivel 0 Nivel 1 Nivel 2 Informe de conversión de temperaturas Leer temperatura Convertir unidades Mostrar informe Celsius → Fahrenheit Celsius → Kelvin Cada nodo del árbol se convierte en una función del programa

💡 ¿Por qué este ejemplo y no otro?

Las funciones celsius_a_fahrenheit y celsius_a_kelvin son genuinamente independientes: cada una recibe un dato, aplica una fórmula distinta y devuelve un resultado. No comparten estado ni requieren un orden particular entre sí. Esta independencia es lo que hace que la descomposición sea auténtica: cada nodo del Nivel 2 es una función autocontenida y reutilizable. Compare esto con un caso donde los subproblemas parecen independientes pero en realidad se resuelven mejor de forma simultánea (como calcular máximo, mínimo y promedio en un único recorrido, según vimos en Semana 3).

4.2. Criterios de calidad modular: cohesión y acoplamiento

No basta con dividir el código en funciones: la calidad de la modularización depende de cómo se realiza la división. Dos criterios complementarios guían esta decisión:

Cohesión y Acoplamiento

Los dos ejes de la calidad modular

Alta cohesión ✓

Cada función realiza una sola tarea bien definida. Todos los elementos internos (variables, operaciones) contribuyen a esa tarea. La función puede describirse con un nombre claro y una oración breve.

Indicador: si la docstring necesita la palabra «y» para describir qué hace la función, probablemente hace demasiadas cosas.

Bajo acoplamiento ✓

Las funciones se comunican exclusivamente a través de parámetros y valores de retorno, sin depender de variables globales ni de detalles internos de otras funciones.

Indicador: si al modificar el interior de una función se rompen otras funciones, el acoplamiento es demasiado alto.

Cohesión Baja: la función hace de todo Alta: una tarea bien definida Acoplamiento Bajo: solo usa parámetros y return Alto: depende de variables globales Objetivo: ALTA cohesión + BAJO acoplamiento

4.3. Ejemplo integrado: refactorización paso a paso

Tomemos como ejemplo un programa monolítico de la Semana 3 (patrón de acumulación con filtrado) y transformémoslo en un diseño modular. Este proceso de mejora del código sin cambiar su comportamiento se denomina refactorización.

❌ Versión monolítica (Semana 3)
'''
Sumar los múltiplos de 3 en una
secuencia terminada en 0.
'''

# Prólogo
print('Ingrese números (0 para'
      'terminar).')

# Resolución
suma = 0
dato_str = input('Número: ')
dato = int(dato_str)
while dato != 0:
    if dato % 3 == 0:
        suma = suma + dato
    dato_str = input('Número: ')
    dato = int(dato_str)

# Epílogo
print('Suma:', suma)
✓ Versión modular (Semana 4)
def es_multiplo(numero, divisor):
    '''Verifica si numero es
    múltiplo de divisor.'''
    return numero % divisor == 0

def leer_entero(mensaje):
    '''Lee un entero del teclado.'''
    texto = input(mensaje)
    return int(texto)

def sumar_multiplos(divisor):
    '''Suma los múltiplos de divisor
    en una secuencia (centinela 0).'''
    suma = 0
    dato = leer_entero('Número: ')
    while dato != 0:
        if es_multiplo(dato, divisor):
            suma = suma + dato
        dato = leer_entero('Número: ')
    return suma

# --- Programa principal ---
print('Ingrese números (0 para'
      'terminar).')
resultado = sumar_multiplos(3)
print('Suma:', resultado)

¿Qué ganamos con la refactorización?

5. Abstracción Funcional

La abstracción es el principio que permite usar una función sin conocer su implementación interna. Cuando invocamos len('hola'), no necesitamos saber cómo Python cuenta los caracteres internamente: solo necesitamos conocer qué hace (devuelve la longitud), qué espera recibir (una secuencia) y qué devuelve (un entero). Este acuerdo entre el invocador y la función se denomina contrato de interfaz.

5.1. El contrato de interfaz

El contrato de una función especifica tres elementos:

Precondición ¿Qué debe ser verdadero ANTES de invocar? Ejemplo: base > 0, altura > 0 ⚙️ Función (caja negra) Postcondición ¿Qué será verdadero DESPUÉS de ejecutar? Ejemplo: retorno == base × altura La docstring documenta el contrato; el cuerpo lo implementa

5.2. La docstring como contrato documentado

En Python, el contrato de una función se expresa mediante su docstring (cadena de documentación): un literal de cadena colocado como primera sentencia del cuerpo de la función. La docstring es accesible en tiempo de ejecución mediante el atributo __doc__ o la función help().

def es_primo(n):
    '''Determina si un número entero positivo es primo.

    Precondición:
      n (int): número entero, n >= 1.

    Postcondición / Retorna:
      bool: True si n es primo (divisible solo por 1 y por sí
            mismo), False en caso contrario.
      Por definición, 1 no es primo.
    '''
    if n < 2:
        return False
    divisor = 2
    while divisor * divisor <= n:
        if n % divisor == 0:
            return False
        divisor = divisor + 1
    return True

Literate programming aplicado a funciones

En el enfoque de literate programming que venimos aplicando desde la Semana 1, la sección declarativa del programa (el docstring del nivel superior) describe el problema completo. Cuando modularizamos, cada función incorpora su propia sección declarativa: su docstring, que documenta su contrato específico. De esta forma, un programa modular bien documentado se lee como una narración en dos niveles: el nivel general (qué hace el programa) y el nivel de cada función (qué hace cada parte).

6. La Pila de Ejecución (Call Stack)

Cuando un programa invoca funciones, ¿cómo sabe Python a dónde regresar después del return? ¿Cómo maneja las variables locales de varias funciones que podrían estar activas simultáneamente? La respuesta está en una estructura de datos fundamental: la pila de ejecución (en inglés, call stack).

6.1. Stack frames: la unidad de la pila

Cada vez que se invoca una función, Python crea un stack frame (marco de pila) que contiene:

La pila opera bajo el principio LIFO (Last In, First Out): el último marco que se apila es el primero que se desapila. Esto garantiza que las funciones se completen en orden inverso al que fueron invocadas.

6.2. Traza de la pila con un ejemplo concreto

Sigamos paso a paso la ejecución del siguiente programa:

def cuadrado(x):
    '''Retorna x².'''
    return x * x

def distancia(x1, y1, x2, y2):
    '''Retorna la distancia euclídea entre dos puntos.'''
    dx = cuadrado(x2 - x1)
    dy = cuadrado(y2 - y1)
    return (dx + dy) ** 0.5

# --- Programa principal ---
d = distancia(0, 0, 3, 4)
print('Distancia:', d)     # → Distancia: 5.0

El siguiente simulador permite recorrer paso a paso la ejecución del programa, observando cómo se apilan y desapilan los stack frames. Use los botones para avanzar y retroceder:

Paso 1 / 8
1 def cuadrado(x):
2     return x * x
3
4 def distancia(x1,y1,x2,y2):
5     dx = cuadrado(x2 - x1)
6     dy = cuadrado(y2 - y1)
7     return (dx + dy) ** 0.5
8
9 d = distancia(0, 0, 3, 4)
10 print('Distancia:', d)
Pila de ejecución
↑ El tope de la pila está arriba
El programa comienza su ejecución. Se crean las definiciones de cuadrado y distancia en memoria (sin ejecutar su cuerpo).

💡 Observe los patrones

Al recorrer el simulador, preste atención a tres regularidades: (1) cada invocación apila un frame encima del anterior; (2) cada return desapila el frame del tope y entrega el valor al frame que queda debajo; (3) las variables locales de un frame son completamente independientes de las de otro, incluso si tienen el mismo nombre (como la x de las dos invocaciones a cuadrado).

🔗 Conexión holística: la pila en el hardware

La pila de ejecución no es un concepto exclusivo de Python: es una estructura de la arquitectura de computadoras que existe en la memoria RAM. En el documento Lenguajes de Máquina y Programación estudiamos que la CPU dispone de un registro especial llamado Stack Pointer (SP) que apunta a la posición actual del tope de la pila. Cada vez que se invoca una función, el SP se decrementa para «hacer espacio» al nuevo stack frame; al retornar, se incrementa para «liberar» ese espacio. Python, como lenguaje de alto nivel, gestiona esto automáticamente a través de su máquina virtual CPython, pero el mecanismo subyacente es el mismo que utiliza un programa en C o ensamblador.

6.3. El stack trace: lectura de errores

Cuando un error ocurre dentro de una función, Python muestra un stack trace (traza de pila): un informe que recorre la cadena de invocaciones desde el programa principal hasta el punto exacto del error. Saber leer esta traza es una habilidad esencial de depuración.

Traceback (most recent call last):
  File "programa.py", line 12, in <module>
    d = distancia(0, 0, 3, 4)
        ^^^^^^^^^^^^^^^^^^^^^^^^
  File "programa.py", line 7, in distancia
    dx = cuadrado(x2 - x1)
         ^^^^^^^^^^^^^^^^^^^^^
  File "programa.py", line 3, in cuadrado
    return x * x
           ^^^^^
TypeError: can't multiply sequence by non-int of type 'str'

💡 Cómo leer un stack trace

El stack trace se lee de abajo hacia arriba. La última línea indica qué error ocurrió (TypeError). Luego, se asciende por las líneas para ver dónde ocurrió: primero en cuadrado (línea 3), que fue invocada desde distancia (línea 7), que fue invocada desde el programa principal (línea 12). Esta cadena es exactamente el contenido de la pila de ejecución en el momento del error.

7. Compiladores e Intérpretes: Cómo Llega el Código a la CPU

Ahora que entendemos las funciones, la pila de ejecución y cómo Python gestiona las invocaciones, podemos profundizar en una pregunta que introdujimos en las primeras semanas: ¿cómo se traduce el código fuente que escribimos a instrucciones que la CPU pueda ejecutar?

7.1. Compilación vs. Interpretación

AspectoCompilaciónInterpretación
Proceso El código fuente completo se traduce una vez a código de máquina antes de ejecutarse. Se produce un archivo ejecutable. El código fuente se traduce y ejecuta línea a línea (o sentencia a sentencia) en tiempo de ejecución.
Errores Se detectan antes de ejecutar (en tiempo de compilación): el programa no se genera si hay errores sintácticos o de tipo. Se detectan durante la ejecución: el programa se detiene al encontrar un error.
Velocidad de ejecución Más rápida: el código ya está traducido a instrucciones nativas de la CPU. Más lenta: hay una capa adicional de traducción en tiempo real.
Portabilidad Menor: el ejecutable es específico para una arquitectura (x86, ARM). Mayor: el mismo código fuente se ejecuta donde haya un intérprete instalado.
Ejemplos C, C++, Rust, Go Python (CPython), JavaScript, Ruby

7.2. Python: un caso híbrido

Python utiliza una estrategia intermedia. Cuando ejecutamos un programa .py, CPython (la implementación de referencia de Python) realiza dos pasos:

Proceso de ejecución en CPython Código fuente .py Compilación Bytecode .pyc Interpretación Máquina Virtual CPython (PVM) Ejecución CPU Hardware Paso 1: se compila a código intermedio Paso 2: la PVM interpreta el bytecode instrucción a instrucción

Este modelo híbrido combina beneficios de ambos enfoques: la compilación a bytecode detecta errores de sintaxis antes de ejecutar, y el bytecode es portable entre plataformas (solo se necesita que CPython esté instalado). La contrapartida es que la ejecución es más lenta que la de un programa compilado a código nativo.

7.3. El módulo dis: observando el bytecode

Python incluye un módulo llamado dis (disassembler) que permite inspeccionar el bytecode generado para cualquier función. Esto permite verificar empíricamente cómo Python traduce nuestro código:

import dis

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

dis.dis(sumar)
  3           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 RETURN_VALUE

Lectura del bytecode

LOAD_FAST 0 carga el primer parámetro (a) en la pila de operandos de la PVM. LOAD_FAST 1 carga el segundo (b). BINARY_ADD toma los dos valores del tope de la pila, los suma y coloca el resultado en el tope. RETURN_VALUE devuelve ese resultado al invocador. Observe la similitud conceptual con las operaciones de la ALU que estudiamos en Arquitecturas de Computadoras: cargar operandos, operar y almacenar el resultado.

8. Modelo de Programa con Funciones

El modelo de programa que hemos utilizado desde la Semana 1 (Sección Declarativa + Sección Algorítmica con Prólogo, Resolución y Epílogo) se extiende naturalmente para incorporar funciones. La estructura de un programa modular queda:

Modelo de programa modular SECCIÓN DECLARATIVA Docstring general: descripción del problema, casos de prueba, recursos globales DEFINICIÓN DE FUNCIONES Cada función con su docstring (contrato), parámetros y return Ordenadas de menor a mayor nivel de abstracción (las auxiliares primero) SECCIÓN ALGORÍTMICA (Programa Principal) 1. PRÓLOGO Presentación al usuario 2. RESOLUCIÓN Invocación de funciones 3. EPÍLOGO Exhibición de resultados El programa principal es breve: delega los cálculos a las funciones y coordina el flujo general

8.1. Ejemplo completo: estadísticas de notas

A continuación, un programa completo que sigue el modelo modular. El problema es calcular el promedio, la nota máxima y la mínima de una secuencia de notas ingresadas por el usuario, donde el valor -1 actúa como centinela.

'''
SECCIÓN DECLARATIVA
Descripción: Calcular promedio, máxima y mínima de notas (centinela: -1).
Análisis de casos de prueba:
  Notas 7, 4, 9, -1 → promedio≈6.67, máx=9, mín=4
  Nota 10, -1 → promedio=10.0, máx=10, mín=10
  Nota -1 (vacío) → mensaje «No se ingresaron notas»
'''

# --- Definición de funciones ---

def leer_nota():
    '''Lee una nota entera del teclado.
    Retorna: int — la nota ingresada (o -1 como centinela).'''
    texto = input('Nota (-1 para terminar): ')
    return int(texto)

def procesar_notas():
    '''Procesa la secuencia de notas y retorna (suma, cantidad, máx, mín).
    Si no se ingresaron notas, retorna (0, 0, 0, 0).'''
    suma = 0
    cantidad = 0
    maxima = 0
    minima = 11    # Mayor que cualquier nota válida (0-10)
    nota = leer_nota()
    while nota != -1:
        suma = suma + nota
        cantidad = cantidad + 1
        if nota > maxima:
            maxima = nota
        if nota < minima:
            minima = nota
        nota = leer_nota()
    return suma, cantidad, maxima, minima

def mostrar_estadisticas(suma, cantidad, maxima, minima):
    '''Exhibe las estadísticas con formato.'''
    if cantidad > 0:
        promedio = suma / cantidad
        print('Promedio:', promedio)
        print('Máxima:', maxima)
        print('Mínima:', minima)
    else:
        print('No se ingresaron notas.')

# --- Programa principal ---

# 1. Prólogo
print('=== Estadísticas de Notas ===')

# 2. Resolución
s, c, mx, mn = procesar_notas()

# 3. Epílogo
mostrar_estadisticas(s, c, mx, mn)

Observe la estructura

El programa principal ocupa apenas cinco líneas de código ejecutable (excluyendo comentarios y líneas en blanco). Todo el trabajo complejo está delegado a funciones con nombres que describen su propósito. Un lector que examine solo el programa principal puede entender qué hace el programa sin leer el cómo de cada función: esta es la esencia de la abstracción funcional.

9. Criterios de Diseño: ¿Cuándo Crear una Función?

La decisión de cuándo abstraer un bloque de código en una función no es arbitraria. Los siguientes criterios proporcionan una guía práctica:

CriterioDescripciónEjemplo
Reutilización El mismo bloque de código aparece (o podría aparecer) en más de un lugar. leer_entero(mensaje) se usa cada vez que se necesita un entero del teclado.
Legibilidad Un bloque complejo puede reemplazarse por una invocación con nombre descriptivo que mejora la comprensión del programa principal. es_bisiesto(anio) es más legible que la expresión booleana completa.
Complejidad Un fragmento de código supera las 10–15 líneas o maneja múltiples responsabilidades. Dividir «validar y procesar entrada» en validar_entrada() y procesar().
Testabilidad Se necesita verificar un cálculo de forma aislada, sin ejecutar todo el programa. es_primo(n) puede probarse con una tabla de valores conocidos.
Modificabilidad Se anticipa que una parte del programa podría cambiar (requisitos, algoritmo, formato). formatear_salida(datos) encapsula una decisión de formato que puede evolucionar.

💡 Regla heurística del «nombre significativo»

Si puedes ponerle a un bloque de código un nombre que lo describa mejor que el código mismo, ese bloque merece ser una función. Si el nombre más descriptivo que encuentras es hacer_cosas() o procesar_datos(), probablemente la función intenta hacer demasiado y debería dividirse.

10. Visión de Conjunto

Las funciones, la modularización y la abstracción representan un cambio cualitativo en nuestra forma de programar: pasamos de escribir secuencias lineales de instrucciones a diseñar soluciones como un conjunto de componentes colaborativos. Este cambio de paradigma —que parece puramente técnico— es en realidad una herramienta cognitiva: nos permite enfrentar problemas más complejos de los que podríamos manejar con código monolítico.

🗺️ Conexiones del tema

⬅️
Semanas 1–3: Variables, tipos de datos, algoritmos secuenciales, estructuras condicionales y los siete patrones de procesamiento de secuencias proporcionaron las herramientas fundamentales. Ahora esas herramientas se encapsulan en funciones reutilizables: cada patrón puede convertirse en una función genérica.
📌
Semana 4 (este tema): Funciones (definición, parámetros, retorno, scope), paso de argumentos (anticipo), programación modular (descomposición top-down, cohesión, acoplamiento), abstracción funcional (contratos de interfaz, docstrings) y pila de ejecución. La introducción a compiladores e intérpretes y al bytecode de Python.
➡️
Semana 5: Algoritmos de búsqueda (lineal y binaria) y complejidad algorítmica. Las funciones serán la herramienta natural para implementar cada algoritmo, y la noción de contrato permitirá especificar precondiciones (por ejemplo, que la búsqueda binaria requiere una secuencia ordenada).
🔮
Semanas posteriores: La pila de ejecución reaparecerá como concepto central al estudiar la recursividad (Semana 8), donde cada llamada recursiva apila un nuevo frame. En la Semana 7 se profundizará en referencias, mutabilidad y paso de argumentos, expandiendo lo que aquí se introdujo como alcance de variables. El proyecto final (Semanas 12–16) exigirá descomposición modular completa.

🔗 Conexión holística: funciones, hardware y sistema operativo

Las funciones no son un concepto exclusivo de los lenguajes de alto nivel. En el nivel de la arquitectura de computadoras, una función se implementa mediante instrucciones CALL (que apilan la dirección de retorno y saltan al código de la función) y RET (que desapilan la dirección y regresan). El Stack Pointer (SP) y el Base Pointer (BP) son registros de CPU dedicados a gestionar la pila.

Desde la perspectiva del sistema operativo, cada proceso tiene su propia pila de ejecución como parte de su espacio de memoria. El tamaño de la pila es limitado (típicamente entre 1 y 8 MB); si se apilan demasiados frames (por ejemplo, por una recursión infinita), se produce un stack overflow — un desbordamiento de pila que el SO detecta y que causa la terminación del proceso.


Funciones, Modularización y Abstracción — Semana 4
Desarrollo de Contenidos — Algoritmia y Programación — Carrera de Informática