Un primer programa en Python para entender cómo funciona un ordenador
Los ordenadores son máquinas que parecen muy complejas. Enciendes uno, y tras un rato y algún logotipo, el ordenador te ofrece un escritorio virtual. En él, puedes navegar por distintas opciones, menús, ventanas y pantallas usando el ratón o el teclado. Abrir aplicaciones, trabajar, jugar, comunicarte,... algo especial debe haber ahí, dentro de esas máquinas maravillosas, para que sean capaces de hacer todo eso. Desde luego, no parece algo sencillo de conseguir.
Como decía un amigo, “sí... y no”. Obviamente, los ordenadores son algo complejo que ha requerido décadas de trabajo y avances para llegar a ponerlos donde están. Sin embargo, y al mismo tiempo, el funcionamiento fundamental de los ordenadores es algo muy sencillo.
Lo que el ordenador esencialmente hace es aplicar operaciones sencillas (las que sean, ya veremos ejemplos) sobre unos datos de entrada, para entregar unos resultados. El secreto es que el ordenador es capaz de llevar a cabo esto millones de veces por segundo. Ejecutando muy rápido muchas operaciones sencillas, los ordenadores son capaces de producir resultados muy elaborados sobre conjuntos de datos muy grandes. Los datos que un ordenador maneja se codifican de forma que, lo que al final se procesa, son valores numéricos. ¿Letras, símbolos, caracteres cualesquiera? Cada uno de ellos se codifica con un valor numérico. ¿Gráficos? Codificados con valores numéricos. ¿Sonido? Valores numéricos.
Así pues, al final, el principio fundamental de funcionamiento de un ordenador se puede resumir con la siguiente imagen:
Vamos a ver ese comportamiento tan fundamental con un ejemplo, esta vez en lenguaje Python.
Echa un vistazo al siguiente programa:
#!/usr/bin/python3
import sys
# importamos la librería sys,
# para poder usar input()
num = int(input())
# - leemos una cadena de texto
# de la entrada estándar
# con input(), y
# - convertimos el resultado
# a un número entero con int()
# - Asignamos el resultado a
# una variable llamada num
if num % 2 == 0:
# Si el resto de dividir num
# entre 2 es 0,
print("par")
# escribimos "par" en la
# salida estándar
else:
# en caso contrario,
print ("impar")
# escribimos "impar".
Aunque el funcionamiento está explicado en los comentarios, que es el texto que viene después de #
, hay algunas cosas que no son evidentes, y también algún argot que quiero explicar por si no has programado nunca. Por orden:
- La primera línea,
#!/usr/bin/python3
, le dice al terminal qué intérprete debe recibir el resto del fichero para poderlo ejecutar. Esto sólo ocurre en lenguajes de programación interpretados, como Python. En el caso de otros lenguajes de programación, como el C, los programas tienen que traducirse expresamente a una forma directamente ejecutable que ya discutiremos, mediante un proceso llamado “compilación”, y por lo tanto no necesitan una línea semejante. Esta ausencia la podemos observar volviendo a consultar el programa de ejemplo de un artículo anterior. - Una librería es un lugar donde alguien ha programado cosas útiles que podemos usar en nuestro programa. En Python podemos traérnoslas a nuestro programa con la sentencia
import
. En el ejemplo, queremos leer mensajes de texto de la... - ...entrada estándar, que es un elemento que el sistema operativo proporciona para que nuestro programa pueda recibir texto de otro programa, o de un usuario usando una terminal de texto.
- Una variable es un elemento de memoria con nombre, que puede almacenar un valor para poder trabajar con él. En el ejemplo,
num
es el nombre por el cual identificamos a la región de memoria donde hemos almacenado un número. Hay otros tipos de elementos de memoria; las variables se caracterizan porque el programador puede cambiar su valor en el transcurso de la ejecución, tantas veces como sea necesario. De ahí su nombre: es variable porque puede variar. - La salida estándar es el mecanismo complementario a la entrada estándar, dispuesto por el sistema operativo, para que nuestro programa pueda sacar resultados de texto a quienquiera que lo haya ejecutado.
Además, hay una serie de construcciones que conviene ir calando: int()
e input()
son funciones. Las funciones son trozos de código con nombre (int
o input
, por ejemplo), que pueden ser invocados muchas veces. Forman parte de una familia de construcciones que permiten organizar el código para hacerlo, entre otras cosas, reutilizable, más fácil de mantener, y más legible. Aceptan datos de entrada entre sus paréntesis en forma de parámetros o argumentos, y ofrecen un resultado por la izquierda:
num = int(input())
En esa línea, input()
, sin parámetros, hace que el programa se detenga, esperando por texto en la entrada estándar. En las condiciones más básicas, el programa se queda esperando a que escribamos algo y pulsemos la tecla intro (también llamada return, o ⏎). El resultado de input()
es introducido como parámetro de entrada en la función int()
, que interpreta el texto como un número, lo convierte al tipo adecuado, y lo devuelve al programa, que lo asigna a num
.
La distinción entre el número 1 y el texto que lo representa, “1”, será objeto de otros artículos más adelante; aunque hay alguna mención al hecho de que los caracteres se codifican con números dentro del ordenador, de momento quédate con que “1” y 1 no son iguales, y para un programa ni valen lo mismo ni funcionan igual, y por eso necesitamos invocar a int()
.
Como ves, el principio de “recibir datos de entrada, realizar un proceso sobre ellos, devolver unos datos de resultado” empieza a salir a flote con el concepto de función. Vamos a seguir analizando el código del programa, y luego volveremos sobre este principio fundamental.
Mención especial merece %
, que es la operación “resto de la división entera”, también llamada “módulo”. La sentencia num % 2 == 0
calcula el resto de la división entera de num
entre 2, y la compara con 0 usando el operador de comparación, ==
. El resultado de la comparación es la noción de verdadero o falso, que es lo que espera recibir la sentencia if
para poder trabajar.
La construcción if [condición]: ... else: ...
es una sentencia de control, porque permite tomar decisiones y controlar el flujo de la ejecución de nuestro programa. En él, si el resto de la división entera de un número entre 2 fuese 0, indicaría que el número es par, y por lo tanto se tomaría un camino. En caso de que el resto no fuese 0, indicaría que el valor de num
es impar, y el camino tomado sería el otro.
Por último el programa usa otra función, print()
, para escribir un texto en la salida estándar, que introducimos de forma literal, es decir, sin necesidad de utilizar variables para ello: "par"
o "impar"
.
El resultado de invocar este programa, suponiendo que se llama paridad.py
, y que tiene permisos de ejecución, es quedarse esperando por datos. Si introducimos 5
y pulsamos intro /return / ⏎, veremos el siguiente resultado:
$ ./paridad.py
5
impar
Otra vez, con 4
:
$ ./paridad.py
4
par
Así de sencillo es el funcionamiento más básico de un ordenador: datos de entrada 👉 proceso 👉 datos de salida. Nada más.
Este programa ilustra ese concepto fundamental que, cuando agregamos millones de veces por segundo, en docenas de procesos que ocurren a la vez en paralelo, usando (enviando datos a) circuitos especializados para producir gráficos y sonido, y usando (recibiendo datos de) componentes que permiten interactuar con el ordenador, como ratones y teclados, tenemos el resultado que estamos acostumbrados a usar.
Un último comentario, que podrás tener en mente: no todos los programas esperan la entrada del usuario, no todos son interactivos. Cuando enciendes el ordenador, hay un montón de cosas que pasan solas. Si lo que ves es el resultado de componer y agregar el resultado de muchos programas independientes entre sí, ¿cómo hace el sistema operativo para conectarlo todo? ¿Cómo conecta el sistema operativo dos o más programas distintos entre sí?
Ya conoces una de las posibles respuestas válidas a esta pregunta, en realidad, y consiste en la entrada y salida estándares. Lo que has experimentado en la terminal, en realidad, es una construcción de un programa externo, la shell, que escribe texto introducido por el usuario en la entrada estándar, recoge datos en la salida estándar, y los imprime por la pantalla.
Utilizando correctamente la entrada y salida estándar, junto con algo de ayuda por parte de la shell, se pueden llegar a lograr composiciones bastante potentes. Mira este ejemplo:
$ echo 5 | ./paridad.py
impar
Con este ejemplo, hemos dicho al sistema operativo que redrija la salida estándar de echo 5
(que no es otra cosa que 5; echo significa “eco”), a la entrada estándar de nuestro programa. Y, como no hemos dicho nada más, el resultado “par” ha sido impreso en la terminal. El programa no se ha detenido para esperar por el usuario, porque ya disponía de datos en la entrada estándar, los datos que echo 5
colocó en la salida estándar (el número 5).
Pero podemos seguir encadenando más operaciones con |
, que se llama pipe y significa tubería, para construír pipelines, o “líneas de tubería”. tr
, por ejemplo, es una pequeña utilidad que permite manipular texto con operaciones como pueden ser la conversión a mayúsculas. Vamos a añadirla a nuestro pipeline:
$ echo 5 | ./paridad.py | tr a-z A-Z
IMPAR
En este caso, hemos seguido entubando datos de un programa a otro, hasta conseguir una versión en mayúsculas del resultado. Y podríamos seguir así hasta conseguir el resultado final que buscamos, sea cual fuere, combinando y agregando distintos programas.
Lógicamente, el sistema operativo no usa el terminal o un shell a escondidas, y por lo tanto usará mecanismos distintos al pipe en la mayoría de las ocasiones. De ahí que etiquetase a la entrada y salida estándares, junto con el pipe, como una posbile respuesta de entre varias. El pipe, |
, es un representante de una familia de mecanismos conocidos como mecanismos de comunicación entre procesos, o ICP (Inter-Process Communication en inglés), que es muy numeroso y variado. Los sistemas operativos disponen de señales con distintos significados, áreas de memoria compartida, interrupciones (procesos y dispositivos pueden interrumpir a otros procesos, de acuerdo a algún evento concreto), estructuras similares a ficheros, pero en memoria, mensajes entre procesos, y un larguísimo etcétera. Aún sonando todos ellos como algo mucho más sofisticado que el pipe, son variantes del mismo concepto: proporcionar datos de entrada y recoger resultados.
En cualquier caso, espero que todo esto represente un ejemplo razonable de cómo pequeños programas colaboran entre ellos para conseguir resultados complejos a base de un mismo principio: aceptar datos de entrada, operar con ellos, y devolver el resultado. Cuando agregas muchas de esas operaciones, algunas sobre el resultado de las anteriores, ejecutándose a menudo en paralelo, y añades un sistema de gráficos, sonido, y todo lo demás, acabas obteniendo ese sistema gráfico al que estamos acostumbrados hoy en día. Que tan complejo parece…, y que tan complejo resulta a veces.
#SistemaOperativo #Ordenador #Python
👉 Sigue a micromáquina en @gabriel@micromaquina.com 👉 Sigue a Gabriel Viso en @gabriel@fedi.gvisoc.com