Introducción a la Programación Orientada a Objetos ================================================== La Programación Orientada a Objetos (OOP, por sus siglas en inglés) es un paradigma de programación que organiza el software en unidades llamadas "objetos". Estos objetos combinan tanto datos (atributos) como comportamiento (métodos), permitiendo una estructura de código más modular y manejable. La OOP se basa en cuatro principios fundamentales: encapsulamiento, abstracción, herencia y polimorfismo. **Encapsulamiento:** Protege los datos internos de un objeto y solo permite su modificación a través de métodos definidos. **Abstracción:** Permite trabajar con conceptos de alto nivel sin preocuparse por los detalles internos. **Herencia:** Facilita la creación de nuevas clases basadas en clases existentes, promoviendo la reutilización del código. **Polimorfismo:** Permite que diferentes objetos sean tratados como instancias de una misma clase base, simplificando el manejo de estructuras complejas. A lo largo de esta sección, utilizaremos ejemplos prácticos para demostrar las ventajas de la OOP. Veremos cómo este enfoque puede mejorar la organización, la reutilización y la escalabilidad del código en comparación con otros paradigmas de programación. Ejemplo - Centro de masas de un enjambre: ----------------------------------------- Usted está estudiando el comportamiento de un enjambre de N abejas. Cada abeja se mueve en línea recta a velocidad constante, y usted conoce la posición y velocidad de cada abeja para un tiempo t=0. Si todas las abejas tienen la misma masa, encuentre para que tiempo t=>0 el centro de masa del enjambre se encuentra lo mas cercano al origen. La posición y velocidad de cada abeja para t=0, se dará al programa como una lista de tuplas: .. code-block:: python :linenos: # Definimos un enjambre de partículas con sus posiciones y velocidades iniciales enjambre = [ ((0, 0, 0), (0, 0, 1)), ((0, 0, 0), (0, 0, -1)), ((0, 0, 0), (0, 1, 0)), ((0, 0, 0), (0, -1, 0)), ((0, 0, 0), (1, 0, 0)), ((0, 0, 0), (-1, 0, 0)), ] def centro_de_masa(enjambre, t): # Inicializamos las coordenadas del centro de masa cmx = 0 cmy = 0 cmz = 0 m = 1 # Suponemos que todas las partículas tienen la misma masa # Calculamos la posición de cada partícula en el tiempo t y acumulamos las coordenadas ponderadas for pos, vel in enjambre: xi, yi, zi = pos vx, vy, vz = vel # Calculamos la posición final de la partícula en el tiempo t xf = xi + vx * t yf = yi + vy * t zf = zi + vz * t # Acumulamos las posiciones finales ponderadas por la masa (aquí masa es 1) cmx += xf * m cmy += yf * m cmz += zf * m # Dividimos las coordenadas acumuladas por el número total de partículas para obtener el centro de masa num_particulas = len(enjambre) cmx /= num_particulas cmy /= num_particulas cmz /= num_particulas return cmx, cmy, cmz # Imprimimos el centro de masa del enjambre en diferentes tiempos t for t in range(10): print(f"Tiempo {t}: Centro de masa {centro_de_masa(enjambre, t)}") Este mismo programa, usando objetos se podría escribir como: .. code-block:: python :linenos: class Abeja: def __init__(self, pos, vel): self.posicion_inicial = pos self.velocidad = vel def posicion(self, t): xi, yi, zi = self.posicion_inicial vx, vy, vz = self.velocidad # Calculamos la posición final de la partícula en el tiempo t xf = xi + vx * t yf = yi + vy * t zf = zi + vz * t return xf, yf, zf class Enjambre: def __init__(self, datos_abejas): """ Inicializa un enjambre con una lista de abejas. :param datos_abejas: Lista de tuplas con datos de las abejas (pos, vel). """ # La siguiente linea es una forma abreviada de escribir: # # self.abejas = [] # for pos, vel in datos_abejas: # self.abejas.append(Abeja(pos, vel)) # # Esta forma abreviada se conoce como: # En inglés: list comprehensions # En español: comprensiones de listas self.abejas = [Abeja(pos, vel) for pos, vel in datos_abejas] def centro_de_masa(self, t): """ Calcula el centro de masa del enjambre en el tiempo t. :param t: Tiempo en el que se quiere calcular el centro de masa. :return: Tupla con las coordenadas del centro de masa (cmx, cmy, cmz). """ m = 1 # Suponemos que todas las partículas tienen la misma masa cmx, cmy, cmz = 0, 0, 0 for abeja in self.abejas: xf, yf, zf = abeja.posicion(t) cmx += xf * m cmy += yf * m cmz += zf * m num_particulas = len(self.abejas) cmx /= num_particulas cmy /= num_particulas cmz /= num_particulas return cmx, cmy, cmz # Definimos un enjambre de partículas con sus posiciones y velocidades iniciales datos_abejas = [ ((0, 0, 0), (0, 0, 1)), ((0, 0, 0), (0, 0, -1)), ((0, 0, 0), (0, 1, 0)), ((0, 0, 0), (0, -1, 0)), ((0, 0, 0), (1, 0, 0)), ((0, 0, 0), (-1, 0, 0)), ] # Creamos el enjambre con los datos iniciales primer_enjambre = Enjambre(datos_abejas) # Imprimimos el centro de masa del enjambre en diferentes tiempos t for t in range(10): print(f"Tiempo {t}: Centro de masa {primer_enjambre.centro_de_masa(t)}") Si bien este programa es más extenso, conceptualmente tiene la ventaja de que separa el comportamiento de las abejas y del enjambre. Para esto se definieron 2 clases nuevas: De la línea 1 a la ínea 15 se definió la clase Abeja, y de la línea 17 a la línea 59 se definió la clase Enjambre. Clase Abeja ~~~~~~~~~~~ .. code-block:: python :linenos: class Abeja: def __init__(self, pos, vel): self.posicion_inicial = pos self.velocidad = vel def posicion(self, t): xi, yi, zi = self.posicion_inicial vx, vy, vz = self.velocidad # Calculamos la posición final de la partícula en el tiempo t xf = xi + vx * t yf = yi + vy * t zf = zi + vz * t return xf, yf, zf Las clases se definen utilizando la palabra reservada ``class`` (línea 1), en la cual se indica el nombre de la clase ``Abeja``. Todo el código que se encuentra indentado a continuación, desde la línea 2 hasta la línea 15, es la definición de la clase. Esta clase en particular contiene dos métodos: el método ``__init__`` y el método ``posicion``. .. note:: Los métodos en Python deben siempre recibir como primer parámetro la variable ``self``, que representa la instancia de la clase desde la cual se está llamando al método. Los métodos son funciones internas de la clase que pueden acceder a las variables internas de la misma. Al definir una clase, se utiliza el nombre ``self`` para hacer referencia a los elementos internos de la clase. Por ejemplo, en las líneas 3 y 4 se están definiendo dos variables internas a la clase (``self.posicion_inicial`` y ``self.velocidad``) en las cuales se guardan datos que son accesibles por todos los métodos de la clase. En las líneas 7 y 8, se está calculando la posición final de la abeja en función del tiempo utilizando el contenido de las variables internas ``self.posicion_inicial`` y ``self.velocidad``. **Ejemplo simple de uso de las clases** Las siguientes líneas de código crean una instancia de la clase ``Abeja`` llamada ``abeja_reina`` y otra llamada ``zangano``. Es decir, definen dos variables (``abeja_reina`` y ``zangano``) cuyo tipo es ``Abeja``. Estas variables siguen todas las definiciones dadas en la clase ``Abeja``. .. code-block:: python abeja_reina = Abeja((0,0,0),(-10,0,0)) zangano = Abeja((0,10,0),(-1,0,0)) Para cada una de estas líneas se ejecuta el método ``__init__`` de la clase ``Abeja`` con los parámetros dados. Por lo tanto, una vez terminada su ejecución, la variable ``abeja_reina.posicion_inicial`` será igual a ``(0,0,0)`` y la variable ``abeja_reina.velocidad`` será igual a ``(-10,0,0)``, y a su vez ``zangano.posicion_inicial`` será igual a ``(0,10,0)`` y ``zangano.velocidad`` será ``(-1,0,0)``. Cuando se llamen los métodos ``abeja_reina.posicion`` y ``zangano.posicion``, el valor de la posición de cada abeja se calculará utilizando los valores correspondientes de sus variables internas ``self.posicion_inicial`` y ``self.velocidad``. Estos métodos retornarán la posición calculada en el tiempo `t`, basada en la posición inicial y la velocidad de cada abeja. Por ejemplo, al llamar: .. code-block:: python abeja_reina.posicion(t) La posición de ``abeja_reina`` se calculará usando su ``self.posicion_inicial`` y ``self.velocidad``, y de igual manera para: .. code-block:: python zangano.posicion(t) La posición de ``zangano`` se calculará usando sus valores de ``self.posicion_inicial`` y ``self.velocidad``. Como se puede ver, la clase ``Abeja`` **encapsula** la información de la posición inicial y de la velocidad de cada abeja, así como el método utilizado para, a partir de estos datos, calcular la posición de cada abeja en función del tiempo. Clase Enjambre ~~~~~~~~~~~~~~ .. code-block:: python :linenos: class Enjambre: def __init__(self, datos_abejas): """ Inicializa un enjambre con una lista de abejas. :param datos_abejas: Lista de tuplas con datos de las abejas (pos, vel). """ self.abejas = [Abeja(pos, vel) for pos, vel in datos_abejas] def centro_de_masa(self, t): """ Calcula el centro de masa del enjambre en el tiempo t. :param t: Tiempo en el que se quiere calcular el centro de masa. :return: Tupla con las coordenadas del centro de masa (cmx, cmy, cmz). """ m = 1 # Suponemos que todas las partículas tienen la misma masa cmx, cmy, cmz = 0, 0, 0 for abeja in self.abejas: xf, yf, zf = abeja.posicion(t) cmx += xf * m cmy += yf * m cmz += zf * m num_particulas = len(self.abejas) cmx /= num_particulas cmy /= num_particulas cmz /= num_particulas return cmx, cmy, cmz La clase ``Enjambre`` se utiliza para definir un grupo de abejas y para calcular el centro de masa de dicho enjambre. En su método ``__init__``, se crea una lista llamada ``self.abejas``, cuyos elementos son instancias de la clase ``Abeja``. Cada instancia se crea con los datos de posición inicial y velocidad proporcionados en ``datos_abejas``. En el método ``centro_de_masa`` se utiliza el método ``posicion`` de cada abeja (que a su vez usa los valores de posición inicial y velocidad de cada abeja) para obtener la posición de cada una de las abejas en un tiempo dado. A partir de estas posiciones, se calcula el centro de masa del enjambre. Como se puede ver, la clase ``Enjambre`` encapsula la lógica necesaria para gestionar un grupo de abejas y calcular su centro de masa en función del tiempo. Al encapsular tanto la lista de abejas como el cálculo del centro de masa, la clase proporciona una estructura organizada y modular que permite manipular fácilmente el enjambre como un todo, sin necesidad de preocuparse por los detalles individuales de cada abeja. Esto simplifica el manejo de la complejidad del sistema y facilita la reutilización y ampliación del código en futuros desarrollos. Abejas Borrachas: ~~~~~~~~~~~~~~~~~ Supongamos ahora que queremos definir un nuevo tipo de abejas que no se mueven a velocidad constante, sino que su posición se determina por la siguiente ecuación: .. math:: P(t)= \vec{P_i} + \vec{V_0} t + \vec{DP} \sin(\omega_0 t) y queremos volver a encontrar la posición del centro de masa del enjambre. Para resolver este problema, vamos a partir de la solución con objetos implemantada en secciones anteriores, pero vamos a hacer unos cambios. **Clase AbejaBorracha:** La primera adición al código es definir una nueva clase llamada ``AbejaBorracha``. .. code-block:: python :linenos: class AbejaBorracha(Abeja): def __init__(self, pos, vel, dp, w0): """ Inicializa una abeja borracha con su posición, velocidad inicial, amplitud de oscilación y frecuencia angular. :param pos: Tupla con la posición inicial (x, y, z). :param vel: Tupla con la velocidad inicial (vx, vy, vz). :param dp: Tupla con la amplitud de la oscilación (dpx, dpy, dpz). :param w0: Frecuencia angular de la oscilación. """ super().__init__(pos, vel) self.dp = dp self.w = w0 def posicion(self, t): """ Calcula la posición de la abeja borracha en el tiempo t según la ecuación P(t) = Pi + V0 * t + DP * sin(w0 * t). :param t: Tiempo en el que se quiere calcular la posición. :return: Tupla con la posición (x, y, z) en el tiempo t. """ xi, yi, zi = self.posicion_inicial vx, vy, vz = self.velocidad dpx, dpy, dpz = self.dp w0 = self.w # Calculamos la posición final de la abeja borracha en el tiempo t xf = xi + vx * t + dpx * math.sin(w0 * t) yf = yi + vy * t + dpy * math.sin(w0 * t) zf = zi + vz * t + dpz * math.sin(w0 * t) return xf, yf, zf En la línea 1 de esta definición estamos indicando que se está creando una clase, llamada ``AbejaBorracha``, que **hereda** de la clase ``Abeja``. Esto significa que ``AbejaBorracha`` tiene acceso a todas las propiedades y métodos definidos en la clase ``Abeja``, a menos que se redefinan. El método ``__init__`` de la clase ``AbejaBorracha`` inicializa una abeja borracha con su posición inicial, velocidad inicial, amplitud de oscilación y frecuencia angular. Para esto, llama al método ``__init__`` de la clase ``Abeja`` utilizando ``super().__init__(pos, vel)``, y luego inicializa las nuevas variables ``dp`` y ``w``. En este ejemplo sencillo, se podrían haber inicializado todas las variables en el método ``__init__`` de ``AbejaBorracha``. Sin embargo, el uso de ``super()`` se incluye para mostrar cómo se puede aprovechar la herencia en Python para reutilizar y extender la funcionalidad de una clase base. Como ``AbejaBorracha`` redefine completamente la ecuación de movimiento, el método ``posicion`` se redefine en esta clase para calcular la posición de la abeja borracha en función del tiempo `t`, utilizando la ecuación P(t) = Pi + V0 * t + DP * sin(w0 * t). Este método retorna una tupla con las coordenadas (x, y, z) calculadas. **Modificaciones a la clase Enjambre** .. code-block:: python :linenos: class Enjambre: def __init__(self, datos_abejas, datos_abejas_borrachas): """ Inicializa un enjambre con una lista de abejas y abejas borrachas. :param datos_abejas: Lista de tuplas con datos de las abejas (pos, vel). :param datos_abejas_borrachas: Lista de tuplas con datos de las abejas borrachas (pos, vel, dp, w0). """ abejas = [Abeja(pos, vel) for pos, vel in datos_abejas] abejas_borrachas = [AbejaBorracha(pos, vel, dp, w0) for pos, vel, dp, w0 in datos_abejas_borrachas] self.abejas = abejas + abejas_borrachas def centro_de_masa(self, t): """ Calcula el centro de masa del enjambre en el tiempo t. :param t: Tiempo en el que se quiere calcular el centro de masa. :return: Tupla con las coordenadas del centro de masa (cmx, cmy, cmz). """ m = 1 # Suponemos que todas las partículas tienen la misma masa cmx, cmy, cmz = 0, 0, 0 for abeja in self.abejas: xf, yf, zf = abeja.posicion(t) cmx += xf * m cmy += yf * m cmz += zf * m num_particulas = len(self.abejas) cmx /= num_particulas cmy /= num_particulas cmz /= num_particulas return cmx, cmy, cmz Para poder utilizar la clase ``Enjambre`` tanto con abejas normales como con abejas borrachas, lo único que hubo que hacer fue modificar el método ``__init__`` de ``Enjambre`` para que este creara la lista de abejas teniendo en cuenta los dos tipos. No hubo necesidad de modificar el método ``centro_de_masa``. Al tener la clase ``Abeja`` y ``AbejaBorracha`` ambas un método ``posicion`` y necesitar este el mismo parámetro (``t``), Python puede usar la misma llamada para ambos tipos de clase. Esto se conoce como **polimorfismo**. El concepto descrito es una forma de polimorfismo, específicamente el **polimorfismo en tiempo de ejecución** o **polimorfismo dinámico**. Este tipo de polimorfismo permite que el mismo método se pueda llamar en objetos de diferentes clases que pertenecen a la misma jerarquía de herencia. En el caso descrito: - Tanto la clase ``Abeja`` como la clase ``AbejaBorracha`` tienen un método ``posicion`` que toma el mismo parámetro (``t``). - Cuando se llama al método ``posicion`` en un objeto de tipo ``Abeja`` o ``AbejaBorracha`` a través de una referencia común, como en el caso de la lista de ``abejas`` en la clase ``Enjambre``, Python decide en tiempo de ejecución qué implementación del método ``posicion`` ejecutar, dependiendo del tipo real del objeto. Este comportamiento permite que el código sea más flexible y reutilizable, ya que puede trabajar con objetos de diferentes tipos de manera uniforme. **Código completo** .. code-block:: python :linenos: import math class Abeja: def __init__(self, pos, vel): """ Inicializa una abeja con su posición y velocidad inicial. :param pos: Tupla con la posición inicial (x, y, z). :param vel: Tupla con la velocidad inicial (vx, vy, vz). """ self.posicion_inicial = pos self.velocidad = vel def posicion(self, t): """ Calcula la posición de la abeja en el tiempo t, asumiendo movimiento lineal uniforme. :param t: Tiempo en el que se quiere calcular la posición. :return: Tupla con la posición (x, y, z) en el tiempo t. """ xi, yi, zi = self.posicion_inicial vx, vy, vz = self.velocidad # Calculamos la posición final de la abeja en el tiempo t xf = xi + vx * t yf = yi + vy * t zf = zi + vz * t return xf, yf, zf class AbejaBorracha(Abeja): def __init__(self, pos, vel, dp, w0): """ Inicializa una abeja borracha con su posición, velocidad inicial, amplitud de oscilación y frecuencia angular. :param pos: Tupla con la posición inicial (x, y, z). :param vel: Tupla con la velocidad inicial (vx, vy, vz). :param dp: Tupla con la amplitud de la oscilación (dpx, dpy, dpz). :param w0: Frecuencia angular de la oscilación. """ super().__init__(pos, vel) self.dp = dp self.w = w0 def posicion(self, t): """ Calcula la posición de la abeja borracha en el tiempo t según la ecuación P(t) = Pi + V0 * t + DP * sin(w0 * t). :param t: Tiempo en el que se quiere calcular la posición. :return: Tupla con la posición (x, y, z) en el tiempo t. """ xi, yi, zi = self.posicion_inicial vx, vy, vz = self.velocidad dpx, dpy, dpz = self.dp w0 = self.w # Calculamos la posición final de la abeja borracha en el tiempo t xf = xi + vx * t + dpx * math.sin(w0 * t) yf = yi + vy * t + dpy * math.sin(w0 * t) zf = zi + vz * t + dpz * math.sin(w0 * t) return xf, yf, zf class Enjambre: def __init__(self, datos_abejas, datos_abejas_borrachas): """ Inicializa un enjambre con una lista de abejas y abejas borrachas. :param datos_abejas: Lista de tuplas con datos de las abejas (pos, vel). :param datos_abejas_borrachas: Lista de tuplas con datos de las abejas borrachas (pos, vel, dp, w0). """ abejas = [Abeja(pos, vel) for pos, vel in datos_abejas] abejas_borrachas = [AbejaBorracha(pos, vel, dp, w0) for pos, vel, dp, w0 in datos_abejas_borrachas] self.abejas = abejas + abejas_borrachas def centro_de_masa(self, t): """ Calcula el centro de masa del enjambre en el tiempo t. :param t: Tiempo en el que se quiere calcular el centro de masa. :return: Tupla con las coordenadas del centro de masa (cmx, cmy, cmz). """ m = 1 # Suponemos que todas las partículas tienen la misma masa cmx, cmy, cmz = 0, 0, 0 for abeja in self.abejas: xf, yf, zf = abeja.posicion(t) cmx += xf * m cmy += yf * m cmz += zf * m num_particulas = len(self.abejas) cmx /= num_particulas cmy /= num_particulas cmz /= num_particulas return cmx, cmy, cmz # Definimos un enjambre de partículas con sus posiciones y velocidades iniciales datos_abejas = [ ((0, 0, 0), (0, 0, 1)), ((0, 0, 0), (0, 0, -1)), ((0, 0, 0), (0, 1, 0)), ((0, 0, 0), (0, -1, 0)), ((0, 0, 0), (1, 0, 0)), ((0, 0, 0), (-1, 0, 0)), ] # Definimos un enjambre de abejas borrachas con sus posiciones, velocidades, amplitud de oscilación y frecuencia angular datos_abejas_borrachas = [ ((0, 0, 0), (0, 0, 1), (0.1, 0, 0), 0.2), ((0, 0, 0), (0, 0, -1), (-0.1, 0, 0), 0.2), ((0, 0, 0), (0, 1, 0), (0, -0.1, 0), 0.2), ((0, 0, 0), (0, -1, 0), (0, 0.1, 0), 0.2), ((0, 0, 0), (1, 0, 0), (0, 0, 0.1), 0.2), ((0, 0, 0), (-1, 0, 0), (0, 0, -0.1), 0.2), ] # Creamos el enjambre con los datos iniciales primer_enjambre = Enjambre(datos_abejas, datos_abejas_borrachas) # Imprimimos el centro de masa del enjambre en diferentes tiempos t for t in range(10): print(f"Tiempo {t}: Centro de masa {primer_enjambre.centro_de_masa(t)}") Ejercicios ~~~~~~~~~~ 1. Modificar el ejemplo para que tanto la clase ``Abeja`` como la clase ``AbejaBorracha`` incluyan la masa de cada abeja, permitiendo así que las abejas de un enjambre tengan masas diferentes. El cálculo del ``centro_de_masa`` en la clase ``Enjambre`` debe ser capaz de utilizar esta nueva característica de las clases ``Abeja`` y ``AbejaBorracha``. Hacer los cambios mínimos necesarios. Ejercicios ~~~~~~~~~~ 1. Modificar el ejemplo para que tanto la clase ``Abeja`` como la clase ``AbejaBorracha`` incluyan la masa de cada abeja, permitiendo así que las abejas de un enjambre tengan masas diferentes. El cálculo del ``centro_de_masa`` en la clase ``Enjambre`` debe ser capaz de utilizar esta nueva característica de las clases ``Abeja`` y ``AbejaBorracha``. Hacer los cambios mínimos necesarios. 2. Definir clases en Python para representar objetos físicos utilizando herencia. La clase base debe almacenar la información sobre el material del que está fabricado el objeto, y las clases derivadas deben contener información sobre la geometría del objeto (por ejemplo, esfera, cubo, pirámide, cilindro, cono). A partir de la información geométrica y del material, las clases derivadas deben ser capaces de calcular la masa total del objeto y el área superficial del objeto. **Ayuda** 1. Crear una clase base que almacene la densidad del material del objeto. 2. Crear clases derivadas que representen diferentes objetos geométricos (esfera, cubo, pirámide, cilindro, cono). Cada clase debe heredar de la clase base y almacenar las propiedades geométricas relevantes (por ejemplo, radio para la esfera, longitud de los lados para el cubo, base y altura para la pirámide). 3. Implementar métodos en cada clase derivada para calcular la masa del objeto utilizando su volumen y la densidad del material. 4. Implementar métodos en cada clase derivada para calcular el área superficial del objeto. 5. Crear un programa que cree diversos objetos de diferentes tipos, los almacene en una lista y, utilizando un ciclo sencillo, imprima el tipo de objeto, la masa, el área superficial y la relación masa/área para cada objeto. Este ejercicio te permitirá practicar la creación de clases, la herencia, el polimorfismo, la validación de datos y la implementación de métodos para calcular propiedades específicas de objetos físicos en Python. **Ejemplo de salida esperada:** .. code-block:: Tipo de objeto: Esfera Masa: 523.6 kg Área superficial: 314.2 m² Relación masa/área: 1.67 kg/m² Tipo de objeto: Cubo Masa: 216 kg Área superficial: 54 m² Relación masa/área: 4 kg/m² Tipo de objeto: Pirámide Masa: 100 kg Área superficial: 120 m² Relación masa/área: 0.83 kg/m² Tipo de objeto: Cilindro Masa: 314 kg Área superficial: 452.4 m² Relación masa/área: 0.69 kg/m² Tipo de objeto: Cono Masa: 209.4 kg Área superficial: 301.6 m² Relación masa/área: 0.69 kg/m²