- Aprender
- Informática
- Ciencias de la computación
- Introducción a la informática
- Lenguajes y paradigmas de programación
Lenguajes y paradigmas de programación
PrincipianteUnidad 5 de 7 del curso Curso de Introducción a la informática
Lo que aprenderás
- Sigue un programa desde el código que escribe una persona hasta las instrucciones binarias que ejecuta un procesador y compara las formas de realizar esa traducción: de antemano, durante la ejecución del programa o mediante una combinación de ambas.
- Compara el código máquina, el lenguaje ensamblador y los lenguajes de alto nivelDescribe los tres niveles en los que se pueden escribir programas, muestra cómo una línea de código de alto nivel se traduce en varias instrucciones máquina y explica por qué la mayor parte del software se escribe en el nivel más alto.
- Explica qué hace un compiladorDescribe cómo un compilador traduce un programa completo antes de su ejecución, nombra sus etapas principales y explica qué ocurre con los errores y con el archivo ejecutable final.
- Explica los intérpretes, el código de bytes y la compilación en tiempo de ejecuciónDescribe cómo ejecuta un programa un intérprete, cómo combinan la compilación y la interpretación lenguajes como Java y Python y cómo valorar las ventajas e inconvenientes de estos enfoques.
- Compara el código máquina, el lenguaje ensamblador y los lenguajes de alto nivel
- Separa las reglas de escritura del código de las reglas que determinan lo que hace, reconoce las estructuras básicas que comparten la mayoría de los lenguajes de alto nivel y sigue programas breves para predecir sus resultados.
- Distingue la sintaxis de la semánticaExplica la diferencia entre la forma de un programa y su significado, distingue los errores de sintaxis, de ejecución y lógicos y detecta cuándo distintos lenguajes dan un significado diferente a un código de aspecto idéntico.
- Sigue la ejecución de las estructuras básicas comunes a los programas de alto nivelReconoce las estructuras presentes en casi todos los lenguajes de alto nivel (variables, expresiones, flujo de control y funciones con su ámbito) y sigue un programa breve paso a paso para predecir su salida.
- Distingue la sintaxis de la semántica
- Describe cómo clasifican y comprueban los lenguajes los valores según su tipo, dónde guarda sus datos un programa en ejecución y cómo se reserva y libera la memoria, de forma manual o automática, con las ventajas e inconvenientes de seguridad y velocidad de cada opción.
- Compara cómo usan y comprueban los lenguajes los tipos de datosExplica qué es un tipo de dato, compara el tipado estático y dinámico y las reglas de conversión estrictas y flexibles y predice cómo tratan los lenguajes habituales la mezcla de tipos y los límites numéricos.
- Describe dónde guarda sus datos un programa en ejecuciónExplica las variables como posiciones de memoria con nombre, las funciones de la pila de llamadas y el montículo de memoria y la diferencia entre copiar un valor y compartir una referencia.
- Compara la gestión manual y automática de memoriaExplica cómo se libera la memoria en los lenguajes donde la gestiona quien programa y en los que usan recolección de basura o reglas de propiedad de los datos, y relaciona los errores de memoria con fallos de los programas y problemas de seguridad.
- Compara cómo usan y comprueban los lenguajes los tipos de datos
- Describe los programas como secuencias de órdenes que cambian el estado almacenado y después como conjuntos de objetos que reúnen datos y comportamiento, y utiliza clases, encapsulación, herencia y polimorfismo para organizar el código.
- Describe los estilos imperativo y procedimentalExplica la programación imperativa como órdenes paso a paso que cambian el estado de un programa, muestra cómo organizan esas órdenes los procedimientos y el flujo de control estructurado y reconoce los riesgos del estado compartido que cambia.
- Modela datos y comportamiento mediante clases y objetosExplica cómo agrupa la programación orientada a objetos los datos y sus operaciones en objetos, lee y diseña una clase sencilla y utiliza la encapsulación para proteger los datos de un objeto.
- Reutiliza y amplía clases mediante herencia, polimorfismo y composiciónUtiliza la herencia para construir clases especializadas, explica cómo permite el polimorfismo que una llamada se comporte de forma distinta con diferentes objetos y decide cuándo combinar objetos en lugar de heredar.
- Describe los estilos imperativo y procedimental
- Construye programas con funciones que devuelven valores nuevos en lugar de cambiar los existentes y escribe programas que expresan el resultado deseado mientras el motor del lenguaje decide cómo calcularlo.
- Crea programas a partir de funciones puras y datos inmutablesExplica las funciones puras, la inmutabilidad, las funciones de primera clase y de orden superior y la recursión, y reescribe un bucle como una cadena de operaciones con
map,filteryreduce. - Describe el resultado mediante lenguajes declarativosExplica la programación declarativa como la expresión del resultado deseado en lugar de cómo obtenerlo y lee ejemplos de un lenguaje de consulta de bases de datos, un lenguaje lógico y otras notaciones declarativas.
- Crea programas a partir de funciones puras y datos inmutables
- Resuelve el mismo problema con distintos estilos, reconoce que la mayoría de los lenguajes modernos mezclan paradigmas y valora los factores técnicos y prácticos que determinan qué lenguaje se adapta a un proyecto.
- Resuelve un mismo problema con varios paradigmasEscribe la misma tarea pequeña con los estilos imperativo, orientado a objetos, funcional y declarativo, compara qué facilita cada versión y reconoce los lenguajes multiparadigma.
- Elige un lenguaje de programación para una tareaValora los factores que determinan qué lenguaje se adapta a un proyecto (plataforma, dominio, rendimiento, seguridad, ecosistema y personas) y justifica una elección para un caso concreto.
- Resuelve un mismo problema con varios paradigmas
Guía de estudio de Lenguajes y paradigmas de programación
1Del código fuente a las instrucciones en ejecución
Un procesador ejecuta pequeñas instrucciones binarias, mientras que las personas suelen escribir programas en lenguajes organizados en torno al problema que quieren resolver. Los traductores conectan estos dos niveles. Sigue el mismo cálculo a través de esas capas y después compara la construcción de un programa antes de ejecutarlo con su interpretación o la compilación de partes durante la ejecución. La vía elegida afecta a la portabilidad, el tiempo de inicio, la velocidad y el momento en que puede aparecer un error.
1.1 Tres niveles de código de programa
Una línea como x = a + b expresa un cálculo sin mencionar ningún registro del procesador. La abstracción lo hace posible: un lenguaje oculta los detalles de niveles inferiores para que quien programa pueda centrarse en el trabajo que debe realizar el programa.
Qué ejecuta el procesador
El código máquina consiste en instrucciones codificadas como números binarios. La arquitectura del conjunto de instrucciones (ISA) de un procesador define sus instrucciones y sus codificaciones. x86-64 es habitual en computadoras personales y servidores; ARM es habitual en teléfonos, tabletas y muchos portátiles. El código construido para una ISA necesita traducción o emulación para funcionar en otra.
El sistema hexadecimal permite mostrar los bytes de las instrucciones de forma más breve. En x86-64, B8 01 00 00 00 coloca 1 en el registro EAX. Los bytes 01 D8 suman el valor de EBX a EAX. Los registros son pequeñas posiciones de almacenamiento dentro del procesador. Cada instrucción realiza una acción pequeña, como mover datos, sumar, comparar o saltar. El ciclo de búsqueda, decodificación y ejecución ejecuta estas instrucciones.
Nombres para las mismas acciones pequeñas
El lenguaje ensamblador sustituye los bytes de las instrucciones por nombres legibles llamados mnemónicos. Esas dos instrucciones de x86-64 pueden escribirse así:
mov eax, 1
add eax, ebx
Un ensamblador convierte el lenguaje ensamblador en código máquina. Una línea de instrucción habitual suele producir una instrucción máquina. Los nombres de los registros y las instrucciones siguen perteneciendo a una ISA concreta, por lo que usar palabras en lugar de bytes no hace portable al lenguaje ensamblador.
Describe un problema en menos líneas
Un lenguaje de alto nivel proporciona variables, expresiones, condiciones, bucles y funciones. Python, C, Java y JavaScript permiten describir operaciones sin seleccionar registros. Una sentencia suele requerir varias instrucciones máquina. Una traducción simplificada de x = a + b haría lo siguiente:
- Cargar el valor de
aen un registro. - Cargar el valor de
ben otro registro. - Sumar los valores de los registros.
- Guardar el resultado para
x.
Un compilador real puede combinar o reordenar esos pasos manteniendo el comportamiento requerido.
| Nivel | Qué escribe quien programa | ¿Qué se encarga de los detalles del hardware? |
|---|---|---|
| Código máquina | Instrucciones codificadas del procesador | Quien programa o el generador de código |
| Lenguaje ensamblador | Mnemónicos y registros | Quien programa, con un ensamblador que codifica las instrucciones |
| Código de alto nivel | Variables y operaciones sobre los datos del problema | Un compilador, un intérprete o una combinación |
El código fuente de alto nivel es más fácil de leer, modificar y comprobar. Un código fuente portable en C puede compilarse para un sistema x86-64 y también para un sistema ARM. Cada compilación sigue necesitando el soporte y las bibliotecas adecuados del sistema operativo. La portabilidad del código fuente no convierte un ejecutable nativo en universal.
Por qué siguen importando los niveles inferiores
El lenguaje ensamblador sigue siendo útil en el código de arranque, algunas rutinas de sistemas operativos y controladores, sistemas embebidos pequeños y el análisis de seguridad de programas cuyo código fuente no está disponible. Para el código de aplicaciones habitual, los compiladores modernos suelen producir código máquina con un rendimiento comparable al del ensamblador escrito a mano.
El avance hacia niveles superiores fue gradual. El lenguaje ensamblador se extendió a finales de la década de 1940 y principios de la de 1950. El sistema A-0 de Grace Hopper, de 1952, ayudó a impulsar la traducción automática. IBM publicó FORTRAN en 1957; COBOL apareció en 1959 y Lisp surgió hacia 1958–1960. «Alto nivel» indica la distancia respecto a los detalles del hardware, no la dificultad de aprender el lenguaje.
Puntos clave
- Un procesador ejecuta instrucciones máquina definidas por su ISA.
- Un ensamblador codifica las instrucciones del lenguaje ensamblador; este sigue ligado a una ISA.
- Las sentencias de alto nivel ocultan los pasos del hardware y suelen convertirse en varias instrucciones.
- El código fuente portable puede traducirse para distintos destinos; el código máquina nativo es específico de su destino.
- El código de bajo nivel sigue siendo útil cuando lo requiere el acceso al hardware o su análisis.
1.2 Cómo construye un programa un compilador
Un compilador traduce código fuente a código destino. El destino puede ser código máquina nativo o una forma intermedia, como el código de bytes. La compilación anticipada (AOT) se realiza antes de ejecutar el programa; la compilación en tiempo de ejecución se realiza mientras este se ejecuta.
Del texto a las instrucciones
En una construcción anticipada, varias etapas transforman un archivo fuente en instrucciones:
- El análisis léxico divide el texto en componentes léxicos: piezas con significado, como nombres, operadores y números.
- El análisis sintáctico comprueba la gramática y construye una estructura que muestra cómo encajan las piezas.
- El análisis semántico comprueba las reglas de significado, incluidas las declaraciones y las combinaciones de tipos permitidas.
- La optimización busca una forma más eficiente de realizar el trabajo requerido.
- La generación de código produce las instrucciones de destino.
Considera la sentencia de C total = price * 3;. Sus seis componentes léxicos son total, =, price, *, 3 y ;. Los nombres son identificadores, 3 es un literal entero y el punto y coma termina la sentencia. El análisis sintáctico registra que la multiplicación está dentro de la asignación: calcula price * 3 y después almacena su valor en total.
Estas etapas tienen tareas distintas. Una secuencia de componentes léxicos correctamente escritos puede incumplir la gramática. Una sentencia gramaticalmente correcta puede usar mal un tipo. Identificar los componentes léxicos no basta para establecer que un programa es válido.
Reduce el coste del mismo trabajo
Supón que el código fuente contiene seconds_per_day = 60 * 60 * 24. Los factores son constantes, por lo que el compilador puede multiplicar 60 por 60 para obtener 3600 y después multiplicar por 24 para obtener 86 400 durante la construcción. Puede colocar el valor final en la salida en lugar de repetir la multiplicación durante la ejecución. Esto es el plegado de constantes.
Una optimización debe conservar el comportamiento que exige el lenguaje. No puede sustituir un cálculo por otro más rápido que dé resultados distintos de los requeridos. El objetivo es reducir el trabajo, el tiempo o la memoria manteniendo el significado del programa.
Une las piezas
Los programas grandes suelen construirse a partir de muchos archivos. Un compilador puede producir un archivo objeto por cada archivo fuente. Un archivo objeto contiene código traducido que aún puede necesitar conexiones con otro código. Un enlazador une las piezas y resuelve las llamadas a rutinas de bibliotecas, como funciones para mostrar texto o realizar cálculos matemáticos.
| Método de enlace | De dónde procede el código de la biblioteca durante la ejecución |
|---|---|
| Enlace estático | Código incluido en el ejecutable durante la construcción |
| Enlace dinámico | Una biblioteca compartida compatible cargada al inicio o durante la ejecución |
El ejecutable resultante se construye para una plataforma de destino, incluidas una ISA y las convenciones del sistema operativo. El usuario no necesita el compilador anticipado para ejecutarlo. Aun así, pueden requerirse bibliotecas compartidas o componentes del entorno de ejecución.
Entiende qué demuestra una construcción correcta
En Java, int count = "five"; no supera una comprobación de tipos antes de la ejecución. Un texto no puede inicializar esa variable entera. El programa debe superar las comprobaciones de compilación requeridas antes de disponer de una versión ejecutable.
Otros fallos sobreviven a la compilación. Dividir por un cero introducido por el usuario depende de los datos de ejecución. Un programa pensado para restar que, en cambio, suma puede ejecutarse y devolver un valor incorrecto. La compilación comprueba las reglas del lenguaje; las pruebas también deben comprobar que el resultado corresponde a la tarea prevista.
Puntos clave
- La compilación traduce el código fuente a código destino, que no tiene por qué ser código máquina.
- Las etapas del compilador comprueban los componentes léxicos, la estructura y el significado antes de producir las instrucciones de destino.
- La optimización conserva el comportamiento requerido; el plegado de constantes evalúa constantes durante la construcción.
- Un enlazador conecta archivos objeto y código de bibliotecas.
- Una construcción correcta aún puede contener fallos de ejecución o errores lógicos.
1.3 Interpretación, código de bytes y compilación JIT
Un programa no tiene que convertirse en un ejecutable nativo antes de que un usuario lo inicie. Un intérprete ejecuta el programa mediante otro programa en ejecución. Muchas implementaciones combinan interpretación y compilación, por lo que la pregunta útil es cómo ejecuta el código ese sistema concreto.
Ejecuta mediante un intérprete
Un intérprete debe estar disponible cada vez que ejecuta el programa. Un REPL interactivo repite cuatro acciones: leer una entrada, evaluarla, mostrar un resultado y volver a empezar para la siguiente entrada. Ofrece una respuesta rápida al probar expresiones pequeñas.
Interpretar no significa necesariamente leer el código fuente directamente, línea por línea. CPython, la implementación estándar de Python, primero compila un archivo a código de bytes. Después, una máquina virtual de software ejecuta esas instrucciones internas. El código de bytes pertenece a ese entorno de ejecución; no es el conjunto de instrucciones nativo del procesador.
Por qué algunos errores aparecen antes
Compara dos archivos de Python. Este tiene una sintaxis válida:
print("start")
x = 10
print(x + "5")
Muestra «start», vincula x a 10 y después genera TypeError. La suma de ese entero y una cadena solo falla cuando se ejecuta la expresión. Una operación similar en una rama que no se visita puede pasar inadvertida hasta que se ejecute esa rama.
Ahora elimina el paréntesis de cierre de la última línea. CPython no puede compilar el archivo porque su sintaxis está incompleta. Informa de un error de sintaxis antes de que se ejecute cualquier sentencia de ese archivo, por lo que no se muestra «start». La comprobación de sintaxis del archivo completo y la comprobación durante la ejecución de una operación ocurren en momentos distintos.
Comparte código de bytes y especialízalo durante la ejecución
Java muestra una vía híbrida habitual. javac Hello.java produce Hello.class, que contiene código de bytes. java Hello inicia una máquina virtual de Java (JVM) compatible. Distintas implementaciones de la JVM permiten ejecutar ese código de bytes en distintas familias de procesadores. El código de bytes es portable entre entornos de ejecución compatibles; cada entorno debe funcionar en su propia plataforma anfitriona. De forma similar, C# se traduce a un lenguaje intermedio utilizado por .NET.
Un compilador en tiempo de ejecución (JIT) genera código máquina mientras se ejecuta el programa. El entorno de ejecución HotSpot de Java suele comenzar con interpretación y compila los métodos más utilizados. Los motores de JavaScript de los navegadores también combinan técnicas de ejecución, incluida la compilación JIT. Compilar las partes más utilizadas puede mejorar la velocidad sostenida, pero consume tiempo y memoria durante la ejecución.
| Vía | Ventaja útil | Coste que considerar |
|---|---|---|
| Construcción nativa anticipada | El trabajo se traduce antes del inicio; el usuario no necesita compilador | Versiones compiladas compatibles distintas para las plataformas de destino |
| Interpretación | Respuesta rápida al editar y ejecutar con un intérprete instalado | El trabajo repetido durante la ejecución puede ralentizar el código con muchos cálculos |
| Código de bytes con máquina virtual y JIT | Código intermedio portable con optimización durante la ejecución | Instalación del entorno de ejecución, calentamiento y consumo adicional de memoria |
Un bucle que suma mil millones de valores suele ejecutarse mucho más rápido en C compilado que en CPython: CPython realiza trabajo de interpretación y comprobaciones de tipos en torno a muchas operaciones. Para un programa breve que cambia el nombre de 20 archivos, el esfuerzo de desarrollo puede importar mucho más que esa diferencia. Mide la carga de trabajo antes de usar la etiqueta de un lenguaje como predicción de velocidad.
Compilado e interpretado describen decisiones de implementación. CPython utiliza interpretación de código de bytes; PyPy utiliza técnicas JIT. Un lenguaje puede tener más de una implementación y una implementación puede usar varios métodos de ejecución.
Puntos clave
- Un intérprete ejecuta un programa y debe estar presente para hacerlo.
- CPython compila un archivo a código de bytes antes de ejecutarlo.
- Un error de sintaxis puede impedir que un archivo se inicie; un error de ejecución aparece cuando se ejecuta su operación.
- Un compilador JIT crea código nativo durante la ejecución, a menudo para las partes más utilizadas.
- Los métodos de ejecución pertenecen a las implementaciones, y el rendimiento depende de la carga de trabajo.
2Lee qué hacen los programas breves
Leer código implica dos preguntas: si su forma sigue las reglas del lenguaje y qué significan sus operaciones. Una expresión de aspecto idéntico puede comportarse de forma distinta en dos lenguajes. Aprende a clasificar errores y después sigue las asignaciones, decisiones, bucles y llamadas a funciones en orden de ejecución. Una pequeña tabla de valores cambiantes hace visible la diferencia entre lo que se pretendía programar y lo que realmente hace el programa.
2.1 Sintaxis, significado y tres tipos de errores
La sintaxis establece las reglas para que el texto de un programa sea válido. La semántica da significado a ese texto. Un programa puede seguir todas las reglas gramaticales y aun así calcular un resultado incorrecto, por lo que comprobar su apariencia es solo la primera parte de su lectura.
Reconoce las piezas y la gramática
Los componentes léxicos incluyen palabras clave como if, identificadores como total, literales como 3 o "hello", operadores como + y == y signos de puntuación. Una gramática describe cómo pueden formar expresiones y sentencias estas piezas.
Python requiere dos puntos después de if x > 3: y utiliza la sangría para marcar las sentencias controladas por esa condición. C, Java y JavaScript utilizan llaves para los bloques; C y Java también requieren punto y coma después de muchas sentencias. En esos lenguajes, la sangría ayuda a quien lee, pero no selecciona el bloque.
Una sintaxis distinta puede expresar una acción similar. Estas sentencias muestran un saludo:
print("hello")
printf("hello\n");
System.out.println("hello");
La secuencia de escape de C \n añade un salto de línea. Conocer la idea de mostrar texto ayuda al aprender cómo la expresa cada lenguaje.
Los mismos símbolos pueden significar cosas distintas
| Expresión y lenguaje | Resultado | Regla |
|---|---|---|
7 / 2 en Python o JavaScript | 3.5 | La división produce un resultado fraccionario |
7 / 2 con operandos enteros en C o Java | 3 | La división entera trunca hacia cero |
-7 // 2 en Python | -4 | La división por suelo redondea hacia abajo |
-7 / 2 con enteros en C | -3 | El truncamiento se acerca a cero |
"3" * 4 en Python | "3333" | Se repite una cadena |
"3" * 4 en JavaScript | 12 | El texto se convierte en un número para la multiplicación |
La apariencia familiar de un símbolo no determina su significado en distintos lenguajes.
El orden de evaluación también importa. La precedencia hace que la multiplicación y la división se realicen antes que la suma. Con a = 4 y b = 6, average = a + b / 2 divide primero 6 entre 2 y después suma 4, lo que produce 7,0. Los paréntesis de average = (a + b) / 2 hacen que la suma se realice primero y dan 5,0.
Distingue los errores
Un error de sintaxis incumple la gramática, como la falta de dos puntos en Python. Un error de ejecución ocurre durante una operación ejecutada, como la división por cero en Python. Un error lógico permite la ejecución, pero da una respuesta no deseada, como en el promedio incorrecto.
Algunas expresiones válidas omiten deliberadamente una operación. En Python, x != 0 and 10 / x > 2 utiliza evaluación en cortocircuito. Cuando x es cero, la primera condición es falsa, por lo que el lado derecho no se evalúa y no se realiza ninguna división. Invertir estas condiciones eliminaría esa protección. Python escribe and, or y not; C, Java y JavaScript utilizan &&, || y !.
La asignación y la comparación son otra distinción importante. = almacena un valor; == comprueba la igualdad. C permite if (x = 5): asigna 5 y trata ese valor distinto de cero como verdadero. Python rechaza if x = 5: por ser sintaxis no válida. Por tanto, el mismo error puede detectarse en una etapa distinta según el lenguaje.
Puntos clave
- La sintaxis define la forma válida; la semántica define el significado.
- Los operadores pueden tener significados distintos en distintos lenguajes.
- La precedencia y los paréntesis determinan qué operaciones se agrupan.
- Los errores de sintaxis, de ejecución y lógicos requieren distintos tipos de comprobaciones.
- La evaluación en cortocircuito puede omitir una operación insegura.
- La asignación almacena un valor; la comparación de igualdad comprueba valores.
2.2 Sigue los valores a través del flujo de control y las llamadas
Un programa breve es más fácil de entender si sigues las sentencias ejecutadas una a una. Mantén visibles los valores actuales en lugar de saltar de la primera línea a un resultado supuesto.
Expresiones y asignaciones
Una expresión produce un valor: price * 3, x > 10 y len(name) son ejemplos. Una sentencia realiza una acción, como asignar un valor o controlar un bloque. En total = price * 3, la expresión de la derecha se evalúa antes de asignar su resultado a total.
Una variable es un nombre que hace referencia a un valor. La asignación da al nombre su valor actual; no es una afirmación matemática de que ambos lados siempre hayan sido iguales. Si count contiene 4, count = count + 1 primero lee 4, suma 1 y vincula count a 5.
Sigue la siguiente sentencia que se ejecuta
Tres estructuras organizan el flujo de control:
- La secuencia ejecuta las sentencias en orden.
- La selección utiliza una condición para elegir un camino, como
ifyelse. - La iteración repite un bloque mediante un bucle
forowhile.
El teorema del programa estructurado, asociado a Böhm y Jacopini en 1966, muestra que estas estructuras pueden expresar cualquier algoritmo. Un bucle for suele recorrer los elementos de una colección. Un bucle while se repite mientras su condición siga siendo verdadera.
Considera este bucle de Python:
total = 0
for n in [3, 8, 5]:
total = total + n
La asignación con sangría se ejecuta una vez por cada elemento. Una tabla de seguimiento registra el estado después de cada actualización:
| Momento | n actual | total |
|---|---|---|
| Antes del bucle | Aún no hay elemento | 0 |
| Primera actualización | 3 | 3 |
| Segunda actualización | 8 | 11 |
| Tercera actualización | 5 | 16 |
Cada total nuevo utiliza el anterior. Reiniciar total dentro del bucle produciría un cálculo distinto porque se perderían las sumas anteriores.
Entra en una función y sal de ella
Una función agrupa una tarea bajo un nombre. Un parámetro es un nombre de la definición; un argumento es un valor proporcionado por una llamada:
def area(width, height):
return width * height
result = area(3, 4)
La llamada vincula width a 3 y height a 4. La multiplicación produce 12 y return lo envía al código que realizó la llamada. La ejecución continúa en la asignación, por lo que result recibe 12. Los nombres describen una tarea que puede reutilizarse con otros argumentos.
Un nombre tiene un ámbito
El ámbito determina dónde puede usarse un nombre. Los nombres creados dentro de una función suelen ser locales a esa llamada. Un nombre del nivel superior es global en su módulo. Las llamadas distintas tienen vinculaciones locales independientes aunque utilicen nombres idénticos.
count = 7
def reset():
count = 0
reset()
print(count)
Esto muestra 7. La asignación dentro de reset crea un count local; no sustituye la vinculación global. En su lugar, una función puede devolver un resultado y dejar que el código que la llama lo asigne.
El ámbito y el tiempo de vida responden a preguntas distintas. Un nombre local no está disponible para el código externo solo porque el objeto al que hacía referencia siga existiendo. Los objetos devueltos pueden seguir vivos, y una función anidada devuelta puede conservar el acceso a las variables de la llamada que la contiene; ese entorno conservado forma parte de una clausura.
Puntos clave
- Evalúa la expresión de la derecha antes de actualizar el destino de una asignación.
- Sigue la secuencia, la selección y la iteración en el orden en que realmente se ejecutan.
- Los parámetros son nombres de la definición; los argumentos son valores proporcionados.
- Un valor de retorno devuelve un resultado al código que realizó la llamada.
- Los nombres locales y globales pueden tener vinculaciones independientes; el ámbito difiere del tiempo de vida.
3Tipos, referencias y memoria
Los valores necesitan un tipo, un lugar en la memoria y un tiempo de vida. Estas decisiones explican por qué un texto que parece numérico puede rechazar operaciones aritméticas, por qué dos nombres pueden mostrar la misma lista modificada y por qué a veces los datos sin uso siguen ocupando memoria reservada. Compara las comprobaciones que realizan los lenguajes, sigue una llamada a una función a través de la memoria y examina tres formas de liberar almacenamiento. Cada enfoque cambia el equilibrio entre control, comodidad y seguridad.
3.1 Los tipos deciden qué operaciones tienen sentido
Un tipo de dato especifica los valores posibles y las operaciones que admiten. Un entero admite operaciones aritméticas con números enteros; un valor booleano representa verdadero o falso. Las cadenas, listas, registros y objetos combinan u organizan otros valores. Los tipos también orientan cómo se representan los valores en memoria, aunque el tamaño de un valor puede depender del propio valor y de la implementación.
¿Cuándo se comprueban los tipos?
| Enfoque | Cuándo se realizan las comprobaciones | Ejemplo |
|---|---|---|
| Tipado estático | Antes de la ejecución | Java rechaza int count = "five"; |
| Tipado dinámico | Durante la ejecución | Python comprueba si una operación ejecutada es compatible con sus operandos |
En Python, count = 5 seguido de count = "five" es válido. El nombre ahora hace referencia a una cadena. Evaluar después count + 1 genera TypeError. El tipado dinámico da tipos definidos a los valores; no elimina los tipos.
El tipado estático no exige una etiqueta de tipo visible en cada variable. La inferencia de tipos permite al compilador deducir un tipo a partir del código que lo rodea. Kotlin infiere un entero para val n = 5, y Rust puede inferir un entero para let n = 5;. Las anotaciones de tipo opcionales de Python son distintas: herramientas independientes como mypy pueden comprobarlas, pero la ejecución habitual de Python no las hace cumplir.
¿Cuándo se convierten los valores?
El momento de comprobación y la rigidez de las conversiones son cuestiones distintas. Python rechaza "3" + 4; la conversión explícita en int("3") + 4 da 7. JavaScript aplica distintas reglas de conversión automática según el operador:
"3" + 4da la cadena"34"."3" * 4da el número12."3" - 4da el número-1.
La conversión automática se llama coerción. Las etiquetas de tipado «fuerte» y «débil» son formas informales de hablar de la rigidez de las conversiones, no otros nombres para el tipado estático y dinámico. Python es dinámico y estricto con la suma de tipos distintos del ejemplo anterior; C es estático y aun así permite muchas conversiones numéricas implícitas.
La función input() de Python devuelve una cadena. Si una persona escribe «30», el resultado sigue teniendo el tipo str. age + 1 falla, mientras que int(age) + 1 da 31 cuando age contiene ese texto. Lo que aparece en pantalla no basta para determinar el tipo.
Los números tienen límites de representación
Un entero con signo de 32 bits representa valores desde −2 147 483 648 hasta 2 147 483 647. El tipo int de Java pasa cíclicamente del máximo al mínimo al sumar 1. El desbordamiento con signo en C tiene un comportamiento indefinido, por lo que no debe suponerse ese mismo retorno cíclico en C. Los enteros de Python crecen según lo permita la memoria: 2 ** 100 es exactamente 1 267 650 600 228 229 401 496 703 205 376.
Los números en coma flotante representan la mayoría de las fracciones decimales de forma aproximada en binario. En Python y JavaScript, 0.1 + 0.2 produce 0.30000000000000004. Comparar ese resultado directamente con 0.3 da falso.
Cuando un cálculo admite un pequeño error, compara los valores con una tolerancia elegida para la tarea, por ejemplo, abs(a - b) < 1e-9. Una tolerancia absoluta tan pequeña solo es adecuada para algunas escalas. Para una contabilidad decimal exacta, utiliza números enteros de la unidad mínima o un tipo decimal adecuado. Cambiar la comparación no puede convertir una representación aproximada en exacta.
Puntos clave
- Un tipo define valores y operaciones admitidas.
- El tipado estático y dinámico describen el momento de comprobación, mientras que la coerción se refiere a las reglas de conversión.
- La inferencia de tipos permite el tipado estático; las anotaciones de Python necesitan herramientas de comprobación independientes.
- Los enteros de ancho fijo pueden desbordarse, con un comportamiento específico de cada lenguaje.
- El redondeo en coma flotante afecta a las pruebas de igualdad y a la elección de la representación numérica.
3.2 Sigue nombres, objetos y llamadas a funciones en la memoria
La memoria principal almacena los datos de un programa en ejecución. Cada byte tiene una dirección. Los lenguajes proporcionan nombres para trabajar con valores sin escribir direcciones directamente en cada operación.
Las llamadas usan marcos; los objetos tienen tiempos de vida
En C, un int suele ocupar 4 bytes, y &x obtiene la dirección de x. Los lenguajes de nivel superior suelen ocultar las direcciones mediante referencias. La distinción importante es entre una vinculación que da nombre a un objeto y el propio objeto.
La pila de llamadas lleva el seguimiento de las llamadas activas a funciones. Cada llamada tiene un marco de pila con información como los parámetros, las vinculaciones locales y el punto donde debe continuar la ejecución tras el retorno. Si main llama a area(3, 4), se añade un marco de area sobre el marco del código que llama. La función calcula 12, retorna y se retira su marco. En las llamadas anidadas habituales, la llamada más reciente retorna primero.
La recursión añade una llamada por cada nivel pendiente. Si las llamadas siguen acumulándose, puede agotarse el espacio disponible en la pila. Python normalmente genera RecursionError después de unas 1000 llamadas anidadas, con un límite que depende de su configuración. Para que termine un cálculo recursivo finito, debe alcanzarse una condición de parada.
El montículo de memoria contiene objetos cuya memoria se reserva dinámicamente, como una lista que crece o un objeto que sobrevive a la función que lo creó. La reserva de memoria aparta espacio de almacenamiento. Una lista devuelta puede permanecer en el montículo después de que termine la llamada que la creó.
Copiar una referencia comparte un objeto
Una referencia identifica un objeto. Un puntero en C o C++ representa explícitamente una dirección utilizada para acceder a datos. Copiar una referencia copia el acceso al objeto, no todos sus datos.
a = [1, 2]
b = a
b.append(3)
Después de la primera línea hay una lista. La segunda línea vincula otro nombre a ella. La última línea modifica esa lista, por lo que tanto a como b muestran [1, 2, 3]. Esta forma de compartir es el uso de alias. Usar b = a.copy() crearía una lista externa independiente; en esta lista de enteros, añadir un elemento a la nueva lista dejaría a sin cambios.
Compara la asignación con la mutación:
x = 5
y = x
y = y + 1
El entero 5 es inmutable. La suma produce 6 y solo se vuelve a vincular y. x sigue haciendo referencia a 5. Los números, cadenas y tuplas de Python son inmutables; las listas y los diccionarios son mutables. El contenido fijo de una tupla puede incluir una referencia a una lista mutable, por lo que la inmutabilidad del contenedor externo no inmoviliza todos los objetos anidados.
Aplica las mismas reglas a los argumentos
Cuando Python pasa una lista a una función, el parámetro hace referencia al mismo objeto. Llamar a items.append(4) dentro de la función cambia esa lista. Asignar items = [] vincula solo el nombre del parámetro local a una lista nueva. El nombre del código que llama sigue vinculado al objeto original.
C pasa los valores de los argumentos por valor. Para permitir que una función actualice una variable del código que llama mediante su dirección, pasa un puntero a ella. En cada lenguaje, hazte dos preguntas distintas durante el seguimiento: «¿A qué objeto hace referencia este nombre?» y «¿Esta operación cambia ese objeto o vincula un nombre a otra cosa?».
Puntos clave
- La pila de llamadas lleva el seguimiento de las llamadas pendientes; el montículo contiene objetos cuya memoria se reserva dinámicamente.
- Un objeto puede sobrevivir a la función que lo creó.
- Copiar una referencia comparte un objeto; no duplica sus datos.
- La mutación cambia un objeto, mientras que la revinculación cambia aquello a lo que hace referencia un nombre.
- El paso de argumentos sigue la misma distinción entre objetos compartidos y vinculaciones locales.
3.3 Libera memoria sin perder datos vivos
La memoria reservada debe poder reutilizarse en algún momento. El reto consiste en liberar los datos después de su último uso y conservar todos los objetos que el programa aún necesita. Los lenguajes asignan esta responsabilidad a quien programa, a un sistema de ejecución o a reglas de propiedad de los datos comprobadas por el compilador.
La gestión manual y sus fallos
En C, malloc(n) solicita n bytes y devuelve un puntero al bloque si tiene éxito. free(p) libera un bloque obtenido mediante una reserva. Quien programa debe utilizarlo en el momento adecuado y exactamente una vez. C++ también admite la gestión manual, aunque sus herramientas estándar pueden automatizar la liberación mediante los tiempos de vida de los objetos.
Varios errores tienen causas distintas:
| Fallo | Qué salió mal |
|---|---|
| Fuga de memoria | No se libera almacenamiento reservado que ya no se necesita |
| Uso después de liberar memoria | Se utiliza un puntero después de liberar el almacenamiento al que apunta |
| Doble liberación de memoria | Se libera dos veces el mismo bloque reservado |
| Desbordamiento de búfer | Una escritura supera los límites de un bloque de memoria |
Un puntero que sigue haciendo referencia a almacenamiento liberado es un puntero colgante. Ese almacenamiento podría reutilizarse para otra cosa, por lo que acceder a él puede corromper datos o hacer que el programa deje de funcionar.
En un arreglo de C int scores[10], las posiciones válidas son de 0 a 9. Escribir en scores[10] supera los límites del arreglo, y C no proporciona una comprobación automática de límites. Python genera IndexError cuando se utiliza el índice 10 en una lista de diez elementos, lo que impide que ese acceso sobrescriba memoria adyacente.
Rastrea la accesibilidad o cuenta las referencias
Un recolector de basura por rastreo parte de raíces vivas, como referencias activas, sigue los enlaces entre objetos y puede recuperar la memoria de los objetos inaccesibles. Java, C#, Go y JavaScript utilizan enfoques de rastreo. La recolección requiere trabajo durante la ejecución y memoria; según el recolector, puede introducir pausas.
El conteo de referencias lleva la cuenta de las referencias a un objeto. Para una lista nueva de Python, la secuencia simplificada es: a = [1, 2] proporciona una referencia; b = a proporciona una segunda; del a elimina una; b = None elimina la otra. CPython normalmente recupera la memoria de esta lista habitual cuando su cuenta llega a cero.
Un grupo de objetos puede mantener las cuentas de los demás por encima de cero aunque ninguna parte viva del programa pueda acceder al grupo. Esto es un ciclo de referencias. CPython complementa el conteo de referencias con la recolección de ciclos. Tanto el conteo de referencias como el rastreo son técnicas de gestión automática de memoria.
Comprueba la propiedad antes de la ejecución
Rust da un propietario a cada valor y comprueba las reglas del préstamo de referencias. Cuando termina la propiedad, el código de limpieza generado libera el valor. Rust seguro rechaza los usos de referencias que podrían sobrevivir al objeto válido al que apuntan, sin necesitar un recolector de basura por rastreo. Swift utiliza conteo automático de referencias insertado por el compilador.
La seguridad de memoria importa más allá de los fallos del programa. Microsoft informó en 2019 de que aproximadamente el 70 % de las vulnerabilidades a las que asignaba CVE cada año afectaban a la seguridad de memoria; Chromium informó de una proporción similar de errores graves en 2020. En diciembre de 2023, las agencias estadounidenses CISA, NSA y FBI y socios internacionales instaron a los fabricantes de software a planificar la transición hacia lenguajes con seguridad de memoria.
La recuperación automática sigue conservando los objetos accesibles. Una lista global que no deja de crecer puede mantener vivos todos los registros antiguos incluso después de que la aplicación haya terminado de utilizarlos. Un recolector no puede decidir que estos registros accesibles ya no son necesarios para la tarea; el programa debe dejar de retenerlos.
Puntos clave
- La reserva manual requiere liberar la memoria en el momento adecuado.
- Las fugas, el uso después de liberar memoria, la doble liberación y el desbordamiento de búfer tienen causas distintas.
- El rastreo encuentra objetos inaccesibles; el conteo de referencias responde a la pérdida de referencias.
- CPython añade la recolección de ciclos al conteo de referencias.
- La propiedad de los datos puede organizar la liberación sin un recolector por rastreo.
- La recolección automática no puede recuperar la memoria de los objetos que el programa sigue reteniendo.
4Organiza el estado cambiante y los objetos que colaboran
Una forma de describir un programa es mediante órdenes que actualizan valores. Otra agrupa esos valores con las operaciones que los controlan. Estas perspectivas suelen combinarse: un método de un objeto puede contener un bucle habitual. Sigue primero los cambios de estado y después construye un pequeño modelo de cuentas para ver cómo protegen las clases las reglas y cómo permiten la herencia, el polimorfismo y la composición que objetos relacionados compartan una interfaz sin asumir responsabilidades inadecuadas.
4.1 Órdenes, procedimientos y estado compartido
Un paradigma de programación es una forma de organizar el código y de pensar en la computación. En la programación imperativa, un programa da órdenes que cambian su estado: los valores que contienen actualmente las variables y los objetos.
Haz visibles los cambios de estado
Un total acumulado es un cálculo imperativo sencillo:
total = 0
for n in [3, 8, 5]:
total = total + n
El estado inicial es total = 0. Sumar 3 lo cambia a 3. Sumar 8 lo cambia a 11. Sumar 5 lo cambia a 16. Cada asignación utiliza el estado que dejó el paso anterior, por lo que el orden forma parte del método.
Este estilo se ajusta a los cambios paso a paso que realiza el procesador en los registros y la memoria. También encaja con algoritmos que actualizan deliberadamente una colección en su propia ubicación, como una rutina de ordenación que intercambia elementos.
Da nombres a las tareas
La programación procedimental organiza el código imperativo en funciones o procedimientos. Un programa de informes podría llamar a read_scores(), average(scores) y print_report(avg). Los nombres permiten ver la tarea general sin seguir todas las líneas internas a la vez. Cada procedimiento puede entenderse y probarse por separado.
C, Pascal, Fortran y COBOL son lenguajes procedimentales clásicos. Python también admite este estilo: un programa breve puede utilizar funciones con nombre sin definir ninguna clase.
La programación estructurada organiza el flujo de control con secuencia, selección, bucles y llamadas a procedimientos. Evita los saltos arbitrarios que dificultan predecir la siguiente sentencia que se ejecutará. La carta de Edsger Dijkstra de 1968 «La instrucción Go To se considera perjudicial» ayudó a popularizar este enfoque. Las salidas controladas como break, continue y return siguen siendo útiles: permiten salir de una iteración concreta, de un bucle o de una función.
Los efectos pueden ir más allá de un valor de retorno
Un efecto lateral cambia algo fuera del resultado que devuelve una función. Mostrar información, guardar un archivo, enviar un mensaje por la red y cambiar datos compartidos son efectos laterales. Los programas necesitan estas acciones para interactuar con el mundo.
Los problemas surgen cuando fragmentos de código sin relación cambian silenciosamente el mismo estado. Supón que apply_discount() reduce una variable global price un 10 % y add_shipping() añade 5. Partiendo de 100, aplicar primero el descuento da 90 y después el envío da 95. Añadir primero el envío da 105 y después aplicar el descuento da 94,5. Ambos procedimientos realizan correctamente sus propios pasos, pero el resultado combinado depende de su orden.
Pasar valores y devolver resultados nuevos puede hacer visible ese orden:
price = add_shipping(apply_discount(100))
La llamada interna se ejecuta primero: descuento y después envío. Un anidamiento distinto expresa una regla distinta. Un flujo de datos claro ayuda a revelar si el propio envío debe recibir el descuento.
| Organización | Pregunta principal al leer |
|---|---|
| Órdenes imperativas | ¿Qué valor cambia a continuación? |
| Procedimientos con nombre | ¿Qué tarea realiza esta llamada? |
| Actualizaciones globales compartidas | ¿Qué acción anterior cambió este valor? |
El código imperativo sigue siendo habitual dentro de los métodos de objetos y los manejadores de eventos modernos. Elegir un diseño general orientado a objetos o funcional no impide que una pequeña parte utilice órdenes directas cuando esos pasos aclaran el algoritmo.
Puntos clave
- El código imperativo describe pasos que cambian el estado almacenado.
- Los procedimientos dan nombre y límites a tareas coherentes.
- El flujo de control estructurado facilita seguir los caminos de ejecución.
- Los efectos laterales incluyen cambios en los datos compartidos e interacciones con el mundo exterior.
- Las actualizaciones compartidas pueden hacer que los resultados dependan del orden de las llamadas.
4.2 Las clases mantienen los datos junto con sus reglas
La programación orientada a objetos (POO) modela un programa como objetos que colaboran. Cada objeto combina datos con operaciones sobre esos datos. Una cuenta puede contener su titular y su saldo y ofrecer operaciones de depósito y retirada.
Separa la plantilla de sus instancias
Una clase define los atributos y métodos que proporcionan sus objetos. Un objeto, o instancia, es un miembro concreto de esa clase. BankAccount es la plantilla; la cuenta de Ana con 150 y la de Ben con 40 son instancias distintas. Sus saldos pueden cambiar de forma independiente.
Un atributo almacena datos asociados a un objeto. Un método es una operación que se llama a través de un objeto, como acct.deposit(50). En un método de instancia de Python, self hace referencia al objeto que recibe la llamada.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self._balance = self._balance + amount
Llamar a BankAccount("Ana", 100) construye una instancia nueva. Su método __init__ inicializa los datos de la instancia. Un constructor es el proceso de preparación de un objeto nuevo; Python separa la creación de esta etapa de inicialización. Llamar a acct.deposit(50) sobre esa cuenta cambia su saldo de 100 a 150.
Coloca la regla en el punto de actualización
La encapsulación reúne los datos y las operaciones que mantienen sus reglas. Supón que una cuenta nunca debe tener un saldo negativo. El método de retirada comprueba el cambio propuesto antes de realizarlo:
def withdraw(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
if amount > self._balance:
raise ValueError("insufficient funds")
self._balance = self._balance - amount
Este método pertenece al interior de BankAccount. Partiendo de un saldo de 150, withdraw(200) genera un error y deja 150. withdraw(30) tiene éxito y deja 120. La llamada fallida no resta primero y descubre después el problema; la comprobación ocurre antes de la actualización.
El resto del código debe utilizar esos métodos en lugar de asignar un saldo sin comprobarlo. Java y C# pueden restringir el acceso directo con private. El guion bajo inicial de _balance en Python señala por convención un detalle interno; no impone privacidad.
La ocultación de información va más allá: el código que llama depende de las operaciones que ofrece una clase sin necesitar conocer su representación interna. Más adelante, el saldo podría almacenarse de otra forma mientras las operaciones públicas de depósito y retirada conservan su significado.
Deja que el problema sugiera las responsabilidades
Un sistema de biblioteca tiene libros, miembros y préstamos. Esos sustantivos sugieren posibles clases o atributos; acciones como prestar y renovar sugieren métodos. Elige una responsabilidad clara para cada clase. Una clase de cuenta que además envía todos los correos electrónicos y da formato a todos los informes resulta más difícil de modificar porque mezcla tareas sin relación.
Las clases y los objetos aparecieron en Simula 67 en 1967; Smalltalk ayudó a difundir el enfoque en la década de 1970. Java, C#, C++ y Python admiten POO, pero utilizar uno de esos lenguajes no obliga a organizar todos los programas en clases. La pregunta útil de diseño es qué datos y reglas deben estar juntos.
Puntos clave
- Una clase define atributos y métodos; cada instancia tiene sus propios datos de instancia.
- Los métodos actúan sobre el objeto que recibe la llamada.
- La encapsulación reúne la validación y los cambios de estado.
- Los guiones bajos de Python señalan atributos internos por convención; algunos lenguajes imponen el acceso privado.
- La ocultación de información permite que el código que llama dependa de las operaciones públicas y no de los detalles internos.
- Una clase debe tener una responsabilidad coherente.
4.3 Especializa el comportamiento y combina objetos
Los objetos suelen necesitar compartir operaciones y diferir en los detalles. La herencia, el polimorfismo y la composición resuelven partes distintas de ese problema de diseño.
Especializa un tipo de objeto existente
La herencia define una subclase a partir de una superclase. En Python, class SavingsAccount(BankAccount): da a la clase especializada acceso al comportamiento heredado de la cuenta. Puede añadir add_interest(rate): un saldo de 1000 al que se añade un interés del 2 % pasa a ser 1020.
La relación prevista es es un. Una cuenta de ahorro sigue siendo una cuenta bancaria y debe respetar las reglas que se esperan de las cuentas bancarias. Reutilizar unas pocas líneas no es motivo suficiente para afirmar que un tipo de objeto es otro.
Una subclase puede redefinir un método con su propia implementación. Supón que una cuenta corriente cobra una comisión de 1 por cada retirada realizada con éxito. Con un saldo de 100, retirar 30 cuesta 31 y deja 69. Para conservar la regla de la cuenta, el método debe comprobar la retirada más la comisión antes de cambiar el saldo. Si ese coste combinado supera el saldo, rechaza toda la operación.
Haz que una llamada funcione con distintos objetos
El polimorfismo permite que la misma operación funcione con distintos tipos de objetos. Tanto un círculo como un rectángulo pueden proporcionar area():
for shape in shapes:
print(shape.area())
El bucle no necesita un if para cada clase de figura. Cada objeto aporta su propia implementación. Seleccionar esa implementación durante la ejecución es el despacho dinámico.
Para Circle(2), el área es aproximadamente 12,57 unidades cuadradas. Para Rectangle(3, 4), es de 12 unidades cuadradas. Su área combinada es aproximadamente 24,57 unidades cuadradas. Un Triangle posterior puede incorporarse a la colección si proporciona una operación area() compatible; el propio bucle no necesita otra rama.
Una interfaz establece las operaciones que debe ofrecer un tipo. La declaración de Java interface Shape { double area(); } exige que las clases que la implementan proporcionen el método de área. Una clase abstracta puede aportar parte de la implementación compartida y dejar otros métodos para las subclases. Python suele utilizar el tipado de pato: un objeto puede funcionar en el bucle si proporciona la operación requerida, incluso sin una superclase compartida.
Contén una pieza en lugar de convertirte en ella
La composición almacena otros objetos como componentes. Un automóvil tiene un motor y ruedas. Puede llamar al método start() de su motor sin heredar de Engine.
| Relación | Diseño adecuado | Motivo |
|---|---|---|
| Una cuenta de ahorro es una cuenta | Herencia | Conserva el comportamiento esperado de una cuenta |
| Un automóvil tiene un motor | Composición | El motor es un componente, no la categoría del automóvil |
| Distintas figuras ofrecen un área | Operación o interfaz compartida | El código que llama puede solicitar la operación de forma uniforme |
Las cadenas profundas de herencia pueden propagar los efectos de un cambio en una superclase a muchas clases. Una subclase que debe desactivar comportamiento heredado puede indicar una relación «es un» inadecuada. La composición suele mantener estas dependencias más reducidas y claras.
Los cuatro pilares habituales de la POO encajan aquí: la encapsulación protege los datos mediante operaciones, la abstracción oculta los detalles de implementación, la herencia especializa un tipo y el polimorfismo permite que el código que llama utilice una operación compartida entre distintas implementaciones.
Puntos clave
- La herencia modela una relación «es un» y debe conservar el comportamiento esperado.
- La redefinición sustituye la implementación de un método en una clase especializada.
- El polimorfismo permite que una operación invoque distintas implementaciones.
- Una interfaz establece las operaciones requeridas; el tipado de pato se basa en las operaciones que proporciona un objeto.
- La composición modela objetos que tienen otros objetos como componentes.
5Calcula valores nuevos y describe los resultados deseados
Un programa puede centrarse en transformar valores o en el resultado que debe producir un motor. Las funciones puras hacen explícitas las dependencias, mientras que los datos inmutables evitan cambios mediante referencias compartidas. Las consultas declarativas permiten que un motor especializado elija los pasos de ejecución. Sigue una cadena de procesamiento de datos y una consulta de base de datos para ver qué facilitan estos enfoques y dónde siguen interviniendo la entrada, la salida y las instrucciones máquina concretas.
5.1 Funciones puras y cadenas de procesamiento de datos
La programación funcional construye resultados combinando funciones. Su práctica central es producir valores nuevos a partir de las entradas y limitar la mutación compartida. Lisp introdujo ideas funcionales de uso extendido hacia 1958; Haskell, Erlang, Clojure, F# y OCaml desarrollan esas ideas de distintas formas.
Reconoce qué hace pura a una función
Una función pura da el mismo resultado para los mismos argumentos y no tiene efectos laterales. def with_tax(price): return price * 1.2 ilustra la idea: with_tax(50) da 60,0. Una función que además añade un elemento a una lista global o muestra un mensaje tiene un efecto más allá de su valor de retorno. Un resultado que depende de la hora actual también depende de algo más que de los argumentos.
Una llamada pura puede sustituirse por su valor de retorno sin cambiar el comportamiento del programa. Esta propiedad es la transparencia referencial. Facilita las pruebas aisladas y permite ejecutar llamadas independientes en paralelo sin competir por actualizar el estado compartido.
La inmutabilidad mantiene sin cambios un valor existente. [n + 1 for n in nums] crea una lista nueva y deja nums intacta. Un programa puede seguir representando información cambiante produciendo versiones nuevas; no necesita sobrescribir la versión anterior.
Pasa comportamiento como datos
Las funciones son de primera clase cuando pueden asignarse a nombres, pasarse como argumentos y devolverse como valores. Una función de orden superior recibe una función o devuelve una. Una expresión lambda es una expresión de función sin nombre, como lambda n: n * n.
Tres operaciones habituales de orden superior forman una cadena de procesamiento:
filterconserva los elementos que cumplen una prueba.maptransforma cada elemento restante.reducecombina los elementos en un resultado mediante un acumulador.
Para sumar los cuadrados de los números pares del 1 al 6, el filtrado deja 2, 4 y 6; elevarlos al cuadrado da 4, 16 y 36; sumarlos da 56.
sum(map(lambda n: n * n,
filter(lambda n: n % 2 == 0, range(1, 7))))
Aquí % 2 == 0 comprueba si el número es par, y range(1, 7) proporciona del 1 al 6. Haskell expresa el mismo cálculo como sum (map (^2) (filter even [1..6])).
Con reduce importado de functools de Python, reduce(lambda acc, n: acc + n, [3, 8, 5], 0) combina 0 con 3 para obtener 3, después con 8 para obtener 11 y después con 5 para obtener 16. La cadena de procesamiento describe transformaciones sin un bucle externo que actualice un total compartido.
Conserva un entorno o llama a un problema más pequeño
def multiplier(k): return lambda x: k * x devuelve una función que conserva el acceso a k. Así, multiplier(3)(10) devuelve 30 incluso después de que termine la llamada externa. La función devuelta y el entorno conservado forman una clausura.
La recursión hace que una función se llame a sí misma con un problema más pequeño. El factorial devuelve 1 cuando n == 0; en caso contrario, devuelve n * factorial(n - 1). Para 5, los productos dan 120. Un cálculo finito necesita un caso base alcanzable. Las llamadas anidadas pendientes de Python acaban generando RecursionError; los lenguajes con soporte adecuado de llamadas de cola pueden reutilizar el espacio de la pila para las llamadas en posición final.
Usar funciones por sí solo no hace funcional al código: las funciones procedimentales pueden modificar el estado. Los programas funcionales reales también leen entradas y escriben salidas, y suelen mantener los efectos cerca de los límites del programa y los cálculos centrales puros.
Puntos clave
- Las funciones puras dependen solo de sus argumentos y no tienen efectos laterales.
- Las actualizaciones inmutables crean valores nuevos.
- Las funciones de primera clase pueden pasarse y devolverse; las de orden superior reciben o devuelven funciones.
- El filtrado selecciona, la aplicación de una función transforma y la reducción combina.
- Una clausura conserva el entorno en el que se creó.
- Un cálculo recursivo finito necesita un caso base que realmente alcance.
5.2 Expresa un resultado y deja que un motor lo encuentre
La programación declarativa describe el resultado deseado y deja los pasos de ejecución al motor de un lenguaje. El código funcional suele agruparse bajo esta idea amplia, junto con los lenguajes de consulta y la programación lógica.
Lee una consulta como una solicitud
Una base de datos relacional almacena datos en tablas. Supón que students tiene las filas de nombres y calificaciones Ana 92, Ben 75, Chen 80 y Dara 64:
SELECT name
FROM students
WHERE grade >= 80
ORDER BY name;
FROM indica la tabla, WHERE da la condición, SELECT elige el campo que se devuelve y ORDER BY solicita el orden alfabético. Ana y Chen cumplen la condición, incluida la calificación de Chen de exactamente 80. El resultado son sus nombres en ese orden.
Un programa imperativo crearía una lista de resultados, recorrería las filas, comprobaría cada calificación, añadiría los nombres que cumplen la condición y ordenaría la lista. SQL da a la base de datos el resultado requerido sin escribir ese bucle.
Un optimizador de consultas elige un plan de ejecución. Podría recorrer la tabla o utilizar un índice disponible, una estructura de búsqueda que puede acelerar las búsquedas adecuadas. Muchos índices utilizan estructuras ordenadas; otros utilizan disposiciones distintas. Añadir un índice puede cambiar el plan sin cambiar la consulta. Más datos siguen suponiendo trabajo: declarar el resultado no garantiza que todas las consultas tengan poco coste.
Pregunta qué se deduce de los hechos y las reglas
La programación lógica describe hechos y relaciones y después busca valores que satisfagan una pregunta. En Prolog:
parent(tom, bob).
parent(bob, ann).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
La regla significa que X es abuelo o abuela de Z cuando existe un Y para el que se cumplen ambas relaciones de progenitor. La consulta ?- grandparent(tom, Who). encuentra Who = ann: Tom es progenitor de Bob y Bob es progenitor de Ann. Los nombres con mayúscula inicial representan variables; los nombres en minúscula identifican a personas concretas.
Reconoce las herramientas declarativas a tu alrededor
Una fórmula de hoja de cálculo =SUM(B2:B5) establece una dependencia. La hoja de cálculo la recalcula cuando cambian las celdas pertinentes. La expresión regular [0-9]{3}-[0-9]{4} describe tres dígitos, un guion y cuatro dígitos, como en «555-0199». Un motor de coincidencia de texto realiza la búsqueda.
CSS establece la apariencia con una regla como h1 { color: navy; }; HTML marca la estructura de la página. Son notaciones declarativas, normalmente clasificadas como lenguajes de estilos y de marcado. Su función difiere de la de un lenguaje de propósito general capaz de expresar una amplia variedad de cálculos.
| Tipo de lenguaje | Enfoque de la tarea | Ejemplos |
|---|---|---|
| Lenguaje específico de dominio | Un tipo concreto de trabajo | SQL, expresiones regulares, CSS |
| Lenguaje de propósito general | Muchos tipos de programas | Python, Java, Haskell |
El código declarativo puede mantenerse próximo al enunciado de un problema y permitir que un motor mejore su método. También da a quien programa menos control directo sobre los pasos individuales, lo que puede dificultar el diagnóstico de un plan lento. Por debajo siguen ejecutándose instrucciones concretas: «declarativo» describe cómo expresa la tarea quien programa, no un procesador que funciona sin pasos.
Puntos clave
- El código declarativo expresa un resultado deseado; un motor elige los pasos de ejecución.
- SQL selecciona filas y campos mediante condiciones y un orden solicitado.
- Un optimizador puede cambiar el plan de una consulta sin cambiar la consulta.
- La programación lógica deduce respuestas a partir de hechos y reglas.
- Los lenguajes específicos de dominio se dirigen a tareas concretas; los de propósito general admiten muchos tipos de programas.
6Elige un estilo y un lenguaje para la tarea
Los distintos estilos de programación pueden producir la misma respuesta organizando el trabajo de formas distintas. Compararlos en una misma tarea facilita ver sus ventajas e inconvenientes. La elección del lenguaje añade restricciones prácticas: la plataforma de destino, las bibliotecas disponibles, los límites de memoria y velocidad, las necesidades de seguridad y las personas que mantienen el código. Un proyecto con varias partes puede utilizar un lenguaje distinto y una combinación de estilos diferente para cada parte.
6.1 Una tarea con varios estilos de programación
Un paradigma cambia cómo se expresa una solución, no la respuesta numérica que debe producir una solución correcta. Considera pedidos con importes [120, 45, 300, 80]: calcula el total de los importes estrictamente mayores que 100.
Sigue los cambios de estado
La versión imperativa hace explícitos el bucle y el acumulador:
total = 0
for amount in amounts:
if amount > 100:
total = total + amount
El primer importe cambia total a 120. El segundo lo deja sin cambios. El tercero lo cambia a 420 y el cuarto lo deja ahí. El programa hace visibles la prueba y el orden de actualización.
Transforma una colección
Una expresión compacta de Python con estilo funcional es sum(a for a in amounts if a > 100). Selecciona 120 y 300 y los combina en 420. Otra versión es sum(filter(lambda a: a > 100, amounts)). Aquí el predicado describe qué elementos se incluyen y sum combina los valores seleccionados.
Una expresión generadora o una llamada a filter utiliza las características multiparadigma de Python; su presencia no hace puras todas las partes de la aplicación que la rodean.
Coloca las reglas junto con los objetos
En un diseño orientado a objetos, un Order almacena un amount y ofrece is_large(), que devuelve self.amount > 100. Un OrderBook contiene objetos de pedido y ofrece large_total(). Ese método puede recorrer los pedidos, preguntar a cada uno si cumple la condición y sumar los importes que la cumplen.
El total sigue siendo 420. La diferencia está en dónde se sitúan las responsabilidades: la regla para un pedido grande pertenece a Order, y el trabajo sobre la colección pertenece a OrderBook. Otras funcionalidades pueden reutilizar la regla sin copiar la comprobación del umbral.
Pídeselo a la base de datos
Si los datos ya están en una base de datos relacional, SQL puede solicitar el total:
SELECT SUM(amount)
FROM orders
WHERE amount > 100;
La base de datos elige el plan de recuperación y agregación. La consulta no indica al código que llama que recorra todas las filas ni que mantenga un acumulador local.
| Estilo | Qué destaca el ejemplo |
|---|---|
| Imperativo | Pruebas ordenadas y actualizaciones de un total |
| Orientado a objetos | Objetos de pedidos y de colección con responsabilidades definidas |
| Funcional | Selección y combinación de valores |
| SQL declarativo | El resultado requerido sobre los datos almacenados |
Combina estilos donde ayuden
Python admite técnicas procedimentales, orientadas a objetos y funcionales. JavaScript proporciona objetos, funciones de primera clase y clausuras. C++, C#, Kotlin, Scala, Swift y Rust también mezclan estilos. Java está muy asociado a la POO, pero admite expresiones lambda desde Java 8 en 2014; Haskell es puramente funcional.
Una aplicación web puede utilizar clases para usuarios y pedidos, transformaciones funcionales para los datos de informes, actualizaciones imperativas dentro de métodos y SQL para consultas a la base de datos. Estas decisiones actúan en capas distintas.
La programación dirigida por eventos añade otra idea de organización: registrar un manejador que se ejecuta cuando llega un evento. En JavaScript, button.addEventListener("click", showMenu) pasa showMenu como valor de función. Un clic posterior la invoca. El propio manejador puede utilizar objetos, transformaciones puras o pasos imperativos. Su función dirigida por eventos indica cuándo se ejecuta, no el único estilo que puede utilizar su cuerpo.
Puntos clave
- Las soluciones correctas en distintos paradigmas pueden calcular el mismo resultado.
- Un estilo cambia dónde aparecen los pasos, el estado, las reglas y las responsabilidades.
- Los lenguajes modernos suelen admitir varios paradigmas.
- Un manejador de eventos se ejecuta en respuesta a un evento y puede utilizar otros estilos internamente.
6.2 Elige según las restricciones y los compromisos entre objetivos
Ningún lenguaje de programación es el más adecuado para todos los proyectos. Los lenguajes de propósito general suelen ser completos en el sentido de Turing: con tiempo y memoria ilimitados, pueden expresar la misma clase de cálculos. Las decisiones reales dependen de si una solución se adapta a la plataforma, los recursos, las herramientas y las personas disponibles.
Empieza por lo que debe funcionar
La plataforma suele ser una restricción ineludible. Los navegadores ejecutan JavaScript y pueden ejecutar módulos compilados mediante WebAssembly. TypeScript añade comprobaciones estáticas y se compila a JavaScript. Swift es una elección habitual para aplicaciones de las plataformas de Apple; Kotlin se recomienda para Android. SQL es el lenguaje habitual para las consultas relacionales.
Después considera el dominio y el ecosistema. Python tiene bibliotecas de datos y aprendizaje automático de uso extendido, como NumPy y pandas. R se centra en la estadística. C y C++ son habituales en sistemas operativos, motores de juegos y código para sistemas embebidos. Java y C# son habituales en grandes sistemas empresariales; Bash es útil para automatizar tareas del sistema operativo.
Son asociaciones prácticas, no territorios exclusivos. Una biblioteca adecuada puede ahorrar mucho más trabajo de desarrollo que una pequeña diferencia en la sintaxis del lenguaje.
Examina las necesidades de recursos y seguridad
Un microcontrolador con solo unos pocos kilobytes de RAM puede necesitar un control preciso de la memoria y de la sobrecarga durante la ejecución. C, C++ o Rust pueden ser adecuados cuando el sistema de destino los admite. Los componentes exigentes de juegos o bases de datos también suelen favorecer esos lenguajes, pero los motores y los recursos disponibles difieren: Unity utiliza C# y algunas bases de datos utilizan Java o Go.
Antes de elegir por velocidad, encuentra el cuello de botella: la parte que limita el rendimiento. Un programa que espera principalmente una respuesta de red no será mucho más rápido solo porque un pequeño bucle local se compile de forma más eficiente. Mide trabajo representativo en la plataforma de destino.
La seguridad de memoria reduce errores como el acceso no válido a memoria. Rust seguro comprueba las reglas de propiedad durante la compilación; los lenguajes con gestión automática, como Java, C#, Go y Python, proporcionan gestión de memoria durante la ejecución. El tipado estático detecta muchas operaciones incompatibles antes de la ejecución, lo que puede ayudar en una base de código grande. Ni la seguridad de memoria ni el tipado estático demuestran que la lógica del programa sea correcta.
Incluye el coste de mantener el resultado
Los conocimientos del equipo, el personal de desarrollo disponible, los depuradores, las herramientas de pruebas, las bibliotecas, las licencias y el código existente afectan a la decisión. Un lenguaje conocido y con buen soporte puede reducir los costes de entrega y mantenimiento. Reescribir un sistema que funciona genera trabajo de migración y riesgos aunque el nuevo lenguaje tenga características atractivas.
En una red meteorológica urbana, los distintos componentes tienen necesidades distintas:
| Componente | Elección razonable de lenguaje | Motivo |
|---|---|---|
| Firmware de sensores pequeños | C o Rust | Control de memoria y compatibilidad con el hardware de destino |
| Lecturas almacenadas | SQL | Filtrado y agregación en la base de datos |
| Limpieza y análisis | Python | Bibliotecas de procesamiento de datos |
| Visualización interactiva en el navegador | JavaScript o TypeScript | Compatibilidad con el navegador y herramientas para interfaces |
Una justificación útil vincula cada elección con un requisito: «El equipo ya mantiene el servicio de Java y sus bibliotecas admiten la nueva funcionalidad». Una posición en una clasificación de popularidad no puede aportar por sí sola ese razonamiento. Las encuestas pueden indicar una comunidad grande o una amplia oferta de personal; no establecen el mejor lenguaje para una carga de trabajo concreta.
Puntos clave
- Comprueba las restricciones de la plataforma antes que las preferencias.
- Las bibliotecas y las herramientas conectan un lenguaje con su dominio de problemas.
- Mide el cuello de botella antes de elegir principalmente por velocidad.
- La seguridad de memoria y las comprobaciones estáticas reducen algunos fallos, pero la lógica sigue necesitando pruebas.
- Las capacidades del equipo, el código existente y los costes de mantenimiento forman parte de la decisión.
- Distintos componentes pueden justificar distintos lenguajes.
Ejemplos de Lenguajes y paradigmas de programación
1Sigue un programa desde el código fuente hasta la ejecución
13 pasos
- 1IntroducciónUn cálculo atraviesa varias capas
Escribes un cálculo, pero un procesador necesita pequeñas instrucciones codificadas llamadas código máquina. Un traductor aporta los detalles que faltan.
Sigue
x = a + bdesde el código fuente hacia los niveles inferiores y después sigue el programa a través de su construcción y ejecución. Separa dos preguntas: ¿en qué forma está el código y qué software lo está ejecutando? Un compilador, un intérprete y un compilador JIT responden de forma distinta a la segunda. Esa distinción explicará tanto la portabilidad como el momento en que aparecen los errores. - 2EjemploUn cálculo se convierte en varias acciones
Para
x = a + b, imagina cuatro acciones de nivel inferior:- Cargar el valor de
aen un registro. - Cargar el valor de
ben otro registro. - Sumar los valores de los dos registros.
- Guardar el resultado para
x.
La sentencia de alto nivel oculta la selección de registros y los detalles de almacenamiento. Las instrucciones reales generadas pueden diferir porque un compilador puede optimizar el trabajo. El cálculo requerido sigue siendo el mismo.
- Cargar el valor de
- 3ExplicaciónLas instrucciones legibles siguen dirigidas a un procesador
El lenguaje ensamblador utiliza nombres de instrucciones como
movyadd. Un ensamblador las codifica como instrucciones máquina.En x86-64,
mov eax, 1coloca 1 en EAX;add eax, ebxle suma EBX. Esos registros y codificaciones pertenecen a esa arquitectura del conjunto de instrucciones. Sustituir bytes por nombres legibles ayuda a una persona, pero no convierte las instrucciones en una descripción portable de alto nivel. - 4DesafíoTraslada el programa a otro procesador
Un equipo tiene código fuente portable en C y un ejecutable nativo x86-64. Necesita un ejecutable nativo ARM. ¿Qué archivo puede utilizar como punto de partida y qué trabajo hace falta?
---reveal---
Utilizar el código fuente y compilarlo para el destino ARM, con el soporte de plataforma requerido. El ejecutable x86-64 contiene instrucciones para una ISA distinta. La portabilidad del código fuente permite otra traducción; no convierte esas instrucciones máquina existentes en código ARM nativo.
- 5EjemploIdentifica los componentes léxicos antes que la estructura
Considera
total = price * 3;en C.El análisis léxico encuentra seis componentes:
total,=,price,*,3y;. El análisis sintáctico agrupa despuésprice * 3como la expresión cuyo resultado se asigna atotal.Son tareas distintas. El análisis léxico identifica las piezas; el sintáctico establece cómo encajan. El análisis semántico puede comprobar después cuestiones como si los nombres están declarados y los tipos de los operandos son compatibles.
- 6EjemploTraslada los cálculos constantes a la construcción
La expresión fuente
60 * 60 * 24utiliza solo constantes. El plegado de constantes puede evaluarla antes de la ejecución: primero 3600 y después 86 400.La generación de código puede utilizar ese resultado en lugar de instrucciones que repitan las multiplicaciones. Es una optimización porque cambia cómo se realiza el trabajo y conserva el resultado requerido. Sustituir un valor por una aproximación meramente conveniente no estaría justificado por esta regla.
- 7ExplicaciónConecta los archivos traducidos con las rutinas de las bibliotecas
La compilación puede producir archivos objeto independientes. Un enlazador conecta esos archivos y resuelve las referencias a las operaciones de las bibliotecas.
El enlace estático incluye el código de las bibliotecas en el ejecutable construido. El enlace dinámico depende de una biblioteca compartida compatible que se carga después. Ninguno de los dos enfoques exige que la computadora del usuario tenga el compilador anticipado, pero un ejecutable con enlace dinámico sigue necesitando sus dependencias de ejecución.
- 8DesafíoUna construcción correcta da una respuesta incorrecta
Un programa de C se construye correctamente. Su informe debería restar una cantidad utilizada de una cantidad inicial, pero el código las suma. ¿Ha dejado el compilador de comprobar alguna regla del lenguaje?
---reveal---
No. La suma puede tener una sintaxis válida y tipos compatibles y, aun así, expresar una tarea incorrecta. Esto es un error lógico. El compilador comprueba las reglas del lenguaje y genera código; hace falta una prueba con un resultado esperado conocido para revelar la diferencia entre el cálculo y su finalidad prevista.
- 9ExplicaciónUn entorno de ejecución puede ejecutar instrucciones intermedias
CPython compila un archivo de Python válido a código de bytes y después ejecuta ese código de bytes en su máquina virtual. El procesador ejecuta las instrucciones máquina del entorno de ejecución mientras este realiza las operaciones de Python.
Java también utiliza código de bytes:
javacproduce archivos de clase para una JVM compatible. El código de bytes pertenece al modelo de instrucciones de un entorno de ejecución, por lo que no debe confundirse con un conjunto universal de instrucciones de procesador. - 10EjemploCompara dos momentos en los que puede detenerse la ejecución
En este archivo de Python válido, la primera línea se ejecuta:
print("start") x = 10 print(x + "5")Muestra «start» antes de que la última línea genere
TypeError. Ahora elimina el último paréntesis de cierre: CPython no puede compilar el archivo, por lo que no se ejecuta ninguna sentencia de ese archivo. Un fallo de sintaxis durante la preparación y un fallo de tipos durante una operación ejecutada detienen el programa en etapas distintas. - 11ExplicaciónCompila el código más utilizado durante la ejecución
Un compilador JIT produce código máquina mientras se ejecuta el programa. Un entorno de ejecución puede interpretar primero, observar los métodos utilizados con frecuencia y compilarlos.
Esto desplaza los costes: la compilación utiliza tiempo y memoria durante la ejecución, mientras que la ejecución repetida puede ser más rápida después. Un servicio de larga duración puede beneficiarse más de ese calentamiento que un programa diminuto de un solo uso. El resultado depende de la implementación y de la carga de trabajo.
- 12DesafíoSepara la portabilidad del código de bytes de la velocidad nativa
Un archivo de clase de Java se ejecuta mediante una JVM compatible en dos ISA distintas. Durante cada ejecución, el entorno compila un método utilizado con frecuencia. ¿Por qué puede compartirse un archivo de clase mientras el código máquina generado es distinto?
---reveal---
El archivo de clase contiene código de bytes de la JVM, que ambos entornos de ejecución compatibles entienden. Cada JIT se dirige a su propio procesador anfitrión. La portabilidad procede del formato intermedio compartido y de los entornos de ejecución compatibles; las instrucciones máquina producidas durante la ejecución siguen siendo específicas de cada plataforma.
- 13RepasoDistingue los niveles de las vías de ejecución
El código máquina se dirige a una ISA; el lenguaje ensamblador da nombres legibles a sus instrucciones; el código fuente de alto nivel oculta más detalles del hardware.
Un compilador traduce código, un intérprete lo ejecuta y un JIT compila durante la ejecución. Un entorno de ejecución puede combinar estos métodos. Para explicar un sistema real, identifica sus formas fuente, intermedia y nativa y después indica qué etapa comprueba la sintaxis y qué etapa ejecuta la operación que puede fallar.
2Sigue el significado a través de un programa breve
10 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónLa forma y el significado responden a preguntas distintas
- 2EjemploUn código válido puede agrupar mal un cálculo
- 3ExplicaciónLa primera condición puede proteger la segunda
- 4DesafíoPredice si se ejecuta la división
- 5ExplicaciónUna asignación lee antes de actualizar
- 6EjemploSigue cada actualización del acumulador
- 7EjemploSigue una llamada y su valor de retorno
- 8DesafíoUna actualización local puede dejar sin cambios el código que llama
- 9DesafíoSigue juntos la gramática y el estado
- 10RepasoLee el camino ejecutado
3Sigue los tipos y los objetos compartidos
14 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónSigue los valores, los objetos y el almacenamiento
- 2ExplicaciónLos valores con tipo pueden pasar de un nombre a otro
- 3Idea claveLa inferencia y la conversión responden a preguntas distintas
- 4DesafíoInterpreta los dígitos según el valor realmente devuelto
- 5ExplicaciónLa representación afecta a los resultados numéricos
- 6ExplicaciónUn marco de llamada puede terminar antes que un objeto
- 7EjemploDos nombres apuntan a una lista
- 8DesafíoUn parámetro puede modificar un objeto y después apuntar a otro
- 9ExplicaciónIdentifica exactamente cómo se utiliza mal el almacenamiento
- 10EjemploSigue las referencias hasta que desaparezca la última
- 11Idea claveUn ciclo puede no tener ningún punto de entrada vivo
- 12ExplicaciónLa propiedad de los datos comprueba el acceso antes de la ejecución
- 13DesafíoDescubre por qué la recolección no puede recuperar la memoria de una lista
- 14RepasoSigue tres decisiones distintas
4Protege las reglas y elige las relaciones entre objetos
12 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónLos pasos necesitan límites fiables
- 2EjemploEl orden de las operaciones cambia el estado compartido
- 3ExplicaciónHaz útil el límite de un procedimiento
- 4DesafíoPredice una actualización con otro orden
- 5ExplicaciónDa a cada cuenta su propio estado
- 6EjemploValida antes de cambiar un saldo
- 7DesafíoLocaliza una regla eludida
- 8EjemploEspecializa sin incumplir el contrato
- 9ExplicaciónDeja que el objeto elija su método de área
- 10ConexiónContén el componente que realiza el trabajo
- 11DesafíoElige dos relaciones en un diseño nuevo
- 12RepasoAjusta los límites a las responsabilidades
5Transforma una colección y consulta un resultado
13 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónExpresa una transformación o una solicitud
- 2ExplicaciónLa pureza describe algo más que el número devuelto
- 3EjemploSelecciona primero los valores pares
- 4EjemploTransforma y combina los valores seleccionados
- 5DesafíoAplica la cadena de procesamiento a un caso nuevo
- 6ExplicaciónDevuelve un comportamiento que recuerde su factor
- 7EjemploReduce el problema hasta que se aplique un caso directo
- 8EjemploLee SQL como una solicitud sobre filas
- 9Idea claveMantén el resultado y cambia el plan
- 10EjemploConecta dos hechos mediante una regla
- 11DesafíoLee una consulta nueva e incluye su valor límite
- 12DesafíoCombina una consulta con una transformación pura
- 13RepasoMantén visible la relación entre la entrada y el resultado
6Compara estilos y justifica la elección de un lenguaje
10 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónMantén fija la tarea
- 2EjemploMantén fija la tarea y cambia el estilo
- 3EjemploSitúa la misma regla en objetos o en una consulta
- 4DesafíoDecide si un diseño tiene un único estilo exclusivo
- 5ExplicaciónDescarta primero los destinos sin soporte
- 6EjemploElige para un sistema meteorológico completo
- 7ConsejoMide qué consume el tiempo
- 8DesafíoDefiende una elección con restricciones en conflicto
- 9DesafíoConecta la elección del lenguaje con la organización del programa
- 10RepasoDa una razón vinculada al requisito
Ejercicios de Lenguajes y paradigmas de programación
Set 1: Lee código e identifica la vía de ejecución10 Preguntas de Práctica
Pregunta 1 de 10
mov y jmp. ¿En qué nivel de lenguaje está escrito ahora el programa?Mostrar respuestaOcultar respuesta
Pregunta 2 de 10
Mostrar respuestaOcultar respuesta
Pregunta 3 de 10
Mostrar respuestaOcultar respuesta
Pregunta 4 de 10
Mostrar respuestaOcultar respuesta
Pregunta 5 de 10
¿Qué valor final almacena este código de Python en total?
total = 1
for n in [4, 7, 2]:
total = total + n
Mostrar respuestaOcultar respuesta
Pregunta 6 de 10
label = "18" va seguido de label = 18. El nombre puede cambiar de tipo, pero "18" + 2 genera un error. ¿Cómo se describen estos dos hechos?Mostrar respuestaOcultar respuesta
Pregunta 7 de 10
¿Qué contiene first después de ejecutar este código de Python?
first = [4, 8]
second = first
second.append(6)
Mostrar respuestaOcultar respuesta
second modifica esa lista compartida, por lo que first también muestra el 6 añadido. Mantener [4, 8] requeriría una lista independiente, como la creada mediante una copia antes de añadir el elemento.Pregunta 8 de 10
read_data(), calculate_total() y save_report(), cada uno con órdenes secuenciales. ¿Qué organización ilustra?Mostrar respuestaOcultar respuesta
Pregunta 9 de 10
n == 0; en caso contrario, se llama a sí misma con n + 1. Partiendo del valor positivo 2, ¿por qué el caso base escrito no hace que termine el cálculo?Mostrar respuestaOcultar respuesta
Pregunta 10 de 10
Mostrar respuestaOcultar respuesta
Set 2: Aplica las reglas de tipos, objetos y consultas10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
a = 8 y b = 12 en Python, ¿qué valores producen a + b / 2 y (a + b) / 2, respectivamente?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
balance y un método deposit. Se crean dos cuentas con saldos iniciales distintos. ¿Dónde corresponden esos saldos distintos?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
[1, 4, 7, 10], una cadena de procesamiento conserva los valores mayores que 3, resta 1 a cada valor conservado y después los suma. ¿Cuál es el resultado?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
¿Cuáles son los valores de x e y después de este código de Python?
x = [12]
y = x
x = x + [3]
y.append(6)
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Set 3: Sigue las llamadas, los efectos y los contratos de los objetos10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
minutes = 60 * 12 por el valor precalculado 720. ¿Qué optimización se utiliza y qué debe conservar?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
¿Qué muestra este programa de Python?
score = 11
def change():
score = 4
return score + 2
print(change())
print(score)
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
¿Cuál es el valor de items después de la llamada?
items = [3]
def revise(values):
values.append(8)
values = [1]
values.append(5)
revise(items)
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
showDetails para un evento de clic. ¿Por qué puede ese mismo manejador seguir utilizando métodos de objetos y asignaciones imperativas?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Set 4: Razona entre los límites de la ejecución y el diseño10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
n = 0 en Python, compara n != 0 and 36 / n > 4 con 36 / n > 4 and n != 0. ¿Qué ocurre?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
¿Qué valor final se almacena en total?
def bump(x):
return x + 2
total = 0
for n in [2, 5, 8]:
if n > 3:
total = total + bump(n)
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
item.render(). Una clase nueva sin relación con las anteriores proporciona un método render() compatible. El código de Python que llama no exige ninguna superclase. ¿Por qué puede incorporarse el objeto al bucle sin una rama específica para su tipo?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Set 5: Diagnostica los límites y combina las ideas10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
¿Qué muestra este programa de Python?
value = 6
def step(value):
value = value + 3
return value
for n in [1, 2]:
result = step(value)
print(value, result)
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
double() lo multiplica por 2; add_three() suma 3. Partiendo del mismo valor, ¿cuál es siempre la diferencia entre duplicar y después sumar, y sumar y después duplicar?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
readings tiene las filas (level, active): (50, true), (65, true), (20, false), (85, true) y (90, false). Una consulta solicita SUM(level) donde active = true AND level > 50. Después de añadir un índice adecuado, ¿qué total se requiere y puede cambiar el plan de ejecución?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
parent(lea, noah). y parent(noah, sora)., además de la regla grandparent(X, Z) :- parent(X, Y), parent(Y, Z).. ¿Qué encuentra grandparent(lea, Who)?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
[50, 65, 20, 85]. ¿Cuáles deben ser sus resultados correctos?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
¿Te atascas con una pregunta tuya? Ayuda con Tareas
Términos Clave
A
Abstracción
Análisis léxico
price * 3 se separa en un nombre, un operador de multiplicación y un número.Análisis sintáctico
total = price * 3; pertenece al interior de la asignación.Arquitectura del conjunto de instrucciones
Asignación
count = count + 1 lee el contador anterior antes de sustituirlo por el valor incrementado.Atributo
Á
Ámbito
C
Caso base
Clase
Loan de una biblioteca puede definir el miembro, el libro, la fecha de devolución y la operación de renovación de cada préstamo.Clausura
Código de bytes
Código máquina
Compilación anticipada
Compilación en tiempo de ejecución
Compilador
Completitud de Turing
Componente léxico
Composición
Conteo de referencias
Conversión implícita de tipos
Cuello de botella
D
Desbordamiento
Desbordamiento de búfer
Despacho dinámico
shape.area() a la implementación del rectángulo cuando el receptor es un rectángulo.Doble liberación de memoria
free sobre una reserva ya liberada puede causar una doble liberación de memoria y un comportamiento impredecible.E
Ecosistema
Efecto lateral
Encapsulación
Enlazador
Ensamblador
add eax, ebx en los bytes correspondientes de la instrucción x86-64.Error de ejecución
Error lógico
a + b / 2 para calcular el promedio de dos valores es un error lógico porque la división ocurre antes que la suma.Evaluación en cortocircuito
and.Expresión
width * height proporciona el valor que devuelve una función de área de rectángulos.Expresión lambda
lambda n: n * n proporciona una operación de elevar al cuadrado a una cadena de transformaciones.Expresión regular
[0-9]{3}-[0-9]{4} coincide con un grupo de tres dígitos seguido de un guion y cuatro dígitos.F
Fuga de memoria
Función de devolución de llamada
showMenu como función de devolución de llamada permite que un sistema de botones llame a esa función cuando el usuario hace clic.Función de orden superior
filter recibe un predicado que decide qué valores de entrada conservar.Función de primera clase
map como la operación que se aplicará a cada elemento de una colección.Función pura
H
Herencia
I
Implementación
Inferencia de tipos
Inmutabilidad
Interfaz
area() y dejar que cada figura lo calcule de forma distinta.Intérprete
L
Lenguaje de alto nivel
Lenguaje ensamblador
Lenguaje específico de dominio
Lenguaje multiparadigma
M
Manejador de eventos
Máquina virtual
Marco de pila
Método
Montículo de memoria
Mutación
N
Número en coma flotante
0.1 + 0.2 y el valor almacenado de 0.3.O
Objeto
Ocultación de información
Optimizador de consultas
P
Paradigma de programación
Parámetro
width recibe 3 cuando el código que llama evalúa area(3, 4).Pila de llamadas
area añade una entrada a la pila de llamadas y la retira cuando su resultado vuelve al código que llamó.Plataforma
Plegado de constantes
60 * 60 * 24 por 86 400 antes de que se inicie el programa.Polimorfismo
area() en círculos y rectángulos sin una rama independiente para cada uno.Programación declarativa
Programación dirigida por eventos
Programación estructurada
Programación funcional
Programación imperativa
Programación lógica
Programación procedimental
Propiedad de los datos
Puntero colgante
R
Recolección de basura
Recursión
Redefinición de métodos
Referencia
REPL
Reserva de memoria
S
Seguridad de memoria
Semántica
Sentencia
total = price * 3 asigna el resultado de una multiplicación a una variable.Sintaxis
if y sangría para el bloque controlado.SQL
T
Tabla de seguimiento
Tipado de pato
area() compatible.Tipado dinámico
Tipado estático
Tipo de dato
U
Uso de alias
b = a, el uso de alias explica por qué añadir un elemento mediante b también puede cambiar lo que se ve mediante a.Uso después de liberar memoria
Sobre esta unidad
Crea tus propios Materiales de Estudio
Convierte tus notas, diapositivas y documentos en Guías de Estudio, Preguntas de Práctica, Flashcards y 6 herramientas más.