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:

La imagen representa datos de entrada fluyendo hacia un proceso, en el medio, y datos de salida fluyendo desde el proceso, por la derecha.

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:

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