Versión imprimible multipagina. Haga click aquí para imprimir.
Programación
- 1: Introducción a la Programación
- 1.1: La Computadora
- 1.2: Sistemas de Numeración
- 1.3: Lógica Booleana
- 1.4: Configura tu Entorno de Desarrollo
- 2: Conceptos Iniciales
- 2.1: Variables y Tipos de Datos
- 2.2: Operaciones de Entrada y Salida
- 2.3: Control de Flujo
- 2.4: Funciones
- 2.5: Funciones Recursivas
- 3: Programación Orientada a Objetos
- 3.1: Clases y Objetos
- 3.2: Los cuatro pilares
- 3.2.1: Encapsulamiento
- 3.2.2: Herencia
- 3.2.3: Polimorfismo
- 3.2.4: Abstracción
- 3.2.5: Conclusión
- 4: Estructuras de Datos
- 4.1: Arreglos
- 4.2: Mapas (Diccionarios)
- 4.3: Listas enlazadas
- 4.4: Pilas
- 4.5: Colas
1 - Introducción a la Programación
La programación, en su esencia, es el acto de instruir a una máquina sobre cómo realizar una tarea específica. Es como si le estuvieras enseñando a tu perro a buscar la pelota, pero en este caso, el perro es tu computadora y la pelota es, por ejemplo, sumar dos valores.
Ahora, es posible que pienses que programar es simplemente escribir líneas de código. En realidad, la programación es un proceso más amplio que incluye no solo escribir código, sino también resolver problemas, diseñar sistemas y pensar lógicamente.
En el universo de la programación, hay lenguajes de alto nivel y lenguajes de bajo nivel. Un lenguaje de bajo nivel, como el ensamblador, está más cerca de lo que la máquina entiende, mientras que un lenguaje de alto nivel, como Python o JavaScript, es más amigable para nosotros, los humanos. Los lenguajes de bajo nivel permiten una manipulación casi directa del hardware con instrucciones especificas para determinadas operaciones, como encender un determinado pixel de la pantalla. Mientras que los lenguajes de alto nivel permiten operaciones complejas en lenguaje casi natural, como por ejemplo mostrar una imagen en pantalla.
Además, algunos lenguajes de programación son compilados y otros son interpretados. Si un lenguaje es compilado, significa que se traduce a un lenguaje que la máquina entiende antes de ser ejecutado. Por otro lado, los lenguajes interpretados son traducidos en tiempo real, mientras se ejecutan.
Breve historia de la programación
La programación no es un concepto nuevo. De hecho, ha estado con nosotros desde mucho antes de que las computadoras existieran en la forma que las conocemos hoy. Dispositivos como el ábaco y el astrolabio son ejemplos tempranos de herramientas que usamos para realizar cálculos complejos.
Pero fue con la llegada de las máquinas mecánicas, como la Máquina Analítica de Charles Babbage, que se sentaron las bases para la programación moderna. ¡Estamos hablando del siglo XIX!
Con el paso del tiempo, nacieron lenguajes que marcaron hitos, como Fortran y COBOL. Estos lenguajes sentaron las bases para las revoluciones tecnológicas que vendrían. Con la evolución de los lenguajes, también surgieron nuevos paradigmas: primero el Procedural, luego el Orientado a Objetos y más recientemente, el Funcional.
Hoy, estamos en una era moderna, dominada por la programación para la web, móviles y la nube. Cada vez que deslizas tu dedo por la pantalla de tu celular o compras algo online, hay líneas y líneas de código trabajando detrás de escena.
La programación hoy
La programación es el motor de nuestra sociedad moderna. Desde aplicaciones para pedir comida hasta sistemas avanzados de inteligencia artificial que ayudan en investigaciones médicas, la programación está en todas partes.
Además de simplificar nuestras vidas cotidianas, la programación tiene un impacto profundo en la sociedad. Ha permitido avances en la automatización, el análisis de datos y el entretenimiento. Y lo que es aún más emocionante, es que apenas estamos rasguñando la superficie. Con los avances en inteligencia artificial, computación cuántica y el Internet de las Cosas (IoT), ¿quién sabe qué maravillas nos esperan en mundo de la programación?
Mas artículos
1.1 - La Computadora
Si alguna vez te has preguntado qué es lo que hace “tictac” dentro de esa caja metálica que llamamos computadora, estás en el lugar indicado. En esencia, una computadora es una combinación de hardware y software que trabajan juntos para llevar a cabo tareas específicas.
El hardware es toda la parte física de la computadora: el CPU (que es como el cerebro de la máquina), la RAM (donde la computadora guarda la información con la que está trabajando en un momento determinado), dispositivos de almacenamiento (donde se guardan los datos de forma permanente) y periféricos (como el teclado, el mouse o la pantalla)1.
Por otro lado, el software es el conjunto de instrucciones que le dice al hardware qué hacer. Hay varios tipos de software, desde el software del sistema, como el sistema operativo que coordina todas las acciones de la máquina, hasta software de aplicación que nos permite hacer cosas como escribir documentos o jugar videojuegos2.
El papel del sistema operativo es crucial. Es el mediador entre el usuario y el hardware, asegurando que todo funcione armónicamente. Si el hardware fuera una orquesta, el sistema operativo sería el director que asegura que cada instrumento toque en el momento y de la manera correcta.
El sistema binario: el lenguaje secreto de las computadoras
A diferencia de nosotros, que usamos un sistema decimal basado en diez dígitos (del 0 al 9), las computadoras usan el sistema binario, que solo tiene dos dígitos: 0 y 1. ¿Por qué? Bueno, en el nivel más básico, una computadora está hecha de millones de transistores que pueden estar en uno de dos estados: encendido o apagado. Estos estados se representan con esos dígitos: 0 para apagado y 1 para encendido3.
Los términos “bit” y “byte” son fundamentales aquí. Un bit es la unidad más pequeña de datos en una computadora y puede tener un valor de 0 o 1. Un byte, por otro lado, es un conjunto de 8 bits y puede representar 256 valores diferentes (desde \(00000000\) hasta \(11111111\) en binario)4.
Este sistema binario no solo representa números, sino también texto, imágenes y cualquier tipo de dato. Por ejemplo, en el código ASCII (un estándar de codificación de caracteres), la letra “A” se representa como \(01000001\) en binario.
En un artículo posterior, vamos a hablar en detalle acerca del sistema binario y otro sistema muy utilizado en el ambiente de la computación, el hexadecimal.
Memoria y almacenamiento: donde residen nuestros datos
La memoria y el almacenamiento son dos conceptos cruciales en la informática. Si bien a menudo se usan indistintamente, tienen roles muy diferentes.
La memoria, específicamente la RAM, es volátil. Esto significa que la información se pierde cuando apagamos la computadora. La RAM es esencialmente el “espacio de trabajo” de la computadora, donde almacena datos e instrucciones mientras está en uso. Hay varios tipos de RAM, siendo DRAM y SRAM los más comunes5.
Por otro lado, tenemos la ROM (Memoria de Solo Lectura). A diferencia de la RAM, la ROM no es volátil y se utiliza para almacenar firmware, es decir, software que está íntimamente ligado al hardware y que no necesita cambios frecuentes.
En cuanto al almacenamiento, dispositivos como discos duros, SSDs y unidades flash nos permiten guardar información de manera permanente. Estos dispositivos forman parte de lo que se conoce como la jerarquía de memoria, que va desde la memoria caché (rápida pero pequeña) hasta el almacenamiento secundario (más lento pero con gran capacidad)6.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
Referencias
Patterson, D. & Hennessy, J. (2014). Arquitectura de Computadoras. Buenos Aires: Ediciones Omega. ↩︎
Silberschatz, A., Galvin, P. B., & Gagne, G. (2009). Fundamentos de Sistemas Operativos. Buenos Aires: Ediciones M. ↩︎
Tanenbaum, A. (2012). Estructura de Computadoras. Buenos Aires: Prentice Hall. ↩︎
Brookshear, J. G. (2011). Ciencia de la Computación: Una visión general. Buenos Aires: Pearson Educación. ↩︎
Jacob, B., Ng, S. W., & Wang, D. T. (2007). Arquitecturas de memoria en sistemas de computadoras. Buenos Aires: Wiley-Interscience. ↩︎
Siewiorek, D. P. & Swarz, R. S. (2017). Principios de diseño de sistemas computacionales. Buenos Aires: Morgan Kaufmann. ↩︎
1.2 - Sistemas de Numeración
El sistema decimal: la base de nuestra cotidianidad
Desde pequeños, nos enseñan a contar usando diez dígitos: del 0 al 9. Este sistema, conocido como decimal, es la base de casi todas nuestras actividades matemáticas y financieras, desde sumar cuentas hasta calcular intereses en el banco1. Tiene su origen en la cantidad de dedos que tenemos en las manos, lo que lo convierte en el sistema más intuitivo y natural para nosotros. Pero lo que lo hace especial es su naturaleza posicional.
Para comprender este concepto, consideremos el número 237:
- El 7, situado a la derecha, está en la posición de las unidades. Es decir, \(7 \times 10^0\) (cualquier número elevado a la potencia de 0 es 1). Por lo tanto, su valor es simplemente 7.
- El 3, en la posición del medio, representa las decenas, es decir, \(3 \times 10^1 = 3 \times 10 = 30\).
- El 2, el número más a la izquierda, está en la posición de las centenas, traduciéndose a \(2 \times 10^2 = 2 \times 100 = 200\).
Si sumamos estos valores,
El sistema binario: el lenguaje secreto de las computadoras
Si bien el sistema decimal domina nuestra vida cotidiana, las máquinas que usamos todos los días, desde nuestros celulares hasta las computadoras, operan en un mundo completamente diferente: el mundo binario. En este sistema, solo existen dos dígitos: 0 y 1. A primera vista, puede parecer limitante, pero este sistema es la esencia de la electrónica digital. Los dispositivos electrónicos, con sus millones de transistores, operan usando estos dos estados: encendido (1) y apagado (0)2.
A pesar de su aparente simplicidad, el sistema binario puede representar cualquier cantidad o información que el sistema decimal pueda expresar. Por ejemplo, el número decimal 5 se representa como 101 en binario.
El sistema binario, con sus unos y ceros, opera de manera similar al sistema decimal, pero en lugar de potencias de 10, usa potencias de 2.
Tomemos el número binario 1011:
- El bit más a la derecha representa \(1 \times 2^0 = 1\)
- El siguiente bit representa \(1 \times 2^1 = 2\)
- Luego viene \(0 \times 2^2 = 0\)
- El bit más a la izquierda en este número representa \(1 \times 2^3 = 8\)
Entonces, 1011 en binario se traduce a decimal de la siguiente manera:
El sistema hexadecimal: un puente entre humanos y máquinas
Mientras que el sistema binario es perfecto para las máquinas, puede ser un poco engorroso para nosotros, especialmente cuando tratamos con números binarios largos. Aquí es donde entra el sistema hexadecimal, que utiliza dieciséis dígitos distintos: 0-9 y A-F, donde A representa 10, B es 11, y así sucesivamente hasta F que es 153.
El hexadecimal es especialmente útil porque proporciona una forma más compacta de representar números binarios. Cada dígito hexadecimal corresponde a exactamente cuatro dígitos binarios (bits). Por ejemplo pensemos en la representación en binario del número 41279 y como el sistema hexadecimal consigue una representación más compacta:
Pero el sistema hexadecimal es más que una representación compacta de números binarios, es un sistema de numeración posicional como el decimal o binario con base 16 en lugar de 10 o 2. Veamos como conseguir la representación decimal del número del ejemplo anterior (A13F).
- El dígito más a la derecha representa \(F \times 16^0 = 15 \times 16^0 = 15\)
- El siguiente representa \(3 \times 16^1 = 48\)
- Luego viene \(1 \times 16^2 = 256\)
- El dígito más a la izquierda en este número representa \(A \times 16^3 = 10 \times 16^3 = 40960\)
Entonces, A13F en hexadecimal se traduce a decimal de la siguiente manera:
Conclusión
Los sistemas de numeración son como lentes a través de los cuales vemos y entendemos el mundo de las matemáticas y la computación. Aunque en nuestra vida diaria el sistema decimal sea el rey, es esencial apreciar y comprender los sistemas binario y hexadecimal, especialmente en esta era digital.
Así que, la próxima vez que estés frente a tu computadora o usando una app en tu celular, recordá que detrás de esa interfaz amigable, hay un mundo binario en pleno funcionamiento, y que el sistema hexadecimal actúa como un traductor entre ese mundo y nosotros.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
Referencias
1.3 - Lógica Booleana
La lógica booleana, nombrada en honor a George Boole, un matemático inglés del siglo XIX, es un sistema matemático que se ocupa de operaciones que tienen solo dos resultados posibles: verdadero o falso, representados generalmente como 1 y 0, respectivamente1. En su obra “An Investigation of the Laws of Thought”, Boole estableció las bases de esta lógica, presentando un sistema algebraico que podría utilizarse para representar estructuras lógicas.
Operaciones Booleanas
Dentro de la lógica booleana, existen operaciones fundamentales que permiten manipular y combinar estas expresiones binarias:
AND (Y): Esta operación devuelve verdadero (1) solo si ambas entradas son verdaderas. Por ejemplo, si tenemos dos interruptores, ambos deben estar en la posición encendido para que una luz se encienda.
OR (O): Devuelve verdadero si al menos una de las entradas es verdadera. Siguiendo con el ejemplo de los interruptores, con que uno de ellos esté encendido, la luz se iluminará.
NOT (NO): Es una operación unaria, lo que significa que solo tiene una entrada. Simplemente invierte el valor de entrada. Si le das un 1, devuelve un 0 y viceversa.
NAND (NO Y): Es la negación de AND. Solo devuelve falso si ambas entradas son verdaderas.
NOR (NO O): Es la negación de OR. Devuelve verdadero solo si ambas entradas son falsas.
XOR (O exclusivo): Devuelve verdadero si las entradas son diferentes. Si ambas son iguales, devuelve falso.
XNOR (NO O exclusivo): Es la negación de XOR. Devuelve verdadero si ambas entradas son iguales.
La importancia de esta lógica en computación y programación
La computación moderna, en su esencia, es la manipulación de bits, esos unos y ceros que mencionamos. Cada operación que realiza una computadora, desde simples cálculos hasta la renderización de gráficos complejos, implica operaciones booleanas en algún nivel2.
En programación, la lógica booleana se utiliza en estructuras de control, como condiciones (if, else) y bucles, permitiendo a los programas tomar decisiones basadas en ciertas condiciones.
Tablas de verdad: el mapa de la lógica Booleana
Una tabla de verdad es una representación gráfica de una operación booleana. Enumera todas las combinaciones posibles de entradas y muestra el resultado de la operación para cada combinación3.
Por ejemplo,
A | B | A AND B | A OR B | A XOR B | A NOR B | A NAND B | NOT A | A NXOR B |
---|---|---|---|---|---|---|---|---|
1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 |
0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
Conclusiones
La lógica booleana es mucho más que un conjunto de reglas matemáticas abstractas. Es el lenguaje fundamental de las máquinas, el código que subyace a la era digital en la que vivimos. Al comprender sus principios, no solo nos volvemos más adeptos a trabajar con tecnología, sino que también adquirimos una apreciación más profunda de las estructuras que sustentan nuestro mundo digital.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
Referencias
1.4 - Configura tu Entorno de Desarrollo
Elegir un lenguaje de programación
La elección del lenguaje de programación es el primer y quizás el más crucial paso en el proceso de aprendizaje. Hay varios factores a considerar al seleccionar un lenguaje, incluyendo:
- Propósito: ¿Para qué quieres programar? Si es para desarrollo web, JavaScript o PHP podrían ser buenas opciones. Si estás interesado en la ciencia de datos, R o Python podrían ser más adecuados.
- Comunidad: Un lenguaje con una comunidad activa puede ser esencial para los principiantes. Una comunidad vibrante generalmente significa más recursos, tutoriales y soluciones disponibles en línea.
- Curva de aprendizaje: Algunos lenguajes son más fáciles de aprender que otros. Es fundamental elegir uno que coincida con tu nivel de experiencia y paciencia.
- Oportunidades de trabajo: Si estás buscando una carrera en programación, investigar la demanda del mercado para diferentes lenguajes puede ser útil.
Aunque hay muchos lenguajes valiosos y poderosos, para este curso, hemos elegido Python. Este lenguaje es conocido por su simplicidad y legibilidad, lo que lo hace ideal para aquellos que están empezando. Además, Python cuenta con una comunidad activa y una amplia gama de aplicaciones, desde desarrollo web hasta inteligencia artificial1.
Instalación de Python
Para usuarios de Windows:
- Descargar el instalador:
- Visita el sitio web oficial de Python en https://www.python.org/downloads/windows/
- Haz clic en el enlace de descarga para la última versión de Python 3.x.
- Ejecuta el instalador:
- Una vez completada la descarga, localiza y ejecuta el archivo instalador
.exe
. - Asegúrate de marcar la casilla que dice “Agregar Python al PATH” durante la instalación. Este paso es crucial para hacer que Python sea accesible desde el Símbolo del Sistema.
- Sigue las indicaciones de instalación.
- Una vez completada la descarga, localiza y ejecuta el archivo instalador
- Verifica la instalación:
- Abre el Símbolo del Sistema y escribe:
python --version
- Esto debería mostrar la versión de Python que acabas de instalar.
- Abre el Símbolo del Sistema y escribe:
Para usuarios de Mac:
- Descargar el instalador:
- Visita el sitio web oficial de Python en https://www.python.org/downloads/mac-osx/
- Haz clic en el enlace de descarga para la última versión de Python 3.x.
- Ejecuta el instalador:
- Una vez descargado, localiza y ejecuta el archivo
.pkg
. - Sigue las indicaciones de instalación.
- Una vez descargado, localiza y ejecuta el archivo
- Verifica la instalación:
- Abre la Terminal y escribe:
python3 --version
- Esto debería mostrar la versión de Python que acabas de instalar.
- Abre la Terminal y escribe:
Para usuarios de Linux (Ubuntu/Debian):
- Actualiza los paquetes:
sudo apt update
- Instala Python:
sudo apt install python3
- Verifica la instalación:
- Después de la instalación, puedes comprobar la versión de Python instalada escribiendo:
python3 --version
- Después de la instalación, puedes comprobar la versión de Python instalada escribiendo:
Entornos de Desarrollo Integrado (IDEs)
Un IDE es una herramienta que facilita el desarrollo de aplicaciones al combinar comúnmente utilizadas en un solo software: editor de código, compilador, depurador, entre otros. Elegir el IDE adecuado puede hacer que el proceso de programación sea más fluido y eficiente.
Al evaluar IDEs, considera:
- Compatibilidad con el lenguaje: No todos los IDEs son compatibles con todos los lenguajes de programación.
- Características: Algunos IDEs ofrecen funcionalidades como autocompletado, resaltado de sintaxis y herramientas de depuración.
- Extensiones y plugins: La posibilidad de personalizar y extender tu IDE a través de plugins puede ser muy útil.
- Precio: Hay IDEs gratuitos y otros de pago. Evalúa si las características adicionales de un IDE de pago justifican el costo.
Para este curso, hemos seleccionado Visual Studio Code (VS Code). Es un IDE popular que es gratuito y de código abierto. Es conocido por su interfaz sencilla, amplia gama de plugins y capacidad para manejar múltiples lenguajes de programación2. Su comunidad activa garantiza actualizaciones regulares y una amplia gama de recursos de aprendizaje.
Instalación de Visual Studio Code
Para usuarios de Windows:
- Descargar el instalador:
- Visita el sitio web oficial de VS Code en https://code.visualstudio.com/
- Haz clic en el botón “Descargar para Windows”.
- Ejecuta el instalador:
- Una vez completada la descarga, localiza y ejecuta el archivo instalador
.exe
. - Sigue las indicaciones de instalación, incluyendo aceptar el acuerdo de licencia y elegir la ubicación de instalación.
- Una vez completada la descarga, localiza y ejecuta el archivo instalador
- Inicia VS Code:
- Tras la instalación, puedes encontrar VS Code en tu menú de inicio.
- Lánzalo, ¡y estarás listo para comenzar a programar!
Para usuarios de Mac:
- Descargar el instalador:
- Visita el sitio web oficial de VS Code en https://code.visualstudio.com/
- Haz clic en el botón “Descargar para Mac”.
- Instala VS Code:
- Una vez descargado, abre el archivo
.zip
. - Arrastra la aplicación Visual Studio Code
.app
a la carpetaAplicaciones
, para que esté disponible en el Launchpad.
- Una vez descargado, abre el archivo
- Inicia VS Code:
- Usa la búsqueda de Spotlight o navega hasta tu carpeta de Aplicaciones para iniciar VS Code.
Para usuarios de Linux (Ubuntu/Debian):
- Actualiza los paquetes e instala las dependencias:
sudo apt update sudo apt install software-properties-common apt-transport-https wget
- **Descarga e instala la claves necesarias:
wget -q https://packages.microsoft.com/keys/microsoft.asc -O- | sudo apt-key add -
- Añade el repositorio de VS Code:
sudo add-apt-repository "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main"
- Instala Visual Studio Code:
sudo apt update sudo apt install code
- Inicia VS Code:
- Puedes iniciar VS Code desde la terminal escribiendo
code
o encontrarlo en tu lista de aplicaciones instaladas.
- Puedes iniciar VS Code desde la terminal escribiendo
Escribe y ejecuta tu primer programa
Una vez que hayas configurado tu entorno de programación, es hora de sumergirse en la codificación.
¡Hola mundo!
Este es posiblemente el programa más icónico para principiantes. Es simple, pero te introduce al proceso de escribir y ejecutar código.
print("¡Hola mundo!")
Cálculo de área y perímetro de un triángulo
Este programa es un poco más complejo. No solo imprime un mensaje, sino que también realiza cálculos matemáticos.
# Entrada del usuario
lado1 = float(input("Introduce la longitud del primer lado: "))
lado2 = float(input("Introduce la longitud del segundo lado: "))
lado3 = float(input("Introduce la longitud del tercer lado: "))
# Cálculo del perímetro
perimetro = lado1 + lado2 + lado3
# Cálculo del área usando la fórmula de Herón
s = perimetro / 2
area = (s*(s-lado1)*(s-lado2)*(s-lado3)) ** 0.5
print(f"El perímetro del triángulo es: {perimetro}")
print(f"El área del triángulo es: {area:.2f}")
Conclusión
Configurar un entorno de programación puede parecer desalentador al principio, pero con las herramientas y recursos adecuados, se convierte en una tarea manejable y gratificante. Esperamos que este artículo te haya proporcionado una base sólida para comenzar tu viaje en programación. ¡Feliz codificación!
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
Referencias
2 - Conceptos Iniciales
2.1 - Variables y Tipos de Datos
Variables
Una variable es un contenedor para almacenar datos en la memoria de la computadora. Podemos pensar en ella como una caja con una etiqueta. La etiqueta es el nombre de la variable y dentro de la caja se almacena su valor.
Para declarar una variable en Python solo escribimos el nombre y le asignamos un valor:
edad = 28
precio = 19.95
soltero = True
Los nombres de variables deben comenzar con letras o guión bajo, y sólo pueden contener letras, números y guiones bajos. Se recomienda usar nombres significativos que representen el propósito de la variable.
En Python las variables no necesitan ser declaradas con un tipo particular. El tipo se infiere automáticamente al asignar el valor:
edad = 28 # edad es de tipo entero (int)
precio = 19.95 # precio es de tipo float
estudiante = True # soltero es de tipo booleano
Una vez asignada, una variable puede cambiar su valor en cualquier momento:
edad = 30 # Cambiamos edad a 30
Alcance y tiempo de vida
El alcance de una variable se refiere a las partes del código donde está disponible. Las variables declaradas fuera de funciones son globales y están disponibles en todo el archivo. Las variables dentro de una función son locales y solo visibles dentro de ella.
El tiempo de vida es el período durante el cual existe la variable en memoria. Las variables locales existen mientras se ejecuta la función, luego son destruidas. Las globales existen mientras el programa está en ejecución.
Asignación
La asignación con el operador =
permite cambiar o inicializar el valor de una variable:
numero = 10
numero = 20 # Ahora numero vale 20
También existen los operadores de asignación compuesta como +=
y -=
que combinan una operación y asignación:
numero += 5 # Suma 5 a numero (numero = numero + 5)
numero -= 2 # Resta 2 a numero
Tipos de datos
Los tipos de datos definen el tipo de valor que puede almacenar una variable. Python tiene varios tipos incorporados, incluyendo:
Numéricos: Para almacenar valores numéricos como enteros, flotantes, complejos:
entero = 10
flotante = 10.5
complejo = 3 + 4j
Cadenas: Para almacenar texto:
texto = "Hola Mundo"
Booleano: Para valores lógicos Verdadero o Falso:
variable_verdadera = True
variable_falsa = False
Colecciones: Para almacenar múltiples valores como listas, tuplas y diccionarios:
Listas: Secuencias mutables de valores:
lista = [1, 2, 3]
Tuplas: Secuencias inmutables de valores:
tupla = (1, 2, 3)
Diccionarios: Estructuras de pares llave-valor:
diccionario = {"nombre":"Juan", "edad": 20}
Es importante elegir el tipo de dato que mejor represente la información que queremos almacenar.
Operadores
Los operadores nos permiten realizar operaciones con valores y variables en Python. Algunos operadores comunes son:
Aritméticos:
+, -, *, /, %, //, **
Comparación:
==, !=, >, <, >=, <=
Lógicos:
and, or, not
Asignación:
=, +=, -=, *=, /=
Veamos ejemplos concretos de expresiones usando estos operadores en Python:
# Aritméticos
5 + 4 # Suma, resultado 9
10 - 3 # Resta, resultado 7
4 * 5 # Multiplicación, resultado 20
# Comparación
5 > 4 # Mayor que, resultado Verdadero
7 < 10 # Menor que, resultado Verdadero
# Lógicos
True and False # Resultado False
True or False # Resultado True
not True # Resultado False
# Asignación
numero = 10
numero += 5 # Suma 5 a numero, equivalente a numero = numero + 5
Cada tipo de operador trabaja con tipos de datos específicos. Debemos usarlos de forma consistente según el tipo de datos de nuestras variables.
Conversiones de tipo
A veces necesitamos convertir un tipo de dato a otro para realizar ciertas operaciones. En Python podemos convertir de forma explícita o implícita:
Explícita: Usando funciones como int()
, float()
, str()
:
flotante = 13.5
entero = int(flotante) # convierte 13.5 a 13
texto = "100"
numero = int(texto) # convierte "100" a 100
Implícita: Python convierte automáticamente en algunos casos:
entero = 100
flotante = 3.5
resultado = entero + flotante # resultado es 103.5, entero se convirtió a float
Algunas conversiones pueden generar pérdida de datos o errores:
flotante = 13.5
entero = int(flotante)
print(entero) # 13, se pierden los decimales
Para prevenir esto debemos elegir explícitamente conversiones que tengan sentido para nuestros datos.
Conclusión
En este artículo revisamos conceptos clave como variables, operadores, tipos de datos y conversiones en Python. Aplicar bien estos conceptos te permitirá manipular datos de forma eficiente en tus programas. Recomiendo practicar con ejemplos propios para ganar experiencia en usar estas características. ¡Éxitos en tu aprendizaje de Python!
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
2.2 - Operaciones de Entrada y Salida
Salida a pantalla
Python también provee funciones para enviar la salida de un programa a la “salida estándar”, generalmente la pantalla o terminal1.
La función print()
muestra el valor pasado como parámetro:
nombre = "Eric"
print(nombre) # muestra "Eric"
Podemos imprimir múltiples valores separados por comas2:
print("Hola", nombre, "!") # muestra "Hola Eric!"
También podemos usar valores literales sin asignar a variables3:
print("2 + 3 =", 2 + 3) # muestra "2 + 3 = 5"
Formateo de salida
Python provee varias formas de dar formato a la salida4:
f-Strings: Permiten insertar variables dentro de una cadena:
nombre = "Eric"
print(f"Hola {nombre}") # muestra "Hola Eric"
%s: Inserta cadenas de texto en una cadena de formato:
nombre = "Eric"
print("Hola %s" % nombre) # muestra "Hola Eric"
%d: Inserta números enteros:
valor = 15
print("El valor es %d" % valor) # muestra "El valor es 15"
.format(): Inserta valores en una cadena de formato:
nombre = "Eric"
print("Hola {}. Bienvenido".format(nombre))
# muestra "Hola Eric. Bienvenido"
Estas opciones de formateo nos permiten interpolar variables y valores en cadenas de texto para generar outputs personalizados. Podemos combinar múltiples valores y formateos en una sola cadena de salida2.
Entrada desde el teclado
Python provee funciones incorporadas para leer datos ingresados por el usuario en tiempo de ejecución. Esto se conoce como “entrada estándar”4.
La función input()
permite leer un valor ingresado por el usuario y asignarlo a una variable. Por ejemplo:
nombre = input("Ingresa tu nombre: ")
Esto muestra el mensaje “Ingresa tu nombre: " y espera a que el usuario escriba un texto y presione Enter. Ese valor se asigna a la variable nombre
2.
La función input()
siempre regresa una cadena de texto. Si queremos pedir un número u otro tipo de dato, debemos convertirlo usando int()
, float()
, etc1:
edad = int(input("Ingresa tu edad: "))
pi = float(input("Ingresa el valor de pi: "))
Leyendo múltiples valores
Podemos pedir y leer varios valores en una misma línea separándolos con comas3:
nombre, edad = input("Ingresa nombre y edad: ").split()
El método split()
divide la entrada en partes y retorna una lista de cadenas. Luego asignamos los elementos de la lista a variables separadas.
También podemos leer varias líneas de entrada con un ciclo4:
nombres = [] # lista vacía
for x in range(3):
nombre = input("Ingresa un nombre: ")
nombres.append(nombre)
Este código lee 3 nombres ingresados por el usuario y los agrega a una lista.
Salida a un archivo
Además de imprimir a pantalla, podemos escribir la salida a un archivo usando la función open()
1:
archivo = open("datos.txt", "w")
Esto abre datos.txt
para escritura (“w”) y retorna un objeto archivo.
Luego usamos archivo.write()
para escribir a ese archivo3:
archivo.write("Hola mundo!")
archivo.write("Este texto va al archivo")
Debemos cerrar el archivo con archivo.close()
cuando terminamos4:
archivo.close()
También podemos usar with
para abrir y cerrar automáticamente2:
with open("datos.txt", "w") as archivo:
archivo.write("Hola mundo!")
# no hace falta cerrar, es automático
Lectura de archivos
Para leer un archivo usamos open()
con modo “r” y iteramos sobre el objeto archivo1:
with open("datos.txt", "r") as archivo:
for linea in archivo:
print(linea) # muestra cada línea del archivo
Esto imprime cada línea, incluyendo los saltos de línea.
Podemos leer todas las líneas a una lista con readlines()
3:
linenas = archivo.readlines()
print(linenas)
Para leer el contenido completo a una cadena usamos read()
4:
texto = archivo.read()
print(texto)
También podemos leer un número determinado de bytes o caracteres con read(n)
2.
Operaciones para el manejo de archivos
Existen varias funciones incorporadas para manipular archivos en Python1:
open()
- Abre un archivo y retorna un objeto archivoclose()
- Cierra el archivowrite()
- Escribe datos al archivoread()
- Lee datos del archivoreadline()
- Lee una línea del archivotruncate()
- Vacía el archivoseek()
- Mueve la posición de lectura/escriturarename()
- Renombra el archivoremove()
- Elimina el archivo
Estas funciones nos permiten efectuar operaciones avanzadas para leer, escribir y mantener archivos.
Conclusión
En este artículo explicamos en detalle operaciones de entrada y salida en Python, incluyendo leer de entrada estándar y escribir a salida estándar o archivos. Manejar correctamente la entrada y salida es esencial para muchas aplicaciones de Python. Recomiendo practicar con ejemplos propios para dominar estas funciones3.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
Referencias
McKinney, W. (2018). Python for data analysis: Data wrangling with Pandas, NumPy, and IPython. O’Reilly Media. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media, Incorporated. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Matthes, E. (2015). Python crash course: A hands-on, project-based introduction to programming. No Starch Press. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Downey, A. B. (2015). Think Python: How to think like a computer scientist. Needham, Massachusetts: Green Tea Press. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
2.3 - Control de Flujo
Condiciones: tomando decisiones en el código
La vida está llena de decisiones: “Si llueve, llevaré un paraguas. De lo contrario, usaré anteojos de sol”. Estas decisiones también están presentes en el mundo de la programación. Las condiciones son como preguntas que la computadora se hace. Nos permiten tomar decisiones y ejecutar código específico dependiendo de una condición1. Pueden ser simples como “¿Está lloviendo?” o complejas como “¿Es fin de semana y tengo menos de $100 en mi cuenta bancaria?”.
if
La estructura if
nos permite evaluar condiciones y tomar decisiones basadas en el resultado de esa evaluación.
edad = 15
if edad >= 18:
print("Eres mayor de edad")
El código anterior permite ejecutar una porción de código si la edad de una persona es mayo o igual a 18 años.
if-else
Cuando se desea ejecutar un código alternativo si la condición es falsa, utilizamos la estructura if-else
edad = 21
if edad >= 18:
print("Eres mayor de edad")
else:
print("Eres menor de edad")
En este caso, se determina si la persona es mayor de edad, o menor de edad, el mensaje mostrado es diferente
if-elif-else
Cuando las condiciones son múltiples y no es suficientes con dos caminos, se utiliza la estructura if-elif-else
para evaluarlas forma encadenada.
edad = 5
if edad <= 13:
print("Eres un niño")
elif edad > 13 and edad < 18:
print("Eres un adolescente")
else:
print("Eres un adulto")
En el código anterior se observan tres caminos claros, uno para cuando la edad es menor o igual a 13 años, otro para cuando la edad esta entre 13 y 18 y otro para cuando es mayor o igual a 18.
Otra manera de resolver este problema es mediante la estructura switch-case
, que, aunque Python no incorpora de manera nativa, como si lo hacen otros lenguajes como Java o C++, es una herramienta importante para conocer. Esta estructura permite a los programadores manejar múltiples condiciones de manera más organizada que una serie de if-elif-else
.
En Java, por ejemplo:
int dia = 3;
switch(dia) {
case 1:
System.out.println("Lunes");
break;
case 2:
System.out.println("Martes");
break;
case 3:
System.out.println("Miércoles");
break;
// ... y así sucesivamente
default:
System.out.println("Día no válido");
}
En el ejemplo anterior, dependiendo del valor de dia
, se imprimirá el día correspondiente2.
Bucles: repitiendo acciones
A veces, en programación, necesitamos repetir una acción varias veces. En lugar de escribir el mismo código varias veces, podemos usar bucles. Estos, permiten repetir la ejecución de un bloque de código mientras se cumpla una condición3.
while
El bucle while
es útil cuando queremos repetir una acción basada en una condición.
# Imprime del 1 al 5
i = 1
while i <= 5:
print(i)
i = i + 1
do-while
Similar a while
pero garantiza al menos una ejecución dado que primero se ejecuta el bloque de código y luego se evalúa la condición. Python no implementa esta estructura, pero otros lenguajes como Java y C++ sí lo hacen.
int i = 1;
do {
System.out.println(i);
i++;
} while(i <= 5);
int numero = 0;
do {
std::cout << "Hola, mundo!" << std::endl;
numero++;
} while (numero < 5);
for
El bucle for
es útil cuando sabemos cuántas veces queremos repetir una acción.
for i in range(5):
print("Hola, mundo!")
El código anterior imprimirá “Hola, mundo!” cinco veces.
También podemos iterar sobre los elementos de una lista u objeto iterable:
nombres = ["María", "Florencia", "Julián"]
for nombre in nombres:
print(f"Hola {nombre}")
# Imprime
# Hola María
# Hola Florencia
# Hola Julián
Las sentencias break
y continue
Podemos usar break
para terminar el bucle y continue
para saltar a la siguiente iteración.
El break
se usa para terminar completamente el bucle cuando se cumple una condición, en el ejemplo siguiente, cuando i
llega a 5.
# Ejemplo de break
i = 0
while i < 10:
print(i)
if i == 5:
break
i += 1
# Imprime:
# 0
# 1
# 2
# 3
# 4
# 5
El continue
se usa para saltarse una iteración del bucle y continuar con la siguiente cuando se cumple una condición. Aquí lo usamos para saltarnos los números pares.
# Ejemplo de continue
i = 0
while i < 10:
i += 1
if i % 2 == 0:
continue
print(i)
# Imprime:
# 1
# 3
# 5
# 7
# 9
Anidamiento: combinando estructuras
Las estructuras de control de flujo pueden anidarse dentro de otras. Por ejemplo, podemos tener bucles dentro de bucles o condiciones dentro de bucles.
for i in range(5):
for j in range(10):
if (i % 2 == 0 and j % 3 == 0):
print(f"i = {i}, j = {j}")
Este código imprimirá combinaciones de i
y j
sólo cuando i
sea divisible por 2 y j
sea divisible por 3, demostrando cómo los bucles se anidan y se ejecutan3.
Patrones de uso comunes
Existen patrones específicos para resolver necesidades habituales con control de flujo.
Búsqueda
Buscar un valor en una colección:
frutas = ["manzana", "naranja"]
buscando = "naranja"
encontrado = False
for fruta in frutas:
if fruta == buscando:
encontrado = True
break
if encontrado:
print("Fruta encontrada!")
Acumulación
Acumular valores incrementales en un bucle.
total = 0
for i in range(10):
total += i
print(total) # Suma de 0..9 = 45
Diagramas de flujo: la ruta visual hacia el entendimiento del código
Los programadores, sin importar si son principiantes o expertos, a menudo se encuentran enfrentando desafíos que requieren una planificación detallada antes de sumergirse en el código. Aquí es donde los diagramas de flujo entran en juego como una herramienta esencial. Estos diagramas son representaciones gráficas de los procesos y la lógica detrás de un programa o sistema. En este artículo, desentrañaremos el mundo de los diagramas de flujo, desde sus conceptos básicos hasta las técnicas avanzadas, y cómo pueden beneficiar a programadores de todos los niveles.
Un diagrama de flujo es una representación gráfica de un proceso. Utiliza símbolos específicos para representar diferentes tipos de instrucciones o acciones. Su objetivo principal es simplificar la comprensión de un proceso, mostrando paso a paso cómo fluye la información o las decisiones. Estos diagramas:
- Facilitan la comprensión de procesos complejos.
- Ayudan en la fase de diseño y planificación de un programa.
- Sirven como documentación y referencia para futuros desarrollos.
Los diagramas de flujo son una herramienta poderosa que no solo beneficia a los principiantes, sino también a los programadores experimentados. Ofrecen una visión clara y estructurada de un proceso o programa, facilitando la planificación, el diseño y la comunicación entre los miembros del equipo.
Elementos básicos
Los diagramas de flujo constan de varios símbolos, cada uno con un significado específico:
- Ovalo: Representa el inicio o el fin de un proceso.
- Rectángulo: Denota una operación o instrucción.
- Diamante: Indica una decisión basada en una condición.
- Flechas: Muestran la dirección del flujo.
graph TD; start((Inicio)) process[Proceso] decision{¿Repetir?} final((Final)) start --> process; process --> decision; decision --> |Si| process decision --> |No| final
Ejemplos
Vamos a diseñar un diagrama de flujo para un programa que pida un número y nos diga si es par o impar.
graph TB inicio((Inicio)) entrada[Ingresar número] decision{¿Es par?} esPar[Es par] esImpar[Es impar] final((Final)) inicio --> entrada entrada --> decision decision --> |Si| esPar decision --> |No| esImpar esPar --> final esImpar --> final
Conforme los programas se vuelven más complejos, es posible que necesites incorporar bucles, múltiples condiciones y otros elementos avanzados en tu diagrama de flujo. Por ejemplo, aquí diagramamos un programa que sume los números desde el 1 al número ingresado por el usuario.
graph TD inicio((Inicio)) entrada[Ingresar número] setVariables[Establecer suma=0 y contador=1] bucle_condicion{¿contador <= N?} bucle_codigo[Sumar valor e incrementar el contador] resultado[Mostrar suma] final((Final)) inicio --> entrada entrada --> setVariables setVariables --> bucle_condicion bucle_condicion --> |Si| bucle_codigo bucle_codigo --> bucle_condicion bucle_condicion --> |No| resultado resultado --> final
Conclusión
El control de flujo es el corazón de la programación. Sin él, los programas serían secuencias lineales de acciones sin la capacidad de tomar decisiones o repetir tareas. Al dominar estas estructuras, no solo mejoras tu capacidad para escribir código, sino también tu capacidad para pensar lógicamente y resolver problemas complejos.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
Referencias
Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media, Incorporated. ↩︎
Deitel, P., & Deitel, H. (2012). Java: How to program. Upper Saddle River, NJ: Prentice Hall. ↩︎
Matthes, E. (2015). Python crash course: A hands-on, project-based introduction to programming. San Francisco, CA: No Starch Press. ↩︎ ↩︎
2.4 - Funciones
¿Qué son las funciones?
Una función, en términos simples, es un bloque de código que se ejecuta sólo cuando es llamado. Puedes pensar en ella como un pequeño programa dentro de tu programa principal, diseñado para realizar una tarea específica1. Una función también puede verse como una caja negra: le pasamos una entrada (parámetros), ocurre algún procesamiento interno, y produce una salida (retorno).
Las funciones nos permiten segmentar nuestro código en partes lógicas, donde cada parte realiza una única acción. Esto brinda varios beneficios2:
- Reutilización: Una vez definida la función, podemos ejecutar (llamar) ese código desde cualquier lugar de nuestro programa cuantas veces sea necesario.
- Organización: Permite dividir un programa grande en partes más pequeñas y manejables.
- Encapsulamiento: Las funciones reducen la complejidad escondiendo los detalles de implementación internos.
- Mantenimiento: Si necesitamos realizar cambios, solo debemos modificar el código en un lugar (la función) en lugar de rastrear todas las instancias de ese código.
Procedimientos vs. Funciones
Es vital distinguir entre estos dos conceptos. Mientras que una función siempre devuelve un valor, un procedimiento realiza una tarea pero no devuelve nada. En algunos lenguajes, esta diferencia es más clara que en otros. Python, por ejemplo, tiene funciones que pueden o no devolver valores.
Anatomía de una función
En Python, una función se declara usando la palabra clave def
, seguida del nombre de la función y paréntesis. El código dentro de la función se denomina el cuerpo de la función3 y contiene el conjunto de instrucciones a ejecutar para cumplir con su tarea..
def mi_funcion():
print("¡Hola desde mi función!")
Para llamar o invocar una función, simplemente usamos su nombre seguido de paréntesis:
mi_funcion() # Salida: ¡Hola desde mi función!
Parámetros y argumentos
Las funciones se vuelven aún más poderosas cuando les pasamos información, conocida como parámetros. Estos actúan como “variables” dentro de la función y permiten que la función trabaje con diferentes datos cada vez que se llama.
Mientras que los parámetros son variables definidas en la definición de la función. Los argumentos son los valores reales pasados al llamar a la función.
def saludo(nombre):
print(f"¡Hola {nombre}!")
saludo("María")
# Salida:
# ¡Hola María!
Podemos definir valores por defecto para los parámetros Python permite parámetros por defecto, que tienen un valor predeterminado, lo cual hace opcional pasar esos argumentos al llamar la función. También permite parámetros nombrados que permiten pasar los argumentos en cualquier orden, especificando su nombre.
def saludo(nombre="María", repeticiones=3):
repeticion = 1
while repeticion <= repeticiones:
print(f"¡Hola {nombre}!")
repeticion += 1
saludo()
# Salida:
# ¡Hola María!
# ¡Hola María!
# ¡Hola María!
saludo("Florencia", 4)
# Salida:
# ¡Hola Florencia!
# ¡Hola Florencia!
# ¡Hola Florencia!
# ¡Hola Florencia!
saludo(repeticiones=2, nombre="Julián")
# Salida
# ¡Hola Julián!
# ¡Hola Julián!
Retorno de valores
Las funciones pueden devolver un resultado o valor de retorno usando la palabra reservada return
.
def area_circulo(radio):
return 3.14 * (radio ** 2)
resultado = area_circulo(10)
print(resultado) # Salida: 314
El valor de retorno se pasa de vuelta a donde se llamó la función y se puede asignar a una variable para usarlo.
Las funciones también pueden ejecutar alguna tarea sin devolver nada explícitamente. En Python esto se conoce como retornar None
.
Variables locales y globales
Las variables locales se definen dentro de una función y solo existen en ese ámbito, mientras que las variables globales están definidas fuera y pueden ser accedidas desde cualquier parte del código. Es crucial entender su alcance (dónde puede ser accesible una variable) y duración (cuánto tiempo vive una variable).
x = 10 # x es global
def suma():
y = 5 # y es local
return x + y
suma() # Salida: 15
print(y) # Error, y no existe fuera de la función
Podemos leer variables globales desde una función, pero si necesitamos modificarla debemos declararla global
.
x = 10
def suma():
global x
x = x + 5
suma()
print(x) # 15
Buenas prácticas
Al crear funciones debemos seguir ciertos principios y patrones4:
- El nombre de una función debe indicar claramente su propósito.
- Hacer las funciones pequeñas, simples y enfocadas en una tarea. Una función debe hacer una cosa y hacerla bien.
- Utilizar nombres descriptivos para las funciones y sus parámetros.
- Evitar efectos secundarios y modificación de variables globales.
- Documentar adecuadamente el propósito y uso de cada función.
- Limitar el número de parámetros, idealmente de 0 a 3 parámetros.
Seguir estas buenas prácticas nos ayudará a crear funciones reutilizables, encapsuladas y fáciles de mantener.
Conclusión
Las funciones son componentes fundamentales en la programación, permitiéndonos organizar, reutilizar y encapsular código. Definiendo funciones que realicen una sola tarea mantenemos nuestros programas simplificados, fáciles de entender y modificar. Al comprender y dominar este concepto, no solo mejoras la calidad de tu código sino también tu eficiencia como desarrollador.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
Referencias
McConnell, S. (2004). Code Complete. Microsoft Press. ↩︎
Joyanes Aguilar, L. (2008). Fundamentos de programación: algoritmos, estructura de datos y objetos. McGraw-Hill. ↩︎
Python Software Foundation. (2022). Documentación oficial de Python. ↩︎
Kindler, E., & Krivy, I. (2011). Object-Oriented Simulation of systems with Java: A working introduction. BoD–Books on Demand. ↩︎
2.5 - Funciones Recursivas
Recursión: el arte de llamarse a sí mismo
Imagina una caja de espejos donde cada espejo refleja lo que ve en el siguiente, creando una serie infinita de reflejos. La recursión en programación es algo similar. Es una técnica donde una función se llama a sí misma directa o indirectamente[^1^]. Esto crea un ciclo en el cual la función resuelve un problema dividiéndolo en instancias más pequeñas del mismo problema, hasta llegar a un caso base sencillo de resolver.
Por ejemplo, imaginemos una función que imprime un contador regresivo:
def cuenta_regresiva(numero):
if numero > 0:
print(numero)
cuenta_regresiva(numero - 1)
else:
print("¡Despegue!")
cuenta_regresiva(5)
Esta función se llama recursivamente reduciendo el número cada vez hasta llegar a 0, y luego imprime el mensaje de despegue.
La recursión es un enfoque declarativo donde se enfoca en dividir un problema en casos recursivos sin necesidad de controlar explícitamente el bucle usando iteradores o contadores como en la programación imperativa.
La estructura de una función recursiva
El poder de la recursión radica en su simplicidad. Sin embargo, es esencial entender su estructura para evitar caer en trampas comunes. Una función recursiva típica tiene dos partes principales1:
- Caso base: El caso más simple con una solución conocida que no requiere recursión. Es la condición de parada, que detiene la recursión. Sin el caso base, caeríamos en una recursión infinita que eventualmente desborda la pila de llamadas.
- Caso recursivo: Es donde ocurre la mágica llamada recursiva. En este punto, la función se llama a sí misma con un argumento modificado, generalmente una versión reducida del problema original.
Ejemplos clásicos de recursión
Factorial
El factorial de un entero positivo \(n\) es el producto de todos los enteros positivos menores o iguales a \(n\). Por ejemplo:
- \(5! = 5 * 4 * 3 * 2 * 1 = 120\)
- \(4! = 4 * 3 * 2 * 1 = 24\)
- \(3! = 3 * 2 * 1 = 6\)
Aquí está el código en Python para calcular el factorial usando recursión:
def factorial(n):
if n == 1:
return 1 # Caso base
return n * factorial(n-1) # Caso recursivo
- Caso base: El caso base es la instancia más simple y pequeña del problema que puede responderse directamente. Para el factorial, cuando \(n = 1\), el resultado es \(1\).
- Caso recursivo: Si \(n\) es mayor que \(1\), la función se llama a sí misma con \(n-1\), y multiplica el resultado por \(n\).
Digamos que quieres calcular el factorial de \(5\), así que llamas a factorial(5)
.
Esto es lo que sucede:
- Paso 1: Como \(n = 5\) no es \(1\), la función llama a
factorial(4)
, luego multiplica el resultado por \(5\). - Paso 2: Ahora, dentro de
factorial(4)
, \(n = 4\), entonces la función llama afactorial(3)
, luego multiplica el resultado por \(4\). - Paso 3: Dentro de
factorial(3)
, \(n = 3\), así que llama afactorial(2)
, luego multiplica el resultado por \(3\). - Paso 4: Dentro de
factorial(2)
, \(n = 2\), así que llama afactorial(1)
, luego multiplica el resultado por \(2\). - Paso 5: Finalmente,
factorial(1)
alcanza el caso base, donde \(n = 1\), así que retorna \(1\).
Ahora los resultados se desenrollan:
factorial(2)
retorna \(2 * 1 = 2\)factorial(3)
retorna \(3 * 2 = 6\)factorial(4)
retorna \(4 * 6 = 24\)factorial(5)
retorna \(5 * 24 = 120\)
El resultado final es \(120\), que es el valor de \(5!\).
Aquí hay una representación visual de la pila de llamadas:
factorial(5)
-> factorial(4)
-> factorial(3)
-> factorial(2)
-> factorial(1)
return 1
return 2
return 6
return 24
return 120
Serie de Fibonacci
La serie de Fibonacci es una secuencia de números donde cada número es la suma de los dos anteriores. Comienza con \(0\) y \(1\), y cada número posterior es la suma de los dos números anteriores. Los primeros números de la secuencia son: \(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …\)
Aquí está el código en Python para calcular el \(n^th\) número de Fibonacci usando recursión de cola:
def fibonacci(n, a=0, b=1):
if n == 0:
return a
return fibonacci(n-1, b, a+b)
La función toma tres parámetros:
- \(n\): La posición del número deseado en la secuencia.
- \(a\) y \(b\): Dos números que ayudan en el cálculo de la secuencia.
Aquí hay un desglose de cómo funciona la función:
Caso Base: Si \(n\) es \(0\), la función devuelve \(a\). Este es el valor del \(n^th\) número en la secuencia.
Caso Recursivo: Si \(n\) no es \(0\), la función se llama a sí misma con \(n-1\), \(b\), y \(a+b\). Estos parámetros cambian la posición en la secuencia y preparan los siguientes números para la suma.
Supongamos que queremos encontrar el \(5^th\) número en la secuencia de Fibonacci llamando a fibonacci(5)
.
Esto es lo que sucede:
- Paso 1: Dado que \(n = 5\), llama a
fibonacci(4, 1, 1)
(porque \(a = 0\), \(b = 1\), \(a + b = 1\)). - Paso 2: Dado que \(n = 4\), llama a
fibonacci(3, 1, 2)
(porque \(a = 1\), \(b = 1\), \(a + b = 2\)). - Paso 3: Dado que \(n = 3\), llama a
fibonacci(2, 2, 3)
(porque \(a = 1\), \(b = 2\), \(a + b = 3\)). - Paso 4: Dado que \(n = 2\), llama a
fibonacci(1, 3, 5)
(porque \(a = 2\), \(b = 3\), \(a + b = 5\)). - Paso 5: Dado que \(n = 1\), llama a
fibonacci(0, 5, 8)
(porque \(a = 3\), \(b = 5\), \(a + b = 8\)). - Paso 6: Dado que \(n = 0\), devuelve \(a\), que es \(5\).
El resultado es \(5\), que es el \(5^th\) número en la secuencia de Fibonacci.
Aquí hay una representación visual de la pila de llamadas:
fibonacci(5, 0, 1)
-> fibonacci(4, 1, 1)
-> fibonacci(3, 1, 2)
-> fibonacci(2, 2, 3)
-> fibonacci(1, 3, 5)
-> fibonacci(0, 5, 8)
return 5
Ventajas y desventajas
La recursión tiene ciertas ventajas2:
- Puede resultar en soluciones simples y elegantes para problemas que se dividen fácilmente en subproblemas.
- Elimina la necesidad de control de bucles explícito.
- Sigue la estructura matemática de una definición recursiva.
Las desventajas incluyen:
- Puede ser menos eficiente (alto consumo de memoria) que la iteración debido a las llamadas repetidas y creación de marcos de pila.
- Demasiada recursión puede desbordar la pila de llamadas y causar errores.
- Puede ser más difícil de depurar y analizar que la iteración.
Por lo tanto, la recursión es una herramienta poderosa que debe usarse con discreción en los casos apropiados.
Recursión vs Iteración
La recursión y la iteración (usando ciclos) son paralelos y podemos usar cualquiera para resolver muchos problemas. Ambas técnicas tienen el potencial de resolver los mismos problemas, pero su implementación y eficiencia pueden variar. Tomemos el ejemplo del factorial:
Iterativo
def factorial_iterativo(n):
resultado = 1
for i in range(1, n+1):
resultado *= i
return resultado
Recursivo
def factorial_recursivo(n):
if n == 1:
return 1
return n * factorial(n-1)
La versión iterativa es más eficiente en términos de espacio, pero la recursiva es más limpia y fácil de entender. La elección entre recursión e iteración a menudo depende del problema específico, las restricciones de memoria y las preferencias del programador.
Conclusión
La recursión es una técnica clave que permite escribir algoritmos elegante, naturales y eficientes si se utiliza adecuadamente. Entender cómo dividir un problema en casos recursivos es esencial para dominar esta habilidad. La recursión ofrece una alternativa declarativa única para resolver problemas complejos sin necesidad de administrar bucles explícitos. Sin embargo, es crucial recordar siempre definir un caso base adecuado y ser consciente de las limitaciones de la recursión en términos de eficiencia y uso de memoria. Saber combinar recursión e iteración nos da flexibilidad al crear soluciones óptimas.
Como siempre, la clave está en encontrar el equilibrio adecuado y utilizar la herramienta correcta para el trabajo adecuado.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
Referencias
3 - Programación Orientada a Objetos
La Programación Orientada a Objetos (POO) es un paradigma de programación que se ha vuelto indispensable en la actualidad. Este enfoque modela elementos del mundo real como “objetos” que tienen propiedades y comportamientos, lo cual permite crear programas más intuitivos y fáciles de mantener. En este artículo veremos los conceptos básicos de POO y sus ventajas frente a otros paradigmas como la programación procedural. ¡Empecemos!
Este paradigma se basa en dos conceptos fundamentales:
- Objetos: entidades que combinan estado (datos) y comportamiento (operaciones) en una misma unidad. Por ejemplo, un objeto “coche” tendría propiedades como color, número de puertas, velocidad máxima, etc. Y comportamientos como acelerar, frenar, girar, etc.
- Clases: especificaciones que definen la estructura y comportamiento común de un grupo de objetos. La clase “coche” serviría como molde para crear objetos coche con las mismas características.
Como explica el programador Alan Kay, uno de los creadores de la POO:
“La idea central de POO es que los usuarios deben manipular objetos conceptuales más que máquinas de Turing. Las interfaces con el mundo real deben, por lo tanto, ser construidas en términos de objetos conceptuales.”
Es decir, la POO modela conceptualmente elementos del mundo real para hacer la programación más intuitiva.
Paradigmas de programación
Antes de profundizar en la POO, conviene entender que existen diferentes paradigmas o enfoques para abordar la programación. Los principales son:
Programación procedural
Secuencia ordenada de instrucciones que el programa debe seguir paso a paso. El foco está en procedimientos y funciones. Por ejemplo, C es un lenguaje orientado a la programación procedural.
La programación procedural es mejor para:
- Problemas sencillos o algoritmos secuenciales.
- Código que no necesitará reusarse ni expandirse mucho.
- Casos donde el rendimiento y eficiencia son críticos.
Programación orientada a objetos
Modelo basado en objetos que contienen datos y código en unidades cohesivas. El foco está en las clases y en la interacción entre objetos. Por ejemplo, Java y Python son lenguajes orientados a objetos.
La POO permite modelar de forma más directa elementos del mundo real, encapsular mejor los datos y reutilizar código a través de la herencia entre clases.
Las principales ventajas de POO frente a la programación procedural son:
- Modularidad: los objetos agrupan datos y operaciones relacionadas, encapsulando la complejidad interna. Esto permite trabajar con módulos independientes.
- Ocultación de información: Los objetos pueden exponer una interfaz simple y ocultar detalles de implementación internos. Esto reduce acoplamientos.
- Reusabilidad: Las clases permiten reuse de código. Una clase abstracta puede heredar a múltiples subclases.
- Extensibilidad: Podemos extender el comportamiento de clases padres creando nuevas subclases.
- Mapeo conceptual: Los objetos representan entidades del mundo real, lo cual facilita la traducción de requerimientos a código.
Sin embargo, la POO también tiene desventajas. Según el programador Paul Graham:
“La programación orientada a objetos suele ser una molestia. Hace que las cosas sean más difíciles de lo que deberían ser.”
Por ejemplo, para problemas simples la POO puede resultar excesiva. Y en proyectos grandes existe el riesgo de abusar de la herencia y el polimorfismo, volviendo el código difícil de seguir.
En definitiva, la POO es más adecuada cuando:
- El problema a modelar tiene entidades claras y estructuradas.
- Queremos reutilizar código encapsulado en clases modulares.
- Trabajamos en sistemas que deben extenderse y mantenerse con facilidad.
Mas artículos
3.1 - Clases y Objetos
Anatomía de una clase
Una clase actúa como un plano o molde para construir objetos similares, definiendo sus características comunes y funcionalidades. Es similar al plano para construir casas de un mismo barrio: todas comparten ciertos atributos clave.
Los componentes típicos de una clase son:
Atributos (propiedades): Variables que caracterizan al objeto. Por ejemplo, para una clase Persona
, atributos como nombre
, edad
, DNI
, etc.
class Persona:
dni = ""
nombre = ""
edad = 0
Métodos: Funciones que definen comportamientos. Por ejemplo, una Persona
puede caminar()
, hablar()
, comer()
, etc. Acceden a los atributos para implementar dicha funcionalidad.
Constructor: Método especial __init__()
que se ejecuta al instanciar la clase y permite inicializar los atributos.
Destructor: Método __del__()
que se ejecuta al eliminar la instancia liberando recursos. Opcional en algunos lenguajes.
Creando objetos
A partir de la clase generamos objetos, que son instancias concretas con sus propios atributos definidos. Digamos que la clase Casa es el plano, y una casa específica en una calle determinada es el objeto.
En código creamos un objeto invocando la clase como si fuera un método:
# Clase Persona
class Persona:
def __init__(self, n, e):
self.nombre = n
self.edad = e
# Objeto Persona específico
pepe = Persona("Pepe", 30)
juan = Persona("Juan", 35)
Cada objeto comparte la estructura y comportamiento general, pero puede almacenar distintos datos.
Utilizando Propiedades y Métodos
Ya tenemos una clase Persona
y un objeto pepe
de tipo Persona
. ¿Cómo interactuamos con el objeto?
- Propiedades: Es posible acceder al valor de un atributo del objeto utilizando la referencia al objeto (
pepe
) y el nombre del atributo.
pepe.nombre # "Pepe"
pepe.edad # 30
- Métodos: De la misma manera en la que se accede a los atributos pero agregando un paréntesis dentro del cual se pasan los argumentos si es que recibe alguno.
# Clase Persona
class Persona:
def __init__(self, n, e):
self.nombre = n
self.edad = e
def comer(self, comida):
print(f"Comiendo {comida}")
# Objeto Persona específico
pepe = Persona("Pepe", 30)
pepe.comer("pizza") # Imprime "Comiendo pizza"
El objeto pepe tiene ahora estado (propiedades) y comportamiento (métodos) propios.
Self vs This
Un detalle importante en los métodos es cómo acceden a los atributos y otros métodos del objeto. Aquí entra otra diferencia entre lenguajes:
- Self: En Python, los atributos y métodos se acceden dentro de la clase anteponiendo
self
. Esto apunta al objeto instanciado.
class Persona:
def __init__(self, nombre):
self.nombre = nombre
def saludar(self):
print(f"Hola! Soy {self.nombre}")
juan = Persona("Juan")
juan.saludar()
# Imprime "Hola! Soy Juan"
- This: En Java o C#, se utiliza
this
en lugar de self. Cumple la misma funcionalidad de apuntar a los miembros del objeto.
public class Person {
private String nombre;
public Person(String nombre) {
this.nombre= nombre;
}
public void saludar() {
System.out.println("Hola! Soy " + this.nombre);
}
}
Person juan = new Person("Juan");
juan.saludar();
// Imprime "Hola! Soy Juan"
Conclusión
Las clases y objetos son los conceptos clave de la POO, permitiendo modelar entidades de la realidad y generar componentes modulares y genéricos de nuestro sistema para construir programas más robustos y fáciles de entender y mantener.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
3.2 - Los cuatro pilares
3.2.1 - Encapsulamiento
La importancia del encapsulamiento radica en varios aspectos clave:
- Protección de datos: Al controlar el acceso a los datos del objeto a través de métodos, podemos asegurar que los datos se mantengan consistentes y válidos.
- Modularidad: El encapsulamiento permite que los objetos sean autocontenidos, facilitando la comprensión y el mantenimiento del código.
- Flexibilidad: La implementación interna puede ser modificada sin afectar otras partes del código que utilizan el objeto.
- Reducción de complejidad: Al ocultar los detalles del funcionamiento interno, el encapsulamiento reduce la complejidad del sistema desde una perspectiva externa.
Implementación en Python
Python ofrece varios mecanismos para implementar el encapsulamiento. Exploremos estos con ejemplos:
1. Uso de atributos privados
En Python, podemos crear atributos privados prefijando el nombre del atributo con doble guion bajo (__
). Esto activa el “name mangling”, que hace que el atributo sea más difícil de acceder desde fuera de la clase.
class CuentaBancaria:
def __init__(self, numero_cuenta, saldo):
self.__numero_cuenta = numero_cuenta # Atributo privado
self.__saldo = saldo # Atributo privado
def depositar(self, cantidad):
if cantidad > 0:
self.__saldo += cantidad
return True
return False
def retirar(self, cantidad):
if 0 < cantidad <= self.__saldo:
self.__saldo -= cantidad
return True
return False
def obtener_saldo(self):
return self.__saldo
# Uso
cuenta = CuentaBancaria("1234567890", 1000)
print(cuenta.obtener_saldo()) # Salida: 1000
cuenta.depositar(500)
print(cuenta.obtener_saldo()) # Salida: 1500
cuenta.retirar(200)
print(cuenta.obtener_saldo()) # Salida: 1300
# Esto generará un AttributeError
# print(cuenta.__saldo)
En este ejemplo:
__numero_cuenta
y__saldo
son atributos privados.- Proporcionamos métodos públicos (
depositar
,retirar
,obtener_saldo
) para interactuar con estos atributos privados. - El acceso directo a
__saldo
desde fuera de la clase generará una excepciónAttributeError
.
2. Uso de propiedades
El decorador @property
de Python nos permite definir métodos que pueden ser accedidos como atributos, proporcionando una forma más pythonica de implementar getters y setters.
class Circulo:
def __init__(self, radio):
self._radio = radio
@property
def radio(self):
return self._radio
@radio.setter
def radio(self, valor):
if valor > 0:
self._radio = valor
else:
raise ValueError("El radio debe ser positivo")
@property
def area(self):
return 3.14159 * self._radio ** 2
# Uso
circulo = Circulo(5)
print(circulo.radio) # Salida: 5
print(circulo.area) # Salida: 78.53975
circulo.radio = 7
print(circulo.radio) # Salida: 7
print(circulo.area) # Salida: 153.93791
# Esto generará un ValueError
# circulo.radio = -1
En este ejemplo:
_radio
es un atributo protegido (el guion bajo simple es una convención para atributos protegidos en Python).- La propiedad
radio
proporciona acceso de lectura y escritura a_radio
con validación. - La propiedad
area
es de solo lectura y se calcula al vuelo.
Beneficios y mejores prácticas
Los beneficios del encapsulamiento son numerosos:
- Mejora de la mantenibilidad: Los cambios en la implementación interna no afectan al código externo que utiliza la clase.
- Mayor seguridad: Los atributos privados no pueden ser modificados accidentalmente desde fuera de la clase.
- Flexibilidad en la implementación: Puedes cambiar cómo se almacenan o calculan los datos sin cambiar la interfaz pública.
- Mejor abstracción: Los usuarios de la clase no necesitan conocer su funcionamiento interno.
Las mejores prácticas para el encapsulamiento en Python incluyen:
- Usar atributos privados (prefijo de doble guion bajo) para datos que no deben ser accedidos directamente desde fuera de la clase.
- Proporcionar métodos públicos o propiedades para el acceso controlado a los datos internos.
- Usar propiedades en lugar de métodos get/set para un enfoque más propio del paradigma Python.
- Documentar claramente la interfaz pública, incluyendo cualquier efecto secundario de los métodos.
Veamos un ejemplo más complejo que demuestra estas prácticas:
class Empleado:
def __init__(self, nombre, salario):
self.__nombre = nombre
self.__salario = salario
self.__proyectos = []
@property
def nombre(self):
return self.__nombre
@property
def salario(self):
return self.__salario
@salario.setter
def salario(self, valor):
if valor > 0:
self.__salario = valor
else:
raise ValueError("El salario debe ser positivo")
def agregar_proyecto(self, proyecto):
"""
Agrega un proyecto a la lista de proyectos del empleado.
:param proyecto: cadena que representa el nombre del proyecto
"""
self.__proyectos.append(proyecto)
def eliminar_proyecto(self, proyecto):
"""
Elimina un proyecto de la lista de proyectos del empleado.
:param proyecto: cadena que representa el nombre del proyecto
:return: True si el proyecto fue eliminado, False si no se encontró
"""
if proyecto in self.__proyectos:
self.__proyectos.remove(proyecto)
return True
return False
@property
def cantidad_proyectos(self):
return len(self.__proyectos)
def __str__(self):
return f"Empleado: {self.__nombre}, Salario: ${self.__salario}, Proyectos: {self.cantidad_proyectos}"
# Uso
emp = Empleado("Juan Pérez", 50000)
print(emp.nombre) # Salida: Juan Pérez
print(emp.salario) # Salida: 50000
emp.agregar_proyecto("Proyecto A")
emp.agregar_proyecto("Proyecto B")
print(emp.cantidad_proyectos) # Salida: 2
emp.salario = 55000
print(emp) # Salida: Empleado: Juan Pérez, Salario: $55000, Proyectos: 2
emp.eliminar_proyecto("Proyecto A")
print(emp.cantidad_proyectos) # Salida: 1
# Esto generará un AttributeError
# print(emp.__proyectos)
Este ejemplo demuestra:
- Atributos privados (
__nombre
,__salario
,__proyectos
) - Propiedades para acceso controlado (
nombre
,salario
,cantidad_proyectos
) - Métodos públicos para manipular datos privados (
agregar_proyecto
,eliminar_proyecto
) - Documentación clara del comportamiento de los métodos
- Un método
__str__
personalizado para una representación de cadena agradable del objeto
Siguiendo estas prácticas, creamos una clase que es flexible y robusta, encarnando el principio de encapsulamiento.
Referencias
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
3.2.2 - Herencia
Los aspectos clave de la herencia incluyen:
- Reutilización de código: La herencia permite reutilizar código de clases existentes, reduciendo la redundancia y promoviendo un desarrollo eficiente.
- Clasificación jerárquica: Permite la creación de jerarquías de clases, representando relaciones y características comunes entre objetos.
- Extensibilidad: Se puede agregar nueva funcionalidad a las clases existentes sin modificarlas, siguiendo el principio abierto-cerrado.
- Polimorfismo: La herencia es un prerrequisito para el polimorfismo en tiempo de ejecución (que discutiremos en detalle más adelante).
Tipos de herencia
Existen varios tipos de herencia, aunque no todos los lenguajes de programación admiten todos los tipos. Los principales tipos son:
- Herencia simple: Una clase derivada hereda de una sola clase base.
- Herencia múltiple: Una clase derivada hereda de múltiples clases base.
- Herencia multinivel: Una clase derivada hereda de otra clase derivada.
- Herencia jerárquica: Múltiples clases derivadas heredan de una sola clase base.
- Herencia híbrida: Una combinación de dos o más tipos de herencia.
Python admite todos estos tipos de herencia. Exploremos cada uno con ejemplos.
Herencia simple
La herencia simple es la forma más básica de herencia, donde una clase hereda de una sola clase base.
class Animal:
def __init__(self, especie):
self.especie = especie
def hacer_sonido(self):
pass
class Perro(Animal):
def __init__(self, nombre):
super().__init__("Canino")
self.nombre = nombre
def hacer_sonido(self):
return "¡Guau!"
# Uso
perro = Perro("Buddy")
print(f"{perro.nombre} es un {perro.especie}") # Salida: Buddy es un Canino
print(perro.hacer_sonido()) # Salida: ¡Guau!
En este ejemplo:
Animal
es la clase base con un método genéricohacer_sonido
.Perro
es derivado deAnimal
, heredando sus atributos y métodos.Perro
sobrescribe el métodohacer_sonido
con su propia implementación.- Usamos
super().__init__()
para llamar al inicializador de la clase base.
Herencia múltiple
La herencia múltiple permite que una clase herede de múltiples clases base. Algunos lenguajes no permiten este tipo de herencia.
class Volador:
def volar(self):
return "¡Puedo volar!"
class Nadador:
def nadar(self):
return "¡Puedo nadar!"
class Pato(Animal, Volador, Nadador):
def __init__(self, nombre):
Animal.__init__(self, "Ave")
self.nombre = nombre
def hacer_sonido(self):
return "¡Cuac!"
# Uso
pato = Pato("Donald")
print(f"{pato.nombre} es un {pato.especie}") # Salida: Donald es un Ave
print(pato.hacer_sonido()) # Salida: ¡Cuac!
print(pato.volar()) # Salida: ¡Puedo volar!
print(pato.nadar()) # Salida: ¡Puedo nadar!
Aquí, Pato
hereda de Animal
, Volador
y Nadador
, combinando atributos y métodos de las tres clases.
Herencia multinivel
En la herencia multinivel, una clase derivada hereda de otra clase derivada.
class Mamifero(Animal):
def __init__(self, especie, es_de_sangre_caliente=True):
super().__init__(especie)
self.es_de_sangre_caliente = es_de_sangre_caliente
def dar_a_luz(self):
return "Dando a luz crías vivas"
class Gato(Mamifero):
def __init__(self, nombre):
super().__init__("Felino")
self.nombre = nombre
def hacer_sonido(self):
return "¡Miau!"
# Uso
gato = Gato("Bigotes")
print(f"{gato.nombre} es un {gato.especie}") # Salida: Bigotes es un Felino
print(gato.hacer_sonido()) # Salida: ¡Miau!
print(gato.dar_a_luz()) # Salida: Dando a luz crías vivas
print(f"¿Es de sangre caliente? {gato.es_de_sangre_caliente}") # Salida: ¿Es de sangre caliente? True
En este ejemplo, Gato
hereda de Mamifero
, que a su vez hereda de Animal
, formando una cadena de herencia multinivel.
Herencia jerárquica
La herencia jerárquica implica múltiples clases derivadas heredando de una sola clase base.
class Ave(Animal):
def __init__(self, especie, puede_volar=True):
super().__init__(especie)
self.puede_volar = puede_volar
class Loro(Ave):
def __init__(self, nombre):
super().__init__("Psitácido", puede_volar=True)
self.nombre = nombre
def hacer_sonido(self):
return "¡Squawk!"
class Pinguino(Ave):
def __init__(self, nombre):
super().__init__("Esfenisciforme", puede_volar=False)
self.nombre = nombre
def hacer_sonido(self):
return "¡Honk!"
# Uso
loro = Loro("Polly")
pinguino = Pinguino("Pingu")
print(f"{loro.nombre} puede volar: {loro.puede_volar}") # Salida: Polly puede volar: True
print(f"{pinguino.nombre} puede volar: {pinguino.puede_volar}") # Salida: Pingu puede volar: False
Aquí, tanto Loro
como Pinguino
heredan de Ave
, lo que demuestra la herencia jerárquica.
Herencia híbrida
La herencia híbrida es una combinación de múltiples tipos de herencia. Veamos un ejemplo más complejo para ilustrar esto:
class Terrestre:
def caminar(self):
return "Caminando en tierra"
class Acuatico:
def nadar(self):
return "Nadando en el agua"
class Anfibio(Animal, Terrestre, Acuatico):
def __init__(self, especie):
Animal.__init__(self, especie)
def adaptarse(self):
return "Puede sobrevivir tanto en tierra como en agua"
class Rana(Anfibio):
def __init__(self, nombre):
super().__init__("Anuro")
self.nombre = nombre
def hacer_sonido(self):
return "¡Croac!"
# Uso
rana = Rana("Kermit")
print(f"{rana.nombre} es un {rana.especie}") # Salida: Kermit es un Anuro
print(rana.hacer_sonido()) # Salida: ¡Croac!
print(rana.caminar()) # Salida: Caminando en tierra
print(rana.nadar()) # Salida: Nadando en el agua
print(rana.adaptarse()) # Salida: Puede sobrevivir tanto en tierra como en agua
Este ejemplo demuestra la herencia híbrida:
Rana
hereda deAnfibio
Anfibio
hereda deAnimal
,Terrestre
, yAcuatico
- Esto crea una combinación de herencia multinivel y múltiple
Consideraciones
La herencia ofrece varias ventajas. Sin embargo, también hay consideraciones importantes:
- Complejidad: Las jerarquías de herencia profundas pueden volverse difíciles de entender y mantener.
- Acoplamiento fuerte: La herencia crea un acoplamiento fuerte entre las clases base y derivadas.
- Problema de la clase base frágil: Los cambios en la clase base pueden afectar inesperadamente a las clases derivadas.
- Problema del diamante: En la herencia múltiple, pueden surgir conflictos si dos clases base tienen métodos con el mismo nombre.
Para abordar estas consideraciones:
- Prefiere la composición sobre la herencia cuando sea posible.
- Mantén las jerarquías de herencia poco profundas y enfocadas.
- Utiliza clases base abstractas para definir interfaces claras.
- Ten cuidado con la herencia múltiple y resuelve los conflictos explícitamente.
Visualicemos las relaciones de herencia que hemos discutido utilizando un diagrama de clases UML:
classDiagram Animal <|-- Mamífero Animal <|-- Ave Mamífero <|-- Perro Mamífero <|-- Gato Ave <|-- Loro Ave <|-- Pingüino Animal <|-- Anfibio Terrestre <|-- Anfibio Acuático <|-- Anfibio Anfibio <|-- Rana class Animal { +especie: str +hacer_sonido() } class Mamífero { +es_de_sangre_caliente: bool +dar_a_luz() } class Ave { +puede_volar: bool } class Anfibio { +adaptarse() } class Terrestre { +caminar() } class Acuático { +nadar() }
Este diagrama ilustra las relaciones de herencia entre las clases que hemos discutido, mostrando tanto la herencia simple como la múltiple.
Referencias
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
3.2.3 - Polimorfismo
El polimorfismo permite escribir código flexible y reutilizable al permitirnos trabajar con objetos a un nivel más abstracto, sin necesidad de conocer sus tipos específicos.
Existen dos tipos principales de polimorfismo en la programación orientada a objetos:
Polimorfismo en tiempo de compilación (Polimorfismo estático)
- Se logra a través de la sobrecarga de métodos.
- Se resuelve en tiempo de compilación.
Polimorfismo en tiempo de ejecución (Polimorfismo dinámico)
- Se logra a través de la sobrescritura de métodos.
- Se resuelve en tiempo de ejecución.
Python admite principalmente el polimorfismo en tiempo de ejecución, ya que es un lenguaje de tipado dinámico. Sin embargo, podemos demostrar conceptos similares al polimorfismo en tiempo de compilación también.
Exploremos diferentes aspectos del polimorfismo en Python:
Duck typing
Python utiliza el duck typing, que es una forma de polimorfismo. La idea es: “Si camina como un pato y grazna como un pato, entonces debe ser un pato”. En otras palabras, Python se preocupa más por los métodos que tiene un objeto que por el tipo del objeto en sí.
class Pato:
def hablar(self):
return "¡Cuac cuac!"
class Perro:
def hablar(self):
return "¡Guau guau!"
class Gato:
def hablar(self):
return "¡Miau miau!"
def sonido_animal(animal):
return animal.hablar()
# Uso
pato = Pato()
perro = Perro()
gato = Gato()
print(sonido_animal(pato)) # Salida: ¡Cuac cuac!
print(sonido_animal(perro)) # Salida: ¡Guau guau!
print(sonido_animal(gato)) # Salida: ¡Miau miau!
En este ejemplo, sonido_animal()
funciona con cualquier objeto que tenga un método hablar()
, independientemente de su clase.
Sobrescritura de métodos
La sobrescritura de métodos es un aspecto clave del polimorfismo en tiempo de ejecución. Ocurre cuando una clase derivada define un método con el mismo nombre que un método en su clase base.
class Figura:
def area(self):
pass
class Rectangulo(Figura):
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
def area(self):
return self.ancho * self.alto
class Circulo(Figura):
def __init__(self, radio):
self.radio = radio
def area(self):
return 3.14159 * self.radio ** 2
# Uso
figuras = [Rectangulo(5, 4), Circulo(3)]
for figura in figuras:
print(f"Área: {figura.area()}")
# Salida:
# Área: 20
# Área: 28.27431
Aquí, Rectangulo
y Circulo
sobrescriben el método area()
de la clase Figura
.
Sobrecarga de operadores
Python permite la sobrecarga de operadores, que es una forma de polimorfismo en tiempo de compilación. Permite que el mismo operador tenga diferentes significados según los operandos.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, otro):
return Vector(self.x + otro.x, self.y + otro.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
# Uso
v1 = Vector(2, 3)
v2 = Vector(3, 4)
v3 = v1 + v2
print(v3) # Salida: Vector(5, 7)
Aquí, hemos sobrecargado el operador +
para nuestra clase Vector
.
Clases base abstractas
El módulo abc
de Python proporciona infraestructura para definir clases base abstractas, que son una forma poderosa de definir interfaces en Python.
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def hacer_sonido(self):
pass
class Perro(Animal):
def hacer_sonido(self):
return "¡Guau!"
class Gato(Animal):
def hacer_sonido(self):
return "¡Miau!"
# Uso
def sonido_animal(animal):
return animal.hacer_sonido()
perro = Perro()
gato = Gato()
print(sonido_animal(perro)) # Salida: ¡Guau!
print(sonido_animal(gato)) # Salida: ¡Miau!
# Esto generará un TypeError
# animal = Animal()
Las clases base abstractas no pueden ser instanciadas y obligan a las clases derivadas a implementar ciertos métodos, asegurando una interfaz consistente.
Aplicaciones en el mundo real
El polimorfismo se utiliza ampliamente en aplicaciones del mundo real:
- Frameworks de GUI: Diferentes widgets (botones, cajas de texto) pueden responder a eventos comunes (clic, hover) de sus propias maneras.
- Interfaces de bases de datos: Diferentes sistemas de bases de datos pueden implementar una interfaz común para consultas, permitiendo que las aplicaciones trabajen con varias bases de datos sin cambiar el código.
- Sistemas de plugins: Las aplicaciones pueden trabajar con plugins a través de una interfaz común, independientemente de la implementación específica de cada plugin.
- Desarrollo de juegos: Diferentes entidades del juego pueden compartir comportamientos comunes (mover, colisionar) pero implementarlos de manera diferente.
Aquí hay un ejemplo simple de un sistema de plugins:
class Plugin(ABC):
@abstractmethod
def procesar(self, datos):
pass
class PluginMayusculas(Plugin):
def procesar(self, datos):
return datos.upper()
class PluginInvertir(Plugin):
def procesar(self, datos):
return datos[::-1]
class Aplicacion:
def __init__(self):
self.plugins = []
def agregar_plugin(self, plugin):
self.plugins.append(plugin)
def procesar_datos(self, datos):
for plugin in self.plugins:
datos = plugin.procesar(datos)
return datos
# Uso
app = Aplicacion()
app.agregar_plugin(PluginMayusculas())
app.agregar_plugin(PluginInvertir())
resultado = app.procesar_datos("Hola, Mundo!")
print(resultado) # Salida: !ODNUM ,ALOH
Este ejemplo demuestra cómo el polimorfismo permite que la clase Aplicacion
trabaje con diferentes plugins a través de una interfaz común.
Referencias
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
3.2.4 - Abstracción
Los aspectos clave de la abstracción incluyen:
- Simplificación: La abstracción reduce la complejidad ocultando detalles innecesarios.
- Enfoque en características esenciales: Enfatiza lo que hace un objeto en lugar de cómo lo hace.
- Separación de preocupaciones: Permite separar la interfaz de una clase de su implementación.
- Modularidad: La abstracción promueve el diseño modular al definir límites claros entre componentes.
Clases abstractas e interfaces
En muchos lenguajes orientados a objetos, la abstracción se implementa a través de clases abstractas e interfaces. Aunque Python no tiene un concepto integrado de interfaz, podemos lograr una funcionalidad similar usando clases base abstractas. El módulo abc
de Python proporciona infraestructura para definir clases base abstractas:
from abc import ABC, abstractmethod
class Figura(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimetro(self):
pass
class Rectangulo(Figura):
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
def area(self):
return self.ancho * self.alto
def perimetro(self):
return 2 * (self.ancho + self.alto)
class Circulo(Figura):
def __init__(self, radio):
self.radio = radio
def area(self):
return 3.14159 * self.radio ** 2
def perimetro(self):
return 2 * 3.14159 * self.radio
# Uso
# figuras = [Figura()] # Esto generaría TypeError
figuras = [Rectangulo(5, 4), Circulo(3)]
for figura in figuras:
print(f"Área: {figura.area()}, Perímetro: {figura.perimetro()}")
# Salida:
# Área: 20, Perímetro: 18
# Área: 28.27431, Perímetro: 18.84954
En este ejemplo:
Figura
es una clase base abstracta que define la interfaz para todas las figuras.Rectangulo
yCirculo
son clases concretas que implementan la interfazFigura
.- No podemos instanciar
Figura
directamente, pero podemos usarla como un tipo común para todas las figuras.
Implementando abstracción en Python
Aunque Python proporciona clases base abstractas para definir interfaces formalmente, también podemos lograr la abstracción mediante convenciones y documentación. Veamos un ejemplo sin utilizar ABC
:
class BaseDeDatos:
def conectar(self):
raise NotImplementedError("La subclase debe implementar este método abstracto")
def ejecutar(self, consulta):
raise NotImplementedError("La subclase debe implementar este método abstracto")
class BaseDeDatosMySQL(BaseDeDatos):
def conectar(self):
print("Conectando a la base de datos MySQL...")
def ejecutar(self, consulta):
print(f"Ejecutando consulta MySQL: {consulta}")
class BaseDeDatosPostgreSQL(BaseDeDatos):
def conectar(self):
print("Conectando a la base de datos PostgreSQL...")
def ejecutar(self, consulta):
print(f"Ejecutando consulta PostgreSQL: {consulta}")
def realizar_operacion_en_bd(base_de_datos):
base_de_datos.conectar()
base_de_datos.ejecutar("SELECT * FROM usuarios")
# Uso
bd_mysql = BaseDeDatosMySQL()
bd_postgres = BaseDeDatosPostgreSQL()
realizar_operacion_en_bd(bd_mysql)
realizar_operacion_en_bd(bd_postgres)
# Salida:
# Conectando a la base de datos MySQL...
# Ejecutando consulta MySQL: SELECT * FROM usuarios
# Conectando a la base de datos PostgreSQL...
# Ejecutando consulta PostgreSQL: SELECT * FROM usuarios
En este ejemplo:
BaseDeDatos
es una clase base abstracta (aunque no usaABC
) que define la interfaz para todos los tipos de bases de datos.BaseDeDatosMySQL
yBaseDeDatosPostgreSQL
son implementaciones concretas.realizar_operacion_en_bd
trabaja con cualquier objeto que cumpla con la interfaz deBaseDeDatos
.
Principios de diseño y patrones
La abstracción es un componente clave de varios principios y patrones de diseño importantes:
Principios SOLID:
- Principio de Responsabilidad Única (SRP).
- Principio de Abierto/Cerrado (OCP).
- Principio de Sustitución de Liskov (LSP).
- Principio de Segregación de Interfaces (ISP).
- Principio de Inversión de Dependencias (DIP).
Patrones de Diseño:
- Patrón método de fábrica.
- Patrón fábrica abstracta.
- Patrón estrategia.
- Patrón método plantilla.
Veamos una implementación del patrón estrategia:
from abc import ABC, abstractmethod
class EstrategiaOrdenamiento(ABC):
@abstractmethod
def ordenar(self, datos):
pass
class OrdenamientoBurbuja(EstrategiaOrdenamiento):
def ordenar(self, datos):
print("Realizando ordenamiento de burbuja")
return sorted(datos) # Usamos sorted() de Python por simplicidad
class OrdenamientoRapido(EstrategiaOrdenamiento):
def ordenar(self, datos):
print("Realizando ordenamiento rápido")
return sorted(datos) # Usamos sorted() de Python por simplicidad
class Ordenador:
def __init__(self, estrategia):
self.estrategia = estrategia
def ordenar(self, datos):
return self.estrategia.ordenar(datos)
# Uso
datos = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
ordenador_burbuja = Ordenador(OrdenamientoBurbuja())
print(ordenador_burbuja.ordenar(datos))
ordenador_rapido = Ordenador(OrdenamientoRapido())
print(ordenador_rapido.ordenar(datos))
# Salida:
# Realizando ordenamiento de burbuja
# [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
# Realizando ordenamiento rápido
# [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
Este ejemplo del Patrón Estrategia muestra cómo la abstracción nos permite definir una familia de algoritmos, encapsular cada uno y hacerlos intercambiables. La clase Ordenador
no necesita conocer los detalles de cómo funciona cada algoritmo de ordenamiento; solo sabe que puede llamar al método ordenar
en cualquier objeto EstrategiaOrdenamiento
.
Referencias
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
3.2.5 - Conclusión
- El encapsulamiento nos permite agrupar datos y métodos, ocultando detalles internos y protegiendo la integridad de los datos.
- La herencia permite la reutilización de código y la creación de relaciones jerárquicas entre clases.
- El polimorfismo proporciona una forma de usar objetos de diferentes tipos a través de una interfaz común, mejorando la flexibilidad y extensibilidad.
- La abstracción nos permite crear modelos simplificados de sistemas complejos, enfocándonos en las características esenciales y ocultando los detalles innecesarios.
A medida que continúes tu viaje en el desarrollo de software, descubrirás que dominar estos conceptos abre nuevas formas de pensar y resolver problemas. Recuerda que la POO no se trata solo de sintaxis o características del lenguaje; es una mentalidad para modelar sistemas complejos y gestionar la complejidad en el software.
Referencias
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
4 - Estructuras de Datos
Las estructuras de datos son formas de organizar y almacenar información en un programa de computadora para que pueda ser accedida y modificada de manera eficiente. Como programadores, es esencial entender las distintas estructuras de datos disponibles y saber cuándo aplicar cada una para optimizar el rendimiento y la utilidad de nuestros programas.
Una estructura de datos es una forma particular de organizar datos en la memoria de la computadora para que puedan ser usados de manera eficiente. Las estructuras de datos vienen en muchas formas, como arrays, listas, pilas, colas, grafos, árboles, hashes, etc.
Cada estructura organiza los datos de acuerdo a un modelo lógico específico y soporta operaciones eficientes para acceder, modificar, agregar y borrar elementos según ese modelo. Por ejemplo, un array organiza los elementos de manera secuencial en memoria para facilitar el acceso aleatorio por índices. Una lista enlazada conecta elementos en memoria usando “nodos” con referencias al siguiente nodo para facilitar la inserción y eliminación.
Al elegir la estructura de datos apropiada para la tarea a resolver, podemos escribir programas más eficientes y reducir la complejidad computacional, utilizando menos recursos como memoria y procesamiento.
Las estructuras de datos nos ayudan a:
Organizar grandes cantidades de datos para que sean más fáciles de acceder y modificar.
Modelar relaciones complejas entre datos, como con grafos y árboles.
Acceder y modificar datos de manera eficiente, optimizando el rendimiento.
Reutilizar código y estructuras de datos existentes en lugar de tener que reescribir soluciones desde cero.
Por ejemplo, un programa que debe almacenar miles de registros de usuarios se beneficia usando una estructura de datos hash para asociar cada usuario a datos como nombre, apellido, email, etc. De esta manera se pueden encontrar usuarios específicos muy rápido sin tener que iterar sobre toda la colección.
Otro ejemplo son los árboles de búsqueda binaria, que permiten encontrar elementos muy rápido en conjuntos ordenados de millones de elementos. Esto se logra descartando mitades del árbol a medida que se busca el elemento deseado.
Tipos de estructuras de datos
Existen muchos tipos de estructuras de datos. A continuación, se presentan algunas categorías útiles para clasificarlas.
Según relación entre elementos
Lineales: Los elementos se organizan secuencialmente uno después del otro. Por ejemplo, arrays, listas, pilas, colas.
No lineales: Los elementos se organizan en una jerarquía o grafo. Este es el caso de los árboles y grafos.
Según tipo de elementos
Homogéneas: Almacenan un solo tipo de datos. Por ejemplo, arrays en un lenguaje como Java.
Heterogéneas: Permiten diferentes tipos de datos. Objetos, registros son ejemplos de esta clasificación.
Según modo de acceso
Acceso secuencial: Sólo se puede acceder a los elementos en orden secuencial. Por ejemplo, listas enlazadas.
Acceso directo: Se puede acceder a cualquier elemento directamente por su posición. En este grupo se encuentran los arrays.
Acceso asociativo: Se accede a elementos por una clave asociada. Aquí se encuentran los diccionarios, hashes.
Según su funcionalidad
Arrays: Acceso rápido a elementos por índice pero difícil insertar/eliminar.
Listas: Fácil insertar/eliminar en cualquier posición pero acceso secuencial lento.
Pilas: Acceso LIFO (último en entrar, primero en salir). Útil para deshacer/rehacer.
Colas: Acceso FIFO (primero en entrar, primero en salir). Útil para procesamiento de eventos.
Árboles: Permiten modelar relaciones jerárquicas como con directorios de archivos o dependencias de tareas.
Grafos: Permiten modelar redes de interconexión como mapas, relaciones sociales, etc.
Hashes / Diccionarios: Asocian elementos con claves únicas para acceso ultra rápido.
Esta clasificación no es exhaustiva pero da una idea de la diversidad de estructuras de datos y sus diferentes propiedades que nos permiten modelar problemas complejos de manera eficiente.
Ejemplo
Veamos un ejemplo en Python para ver cómo se crea y utiliza una estructura de datos. Supongamos que queremos representar una cola de impresión donde las impresiones se procesan en orden de llegada (FIFO).
Primero, definimos una clase PrintQueue
para representar nuestra cola:
class PrintQueue:
def __init__(self):
self.items = []
def enqueue(self, item):
self.items.append(item)
def dequeue(self):
return self.items.pop(0)
def is_empty(self):
return len(self.items) == 0
Luego la utilizamos para agregar impresiones y procesarlas en orden:
print_queue = PrintQueue()
print_queue.enqueue("documento1.doc")
print_queue.enqueue("imagen3.jpg")
print_queue.enqueue("presentacion.ppt")
while not print_queue.is_empty():
next_item = print_queue.dequeue()
print(f"Imprimiendo {next_item}")
Esto imprimirá:
Imprimiendo documento1.doc
Imprimiendo imagen3.jpg
Imprimiendo presentacion.ppt
Con una estructura de datos como la cola implementamos la lógica FIFO de una forma sencilla y reutilizable. ¡Y esto es sólo una muestra, las posibilidades son infinitas!
Conclusión
Las estructuras de datos son herramientas fundamentales en programación que nos permiten organizar información de forma óptima para resolver problemas complejos. Conocer los distintos tipos de estructuras disponibles, como arrays, listas, pilas, colas, hashes, grafos y árboles, nos permite construir programas más eficientes. ¡Espero que esta introducción te haya dado algunos conocimientos y herramientas para comenzar a dominar este apasionante tema!
4.1 - Arreglos
Un array es una estructura de datos que representa un conjunto de elementos, los cuales se acceden a través de índices numéricos contiguos que van desde 0 hasta el tamaño del array menos 1. Los arrays proveen acceso rápido y directo a los elementos en base a su posición.
En lenguajes como Python y Ruby, los arrays se conocen como ’listas’ (lists). En Javascript se les conoce como ‘arreglos’ (arrays).
Los arrays son típicamente homogéneos, almacenando elementos del mismo tipo como enteros, cadenas, etc. Algunos lenguajes permiten arrays heterogéneos con valores de distintos tipos.
Creación de arrays
La manera de crear arrays varía según el lenguaje de programación:
MI_ARRAY = ["A", "B", "C"] # array literal
mi_array = list(range(5)) # array a partir de rango
Al crear un array literal se inicializan sus elementos directamente. Al construir un array vacío se especifica su tamaño pero sus elementos son inicializados con un valor default (0 para números, null para objetos, etc).
Acceder y modificar elementos
Los elementos individuales se acceden rápidamente por su índice utilizando corchetes []
:
my_array = ['a', 'b', 'c']
print(my_array[0]) # 'a'
print(my_array[2]) # 'c'
my_array[2] = 'z'
print(my_array[2]) # 'z'
Los índices comienzan en 0, por lo que en un array de tamaño N, los índices válidos están entre 0 y N-1.
Acceder a un índice inválido causa un error, por ejemplo, acceder al índice 3 en un array de tamaño 3. Esto se conoce como “index out of bounds”.
Recorrer un array
Podemos recorrer todos los elementos usando un ciclo for
:
letras = ['a', 'b', 'c']
for i in range(len(letras)):
print(letras[i])
Esto imprime cada elemento en orden. len()
devuelve la longitud total del array.
Otra forma es iterando directamente sobre los elementos:
letras = ['a', 'b', 'c']
for letra in letras:
print(letra)
Buscar en un array
Podemos buscar un elemento en un array mediante un ciclo y comparando elemento por elemento:
letras = ['a', 'b', 'c']
def buscar_en_array(array, elemento):
for i in range(len(array)):
if array[i] == elemento:
return i
return False
print(buscar_en_array(letras, 'b')) # 1
print(buscar_en_array(letras, 'z')) # False
Devuelve el índice si se encuentra o False
si no se encuentra.
Array multidimensional
Los arrays pueden tener más de una dimensión, por ejemplo matrices 2D, cubos 3D, etc.
Un array 2D se puede ver como una tabla con filas y columnas. Para acceder a un elemento se especifican dos índices, uno para la fila y otro para la columna:
matrix = [
[1, 2, 3],
[4, 5, 6]
]
print(matrix[0][2]) # 3
print(matrix[1][0]) # 4
Pueden tener más dimensiones, por ejemplo un array 3D para representar pixeles en una imagen.
Conclusión
Los arrays son estructuras de datos fundamentales en programación que proveen un acceso eficiente a elementos en memoria mediante índices numéricos. Tener un buen dominio de arrays, matrices y sus usos es indispensable para cualquier programador.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
4.2 - Mapas (Diccionarios)
Un diccionario, o mapa, consiste en una colección de pares clave-valor. La clave se utiliza para acceder al valor asociado. Las claves deben ser únicas dentro de un diccionario. Los valores pueden repetirse.
Operaciones principales
- Añadir/actualizar: Inserta un par clave-valor. Si la clave existía, su valor es reemplazado.
diccionario['clave'] = 'valor'
- Obtener valor: Acceder al valor dada una clave.
valor = diccionario['clave']
- Eliminar: Remueve un par clave-valor del diccionario.
del diccionario['clave']
- Recorrer: Iterar sobre las claves, valores o pares del diccionario.
for clave in diccionario: print(clave, diccionario[clave]) # clave, valor
Creación de un diccionario o mapa
La sintaxis para crear mapas o diccionarios en Python es la siguiente:
diccionario_vacio = {}
persona = {
'nombre': 'Juan',
'edad': 25
}
Ejemplos de uso
Los diccionarios son útiles en muchos casos. A continuación se mencionan algunos de ellos.
Objetos y mapeos
Podemos modelar objetos y entidades con atributos clave-valor:
producto = {
'nombre': 'Smartphone',
'precio': 500,
'marca': 'XYZ'
}
Conteos y frecuencias
Contar ocurrencias de elementos en secuencias:
texto = "Hola mundo mundo"
frecuencias = {}
for palabra in texto.split():
if palabra in frecuencias:
frecuencias[palabra] += 1
else:
frecuencias[palabra] = 1
print(frecuencias)
# {'Hola': 1, 'mundo': 2}
Almacenar y acceder a datos
Como alternativa de alta performance a lists y arrays.
Conclusión
Los diccionarios son estructuras de datos versátiles gracias a su rápido acceso basado en claves únicas. Tienen usos en casi todos los programas, por lo que dominar diccionarios es indispensable en cualquier lenguaje.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
4.3 - Listas enlazadas
Una lista enlazada se compone de nodos
donde cada nodo tiene dos partes:
- Dato o información
- Referencia al siguiente nodo
Los nodos se organizan de forma secuencial, cada uno apuntando al siguiente. El último nodo apunta a nulo para indicar el final.
Esta estructura dinámica permite inserción y eliminación eficiente de nodos.
Tipos de listas enlazadas
Existen varios tipos:
- Simplemente enlazada: Cada nodo apunta al siguiente. Son útiles para colas (queues) y pilas (stacks).
- Doblemente enlazada: Cada nodo tiene referencia al siguiente y al anterior. Permiten recorrer en ambos sentidos.
- Circular: El último nodo apunta al primero formando un ciclo. Útiles para buffers circulares.
Operaciones comunes
Insertar: Agregar nodos al inicio, final o medio de la lista.
Eliminar: Quitar nodos por valor o posición.
Buscar: Encontrar nodos por valor recorriendo la lista.
Recorrer: Iterar los nodos accediendo por las referencias.
Implementación
Las listas enlazadas se pueden implementar de la siguiente manera:
Usa la clase ListNode
para representar nodos:
class ListNode:
def __init__(self, value):
self.value = value
self.next = None
Luego para crear y usar una lista se define una clase LinkedList con métodos para las operaciones.
class LinkedList:
def __init__(self):
self.head = None
def add_to_start(self, new_value):
new_node = ListNode(new_value)
new_node.next = self.head
self.head = new_node
def print(self):
current = self.head
while current != None:
print(current.value)
current = current.next
def search(self, searched_value):
current = self.head
while current != None:
if current.value == searched_value:
return True
current = current.next
return False
#...other methods
Con esta clase LinkedList
podemos crear una lista, agregar nodos, imprimirla, buscar elementos, etc.
Se podrían agregar otros métodos como insertar al final, eliminar por valor, obtener por índice, etc. Pero esto da una idea de cómo encapsular la funcionalidad de la lista enlazada en una clase. Como práctica, podés intentar agregar estos métodos por tu cuenta, ¡no dudes en dejar tus comentarios y contactarte si necesitas ayuda!
Ventajas y desventajas
Ventajas:
- Insertar y eliminar nodos es eficiente.
- No requiere definir tamaño fijo como los arrays.
- Estructura dinámica y flexible.
Desventajas:
- Mayor uso de memoria por tener que almacenar referencias.
- El acceso a elementos por índice es más costoso al ser secuencial.
Ejemplos de uso
- Implementar estructuras como pilas (stacks) y colas (queues).
- En listas doblemente enlazadas, recorrer la lista en el sentido ambos sentidos.
- Blockchains como la de Bitcoin.
- Reproducir elementos en orden como playlists de música.
Conclusión
Las listas enlazadas son una estructura de datos versátil para almacenar secuencias dinámicas de elementos. Tener un buen manejo de estas listas, sus operaciones y usos es indispensable para cualquier programador.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
4.4 - Pilas
La naturaleza LIFO de las pilas se debe a que sólo se puede acceder y manipular el elemento superior. La operación de colocar un elemento sobre la pila se conoce como “push”, mientras que sacar un elemento de la pila es un “pop”. El funcionamiento LIFO provoca que el último elemento colocado en una pila sea el primero en ser retirado.
Operaciones principales
Las operaciones primarias que soporta una estructura de pila son:
- Push: agrega un elemento encima de la pila.
- Pop: saca el elemento de la pila que se encuentra en la cima.
- Peek: permite acceder al elemento de la cima sin sacarlo de la pila.
- isEmpty: consulta si la pila se encuentra vacía.
La mayoría de los lenguajes como Python y Java proveen implementaciones de pilas en sus librerías estándar.
Implementación
Una pila puede implementarse utilizando una lista enlazada de manera que cada node apunte al nodo anterior.
class Node:
def __init__(self, value):
self.value = value
self.previous = None
class Stack:
def __init__(self):
self.top = None
self.size = 0
def push(self, value):
new_node = Node(value)
if self.top is None:
self.top = new_node
else:
new_node.previous = self.top
self.top = new_node
self.size += 1
def pop(self):
if self.top is None:
return None
top_node = self.top
self.top = self.top.previous
self.size -= 1
return top_node.value
def peek(self):
if self.top is None:
return None
return self.top.value
def is_empty(self):
return self.top is None # Returns true if top is None
def __len__(self):
return self.size
def __str__(self):
values = []
current = self.top
while current:
values.append(str(current.value))
current = current.previous
return "\n".join(values)
Ejemplos de uso
Las pilas tienen muchos usos en programación:
Pila de ejecución (call stack): registra las llamadas a funciones pendientes de resolver. Implementa el comportamiento LIFO esperado.
Pila de navegador: permite volver atrás (undo) en el historial de navegación de forma similar a una pila LIFO.
Ejecución de expresiones matemáticas: mediante una pila se puede verificar paréntesis, corchetes, llaves, etc.
Algoritmos y estructuras de datos: como en el algoritmo quicksort y en la implementación de buses de datos (datapaths).
Conclusión
Las pilas son estructuras de datos versátiles gracias a su principio de funcionamiento LIFO. Tener un buen dominio de pilas, sus usos y aplicaciones es esencial en la ciencia de la computación.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
4.5 - Colas
La naturaleza FIFO (first in, first out) de las colas se debe a que sólo se puede acceder y manipular el elemento inicial. Cuando se agrega un elemento a la cola se conoce como “enqueue”, mientras que eliminar un elemento se denomina “dequeue”.
Esto hace que el primer elemento en ser añadido a la cola también sea el primero en ser retirado, de ahí su comportamiento FIFO.
Operaciones principales
Las operaciones básicas de una cola son:
- Enqueue: Agrega un elemento al final de la cola.
- Dequeue: Saca el elemento del frente de la cola.
- Peek: Obtiene el elemento al frente sin sacarlo.
- isEmpty: Consulta si la cola está vacía.
Implementación
Al igual que las pilas, las colas se pueden implementar usando listas enlazadas. Se agrega al final y se saca del frente manteniendo referencias a ambos extremos.
class Node:
def __init__(self, value):
self.value = value
self.next = None
class Queue:
def __init__(self):
self.front = None
self.end = None
self.size = 0
def enqueue(self, value):
new_node = Node(value)
if self.end is None:
self.end = new_node
self.front = new_node
return
self.end.next = new_node
self.end = new_node
self.size += 1
def dequeue(self):
if self.is_empty():
return None
value = self.front.value
self.front = self.front.next
if self.front is None:
self.end = None
self.size -= 1
return value
def peek(self):
if self.is_empty():
return None
return self.front.value
def is_empty(self):
return self.front is None # Returns true if front is None
def __len__(self):
return self.size
def __str__(self):
values = []
current = self.front
while current:
values.append(str(current.value))
current = current.next
return "\n".join(values)
Ejemplos de uso
Algunos usos comunes de colas:
- Colas de impresión donde primero en entrar, primero en imprimir.
- Colas de tareas en sistemas operativos para orden de ejecución.
- Simulaciones donde se debe respetar orden de llegada como en bancos.
- Canales de mensajes como los de RabbitMQ o Kafka.
- Buffers circulares en audio para streaming.
Conclusión
Las colas son estructuras versátiles gracias a su principio FIFO. Tener un buen manejo de colas, implementación y aplicaciones reforzará tus habilidades como programador.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!