Distinguir los elementos fundamentales de los lenguajes de programación, considerando el entorno de desarrollo, su estructura y sintaxis, para utilizarlos en la creación de programas de cómputo.
Todo lenguaje de programación —imperativo, orientado a objetos, funcional o lógico— se construye sobre un mismo puñado de decisiones de diseño. Antes de comparar paradigmas (Unidades III-V), necesitamos el vocabulario preciso para describir qué varía entre ellos.
Ya vimos sintaxis (qué se puede escribir) y semántica (qué significa). Ahora preguntamos: ¿cómo un nombre llega a tener un significado concreto durante la ejecución?
Encapsulamiento (POO), inmutabilidad (funcional) y unificación (lógico) son formas distintas de resolver los mismos problemas: nombres, alcance, memoria y tipos.
Cuando un programa ejecuta x = f(y), ¿qué determina de dónde viene el valor de y, dónde vive el resultado en memoria, y qué puede hacer f con lo que recibe? Responder esto con precisión es el objetivo de las 5 secciones siguientes.
Cinco subtemas más una sección de profundización en Python, enmarcados en una clase de 180 minutos. Cada bloque alterna teoría, animación y una corrida de escritorio que vuelve visible el proceso interno. Clic en cada bloque para ir a su diapositiva.
x = f(y)?Objetivo de la sesión (enmarcarlo al inicio): definir con precisión nombres, alcance, memoria y tipos en un paradigma imperativo, usando Python como espejo.
Antes de 2.1: el "bug inicial" — muestra x = f(y) y pregunta: "¿de dónde sale el valor de y, dónde vive el resultado y qué puede hacer f con lo que recibe?". Conduce a la diapositiva 1.
Ritmo: 2 bloques largos de 35 min con animaciones; no rebasar los recesos. La corrida de cierre (curso, 140–165 min) es el momento de integrar los 3 ejes.
Un nombre (identificador) es una etiqueta textual. Un objeto es la entidad real en memoria. La asociación entre ambos se llama binding (ligadura) — y no siempre es de uno a uno.
El momento en que ocurre la ligadura puede ser en el diseño del lenguaje, en compilación, en enlace (linking) o en ejecución. Cuanto más tarde ocurre, mayor flexibilidad pero menor eficiencia (Sebesta, cap. 5).
int x; liga el nombre x a un tipo desde la compilación. El binding con un valor concreto ocurre después, pero el tipo ya quedó fijo.
x = 5 liga el nombre a un objeto en ejecución; el mismo nombre puede religarse luego a un objeto de otro tipo.
En lenguajes con semántica de referencia (Python, Java para objetos), una variable almacena una referencia al objeto, no el objeto mismo. Esto permite el aliasing.
a = [1, 2, 3] b = a # b y a son dos NOMBRES para el MISMO objeto b.append(4) print(a) # [1, 2, 3, 4] — el objeto cambió, visible desde ambos nombres print(id(a) == id(b)) # True: misma identidad de objeto
Confundir "reasignar un nombre" con "mutar un objeto". b = [9] rompe el alias; b.append(9) muta el objeto compartido. Volveremos a esta distinción con una animación en 2.4.
Un bloque es una región textual del programa (cuerpo de función, if, for) que puede introducir nuevos nombres. Un entorno es la función que, en un punto de ejecución dado, mapea cada nombre visible a su binding.
El alcance de un nombre es la región del programa donde ese nombre es visible y se refiere a un binding específico. Determina, para cada uso de un nombre, a qué entorno hay que preguntarle.
| Criterio | Estático (léxico) | Dinámico |
|---|---|---|
| Determinado por | Estructura textual del código | Secuencia de llamadas en ejecución |
| Momento de resolución | Compilación (conceptualmente) | Ejecución |
| Predictibilidad | Alta — se lee el código y se sabe el binding | Baja — depende de quién llamó |
| Lenguajes | Python, Java, C, Haskell, Scheme | Bash (variables), Common Lisp (special) |
El alcance dinámico dificulta el razonamiento local: no basta con leer una función para saber a qué se refiere un nombre libre; hay que rastrear la pila de llamadas completa. Por eso casi todos los lenguajes modernos usan alcance estático.
Orden de resolución de nombres: Local → Enclosing (funciones anidadas) → Global → Built-in. Es la implementación concreta del alcance estático en Python — la vemos en la siguiente animación.
interna() busca a x siguiendo la cadena léxica LEGB.Duración: ~8 min. Recorre la animación PASO A PASO (no la corrida directa): localiza primero el entorno correcto antes de dar "Siguiente", y haz que el grupo pronostique en qué entorno se encuentra x.
Punto clave: LEGB se detiene en el primer match — "externa" gana sobre "global". La corrida refuerza que el intérprete no "busca en todas partes".
Pregunta para la clase: ¿qué imprimiría si interna() tuviera su propia x = "interna"? (Respuesta: "interna").
Memoria organizada en marcos de activación (uno por cada llamada activa). Se libera automáticamente al retornar (LIFO). Guarda parámetros, variables locales, dirección de retorno.
Memoria de asignación dinámica, tamaño no conocido de antemano, tiempo de vida no ligado al de la función que la crea. Requiere gestión explícita o automática.
Cada llamada recursiva crea un nuevo marco de activación en el stack. En 2.3 veremos esa pila crecer y liberarse paso a paso con factorial(4).
| Estrategia | Cómo funciona | Riesgo / costo | Ejemplos |
|---|---|---|---|
| Manual | El programador libera explícitamente | Memory leaks, dangling pointers | C (malloc/free), C++ (new/delete) |
| Garbage Collection | El entorno detecta objetos inalcanzables y libera | Pausas de recolección, menos control fino | Java, Python, Go, JavaScript |
| Ownership | El compilador verifica en tiempo de compilación cuándo un valor pierde dueño | Curva de aprendizaje (borrow checker) | Rust |
Python combina conteo de referencias (libera en cuanto el contador llega a 0) con un recolector cíclico adicional para referencias circulares. sys.getrefcount(obj) muestra el conteo activo. Profundizamos esto en la sección de Python.
Se evalúa y produce un valor. Ej.: 3 + 4, f(x).
Se ejecuta por su efecto (asignación, impresión, control de flujo); no necesariamente produce un valor utilizable.
Lisp, Scheme y —en menor medida— Rust tratan casi todo como expresión ("todo devuelve un valor"), difuminando la distinción: un rasgo típico del paradigma funcional que veremos en la Unidad IV.
Comandos ejecutados uno tras otro, en orden textual. Es la estructura por defecto del paradigma imperativo.
Bifurca el flujo según una condición booleana.
def clasificar(n: int) -> str: if n < 0: return "negativo" elif n == 0: return "cero" else: return "positivo"
Repetición de un bloque mientras se cumpla una condición (while) o sobre una secuencia (for).
def suma_iterativa(n: int) -> int: acumulado = 0 for i in range(1, n + 1): acumulado += i return acumulado
acumulado, i = 0, 1 while i <= n: acumulado += i i += 1
Un subprograma que se invoca a sí mismo (directa) o mediante una cadena de llamadas que regresa a él (indirecta/mutua). Requiere caso base y caso recursivo que progrese hacia él.
def factorial(n: int) -> int: if n == 0: # caso base return 1 return n * factorial(n - 1) # caso recursivo
Scheme, Haskell y Scala garantizan tail-call optimization (TCO): reutilizan el marco de pila en llamadas de cola. Python NO lo hace — cada llamada consume un nuevo marco, sea o no de cola. El límite por defecto son ~1000 llamadas anidadas (sys.getrecursionlimit()).
for acumulador en recursiónMismo problema, dos mecanismos: la iterativa guarda el parcial en una variable mutable (acumulado); la recursiva lo distribuye en la pila de llamadas. La recursión aparece "a cambio de" la variable acumuladora.
def suma_iterativa(n: int) -> int: acumulado = 0 for i in range(1, n + 1): acumulado += i return acumulado
def suma_recursiva(n: int) -> int: if n == 0: # caso base return 0 return n + suma_recursiva(n - 1) # caso recursivo
Conviertan el for de abajo a recursión (el caso base es 0). La pista: el "acumulado" de cada nivel es el resultado que devuelve la llamada hija: return n + suma_recursiva(n - 1).
Duración: ~7 min. Corre la corrida una vez completa (10), luego pide el pronóstico de suma_recursiva(4) antes de la siguiente ejecución.
Diferencia clave para verbalizar: la iterativa reutiliza una celda de memoria (acumulado); la recursiva crea n marcos de pila, uno por nivel. De aquí sale el "costo de la elegancia".
Puente con 2.4: la recursividad se apoyará en el paso de parámetros y en la pila que veremos en la siguiente animación con factorial(4).
factorial(4)Duración: ~8 min. Camina los 10 pasos; enfatiza los momentos de push (crece abajo hacia arriba) y pop retornando (LIFO).
Verbaliza: cada marco guarda n, la dirección de retorno y el estado local — por eso la pila "recuerda" multiplicar n por el resultado hijo al desenrollar.
Antes de tocar la pila con un while: pregunta cuántas llamadas se apilan para factorial(4) (5: 4..0). Conecta con el límite recursionlimit ~1000 de la diapositiva 13.
Un subprograma es una unidad de código nombrada, parametrizable e invocable desde distintos puntos del programa.
Retorna un valor y, idealmente, no produce efectos secundarios (relevante al comparar con el paradigma funcional).
Invocado por su efecto, sin valor de retorno significativo (Pascal). Python solo tiene funciones — pueden devolver None.
Java/C++ resuelven en compilación cuál versión de un subprograma usar según tipo/cantidad de parámetros. Python no soporta sobrecarga real por firma: la última definición sobrescribe a las anteriores; se simula con valores por defecto, *args/**kwargs o functools.singledispatch.
| Mecanismo | Qué se copia/liga | Efecto sobre el original | Dónde aparece |
|---|---|---|---|
| Por valor | El valor del argumento | Modificar el parámetro NO afecta al original | C (primitivos), Java (primitivos) |
| Por referencia | La misma localización/variable | Modificar el parámetro SÍ afecta al original | C++ (&), Pascal (var) |
| Por referencia de objeto | Copia de la referencia al objeto | Mutar SÍ se refleja; reasignar NO | Python, Java (objetos), JavaScript |
El parámetro recibe una copia de la referencia. Si el objeto es mutable y se muta con un método, el cambio es visible fuera. Si el parámetro se reasigna, solo cambia el binding local. Lo vemos paso a paso en la siguiente animación.
print de salida. Solo una de las dos operaciones dejó huella en original.Duración: ~8 min. Usa las pestañas mutar/reasignar primero en la animación; la corrida sintetiza ambos casos en un solo script.
Punto clave: Python "pasa por referencia de objeto": mutar (lista.append) se ve fuera; reasignar (lista = ...) no. Es la misma idea de la diapositiva 4 (aliasing).
Pregunta trampa: ¿qué pasa si el parámetro se llama original y se escribe original = original.copy() al inicio? (Respuesta: rompe el alias deliberadamente).
Un tipo de dato define un conjunto de valores posibles y las operaciones válidas sobre ellos.
int, float, bool, char — no descomponibles.
Arreglos, registros, listas, tuplas, diccionarios — construidos a partir de otros tipos.
Definidos por su comportamiento, no por su representación interna — base conceptual de las clases en POO (Unidad III).
| Criterio | Estático | Dinámico | Fuerte | Débil |
|---|---|---|---|---|
| Cuándo se verifica | Compilación | Ejecución | — | |
| Coerción implícita | — | No permitida | Permitida | |
| Ejemplos | Java, C, Haskell | Python, JS | Python, Java | C, JavaScript |
Confundir "estático/dinámico" (cuándo se verifica) con "fuerte/débil" (qué tan estrictas son las conversiones). Python es dinámico y fuerte: "5" + 3 lanza TypeError, no concatena silenciosamente como haría JavaScript.
x = 5 x = "ahora soy texto" # válido: dinámico "5" + 3 # TypeError — Python es FUERTE, no coerciona
Mecanismo por el cual el compilador deduce el tipo de una expresión sin anotación explícita, preservando las garantías del tipado estático con la brevedad del dinámico.
def area_circulo(radio: float) -> float: return 3.14159 * radio ** 2
Las anotaciones no se verifican en ejecución; herramientas externas como mypy las validan estáticamente.
let x = 5; infiere i32.var desde Java 10.auto.Este tema conecta con los tipos algebraicos de la Unidad IV (tipos suma/producto en Haskell) y con el tipado estructural vs. nominal relevante al polimorfismo de la Unidad III.
A lo largo de la unidad usamos Python para ejemplificar cada concepto. Antes de la comparativa final, formalicemos por qué se comporta como se comporta.
Prioriza legibilidad y rapidez de desarrollo sobre el control de bajo nivel. "Explicit is better than implicit" (Zen de Python) — pero eso no significa verboso: significa sin magia oculta.
Tipado dinámico (se verifica en ejecución) y fuerte (no coerciona entre tipos incompatibles). Interpretado (CPython compila a bytecode e interpreta ese bytecode).
Python ilustra con claridad los 5 subtemas porque hace visibles decisiones que otros lenguajes ocultan en tiempo de compilación: el binding tardío de nombres, el paso por referencia de objeto, y un sistema de tipos totalmente dinámico.
En Python, enteros, funciones, clases, módulos e incluso tipos son objetos con identidad (id()), tipo (type()) y valor. No hay "tipos primitivos" en el sentido de C — todo pasa por el mismo modelo de referencias.
def saludar(): pass print(type(5)) # <class 'int'> print(type(saludar)) # <class 'function'> — la función también es un objeto print(id(5)) # dirección/identidad del objeto entero 5
Cada objeto lleva un contador de referencias. Cuando llega a 0, se libera inmediatamente (determinístico, a diferencia del GC de Java). Un recolector cíclico adicional se ejecuta periódicamente para detectar referencias circulares que el conteo por sí solo no puede romper (ej. dos objetos que se referencian mutuamente).
La animación de la sección 2.4 solo tiene sentido si distinguimos qué tipos de Python son mutables y cuáles no.
| Inmutables | Mutables |
|---|---|
int, float, bool | list |
str | dict |
tuple | set |
frozenset | Instancias de clases definidas por el usuario (por defecto) |
x = 5 def incrementar(n): n = n + 1 # int es inmutable: SIEMPRE crea un nuevo objeto y religa localmente incrementar(x) print(x) # 5 — nunca cambia, sea cual sea el tipo, si solo se reasigna
Si el tipo es inmutable, ninguna operación dentro de una función puede afectar al objeto del llamador — como máximo se religa el nombre local. Si es mutable, los métodos que mutan in situ (.append, .update, [i] =) sí se ven reflejados fuera.
| Aspecto | Python | Java | C++ |
|---|---|---|---|
| Alcance | Estático (LEGB) | Estático (por bloque) | Estático (por bloque) |
| Sistema de tipos | Dinámico, fuerte | Estático, fuerte | Estático, moderadamente débil |
| Gestión de memoria | Refcounting + GC cíclico | GC generacional (JVM) | Manual (new/delete) o RAII |
| Paso de parámetros | Por referencia de objeto (todo) | Por valor (primitivos); por referencia de objeto (objetos) | Por valor por defecto; por referencia explícita con & |
| Sobrecarga de funciones | No soportada nativamente | Sí, resuelta en compilación | Sí, resuelta en compilación |
| Recursión de cola (TCO) | No optimizada | No optimizada | Depende del compilador (no garantizado por el estándar) |
Una función que primero muta la lista y luego reasigna el parámetro. Ejecuta la corrida en cada pestaña y fíjate cómo cambia (o no) el resultado según la semántica de paso de parámetros de cada lenguaje.
def arreglar(lista): lista.append(99) lista = [7, 7] # religa el nombre local original = [1, 2, 3] arreglar(original) print(original)
static void arreglar(List<Integer> lista) { lista.add(99); lista = new ArrayList<>(); // religa la referencia local } var original = new ArrayList<Integer>(List.of(1,2,3)); arreglar(original); System.out.println(original);
void arreglar(std::vector<int> lista) { // paso POR VALOR lista.push_back(99); lista = {7, 7}; // trabaja sobre la copia } std::vector<int> original{1, 2, 3}; arreglar(original); // copia: original intacto std::cout << original;
Python y Java imprimen [1, 2, 3, 99]: la mutación append/add tocó el objeto compartido y la reasignación local se perdió. C++ imprime [1, 2, 3]: al pasar por valor se copió el vector y ninguna de las dos operaciones afectó al original — el contraste que define, en una línea, las tres semánticas.
Duración: ~15 min (Bloque 3). Es LA corrida de integración. Pide pronóstico antes de cada tab (3 veces) antes de revelar la salida.
Encaje conceptual: usa la tabla comparativa (diapositiva anterior) como respuesta de la razón: Python (referencia de objeto), Java (referencia de valor de objeto), C++ (valor, salvo &). Para C++ menciona que con & el resultado sería como Python.
Al terminar: regresa a la pregunta central de la unidad y salta al mapa conceptual.
Asumir que un nombre libre en una función anidada se resuelve "buscando en todas partes" en vez de seguir estrictamente la cadena LEGB, deteniéndose en el primer match.
Confundir "el nombre cambió de valor" con "el objeto se mutó". Son eventos distintos que solo coinciden cuando el tipo es mutable y se muta in situ.
Olvidar el caso base, o escribir un caso recursivo que no progresa hacia él — produce recursión infinita y agota la pila (RecursionError en Python).
Esperar que def f(n): n = n + 1 modifique la variable externa en Python — nunca ocurre, sin importar el tipo, porque es una reasignación, no una mutación.
Tratar "dinámico" y "débil" como sinónimos. Python es dinámico y fuerte a la vez; JavaScript es dinámico y débil — son ejes distintos.
Intentar definir dos funciones con el mismo nombre en Python esperando comportamiento tipo Java/C++ — la segunda definición sobrescribe silenciosamente a la primera.
Uso: diapositiva de "parcheo". Antes de iniciar el bloque de Python, pide a la clase votar cuál error les ha pasado alguna vez.
Diagnóstico rápido: las tarjetas de error son el checklist del examen. Los dos errores con más incidencia real en los años anteriores: paso de parámetros y dinámico/débil.
Estos cinco ejes reaparecerán, con otro nombre, en cada paradigma de las Unidades III-V.
Implementa en Python un programa que contenga:
Documenta con comentarios qué mecanismo de scope/paso de parámetros se está observando en cada bloque.
¿Qué problemas puede causar el alcance dinámico frente al estático en programas grandes?
¿Por qué la recursión es central en el paradigma funcional pero opcional en el imperativo?
¿Qué ventajas y riesgos trae un sistema de tipos con inferencia frente a uno con anotación explícita?
Si Python "pasa por referencia de objeto", ¿por qué incrementar(x) con x = 5 nunca modifica x fuera de la función?
Marcar una opción por pregunta y presionar Calificar. El sistema resalta aciertos y errores con retroalimentación. No cuenta puntos: es para saber qué reforzar antes del examen.
1 · Alcance. Dado def f(): x = 7, x = 5, f(), print(x) — ¿qué imprime el programa?
2 · Memoria / aliasing. Si a = [1, 2] y b = a, tras b.append(3) con print(a) obtenemos…
3 · Control / recursión. Una función recursiva sin caso base que pueda alcanzarse provoca…
4 · Parámetros. def f(n): n = n + 1, x = 5, f(x), print(x) — ¿qué imprime?
5 · Tipos. En Python, "5" + 3 produce…
Duración: ~8 min individual, en silencio. Después, 5 min de socialización: cada pregunta la lee un estudiante; usa las explicaciones del quiz para cerrar el debate.
Función: datos para ti (qué reforzar) y para ellos (metacognición). Preguntas con mayor caída histórica: la 3 (recursión) y la 5 (fuerte vs. dinámico).
Opción: si el grupo está trabado, abre la animación correspondiente (7 LEGB, 18 mutar/reasignar) antes de revelar la respuesta correcta.
Anota tres conceptos que hoy quedaron claros.
Dos puntos que quieras repasar o ver más despacio.
Una idea que conecte la clase con tu forma de programar o tu proyecto.
Dr. Jose Manuel Muñoz Contreras
Facultad de Ciencias de la Ingeniería y Tecnología (FCITEC) — Unidad Valle de las Palmas, UABC
Ingeniería en Desarrollo de Software y Tecnologías Emergentes — Plan de Estudios 2021-2
Paradigmas de Programación · Grupo 544, Subgrupo 0
Cierre (spa 165–180): primero el mapa conceptual (slide 28) para integrar, luego el mini-quiz (31), y al final la tarjeta de salida. La tarjeta es la evidencia formativa de la sesión: úsala para decidir qué repasar en la próxima.
Las lecturas de Sebesta/Pratt/Scott son las referencias oficiales de la asignatura; señalarlas aquí cierra el ciclo guion→presentación→bibliografía.