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:

 1# Definimos un enjambre de partículas con sus posiciones y velocidades iniciales
 2enjambre = [
 3    ((0, 0, 0), (0, 0, 1)),
 4    ((0, 0, 0), (0, 0, -1)),
 5    ((0, 0, 0), (0, 1, 0)),
 6    ((0, 0, 0), (0, -1, 0)),
 7    ((0, 0, 0), (1, 0, 0)),
 8    ((0, 0, 0), (-1, 0, 0)),
 9]
10
11def centro_de_masa(enjambre, t):
12    # Inicializamos las coordenadas del centro de masa
13    cmx = 0
14    cmy = 0
15    cmz = 0
16    m = 1  # Suponemos que todas las partículas tienen la misma masa
17
18    # Calculamos la posición de cada partícula en el tiempo t y acumulamos las coordenadas ponderadas
19    for pos, vel in enjambre:
20        xi, yi, zi = pos
21        vx, vy, vz = vel
22
23        # Calculamos la posición final de la partícula en el tiempo t
24        xf = xi + vx * t
25        yf = yi + vy * t
26        zf = zi + vz * t
27
28        # Acumulamos las posiciones finales ponderadas por la masa (aquí masa es 1)
29        cmx += xf * m
30        cmy += yf * m
31        cmz += zf * m
32
33    # Dividimos las coordenadas acumuladas por el número total de partículas para obtener el centro de masa
34    num_particulas = len(enjambre)
35    cmx /= num_particulas
36    cmy /= num_particulas
37    cmz /= num_particulas
38
39    return cmx, cmy, cmz
40
41# Imprimimos el centro de masa del enjambre en diferentes tiempos t
42for t in range(10):
43    print(f"Tiempo {t}: Centro de masa {centro_de_masa(enjambre, t)}")

Este mismo programa, usando objetos se podría escribir como:

 1class Abeja:
 2    def __init__(self, pos, vel):
 3        self.posicion_inicial = pos
 4        self.velocidad = vel
 5
 6    def posicion(self, t):
 7        xi, yi, zi = self.posicion_inicial
 8        vx, vy, vz = self.velocidad
 9
10        # Calculamos la posición final de la partícula en el tiempo t
11        xf = xi + vx * t
12        yf = yi + vy * t
13        zf = zi + vz * t
14
15        return xf, yf, zf
16
17class Enjambre:
18    def __init__(self, datos_abejas):
19        """
20        Inicializa un enjambre con una lista de abejas.
21
22        :param datos_abejas: Lista de tuplas con datos de las abejas (pos, vel).
23        """
24
25        # La siguiente linea es una forma abreviada de escribir:
26        #
27        # self.abejas = []
28        # for pos, vel in datos_abejas:
29        #   self.abejas.append(Abeja(pos, vel))
30        #
31        # Esta forma abreviada se conoce como:
32        # En inglés: list comprehensions
33        # En español: comprensiones de listas
34
35        self.abejas = [Abeja(pos, vel) for pos, vel in datos_abejas]
36
37
38    def centro_de_masa(self, t):
39        """
40        Calcula el centro de masa del enjambre en el tiempo t.
41
42        :param t: Tiempo en el que se quiere calcular el centro de masa.
43        :return: Tupla con las coordenadas del centro de masa (cmx, cmy, cmz).
44        """
45        m = 1  # Suponemos que todas las partículas tienen la misma masa
46        cmx, cmy, cmz = 0, 0, 0
47
48        for abeja in self.abejas:
49            xf, yf, zf = abeja.posicion(t)
50            cmx += xf * m
51            cmy += yf * m
52            cmz += zf * m
53
54        num_particulas = len(self.abejas)
55        cmx /= num_particulas
56        cmy /= num_particulas
57        cmz /= num_particulas
58
59        return cmx, cmy, cmz
60
61# Definimos un enjambre de partículas con sus posiciones y velocidades iniciales
62datos_abejas = [
63    ((0, 0, 0), (0, 0, 1)),
64    ((0, 0, 0), (0, 0, -1)),
65    ((0, 0, 0), (0, 1, 0)),
66    ((0, 0, 0), (0, -1, 0)),
67    ((0, 0, 0), (1, 0, 0)),
68    ((0, 0, 0), (-1, 0, 0)),
69]
70
71# Creamos el enjambre con los datos iniciales
72primer_enjambre = Enjambre(datos_abejas)
73
74# Imprimimos el centro de masa del enjambre en diferentes tiempos t
75for t in range(10):
76    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

 1class Abeja:
 2    def __init__(self, pos, vel):
 3        self.posicion_inicial = pos
 4        self.velocidad = vel
 5
 6    def posicion(self, t):
 7        xi, yi, zi = self.posicion_inicial
 8        vx, vy, vz = self.velocidad
 9
10        # Calculamos la posición final de la partícula en el tiempo t
11        xf = xi + vx * t
12        yf = yi + vy * t
13        zf = zi + vz * t
14
15        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.

Nota

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.

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:

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:

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

 1class Enjambre:
 2    def __init__(self, datos_abejas):
 3        """
 4        Inicializa un enjambre con una lista de abejas.
 5
 6        :param datos_abejas: Lista de tuplas con datos de las abejas (pos, vel).
 7        """
 8        self.abejas = [Abeja(pos, vel) for pos, vel in datos_abejas]
 9
10    def centro_de_masa(self, t):
11        """
12        Calcula el centro de masa del enjambre en el tiempo t.
13
14        :param t: Tiempo en el que se quiere calcular el centro de masa.
15        :return: Tupla con las coordenadas del centro de masa (cmx, cmy, cmz).
16        """
17        m = 1  # Suponemos que todas las partículas tienen la misma masa
18        cmx, cmy, cmz = 0, 0, 0
19
20        for abeja in self.abejas:
21            xf, yf, zf = abeja.posicion(t)
22            cmx += xf * m
23            cmy += yf * m
24            cmz += zf * m
25
26        num_particulas = len(self.abejas)
27        cmx /= num_particulas
28        cmy /= num_particulas
29        cmz /= num_particulas
30
31        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:

\[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.

 1class AbejaBorracha(Abeja):
 2    def __init__(self, pos, vel, dp, w0):
 3        """
 4        Inicializa una abeja borracha con su posición, velocidad inicial, amplitud de oscilación y frecuencia angular.
 5
 6        :param pos: Tupla con la posición inicial (x, y, z).
 7        :param vel: Tupla con la velocidad inicial (vx, vy, vz).
 8        :param dp: Tupla con la amplitud de la oscilación (dpx, dpy, dpz).
 9        :param w0: Frecuencia angular de la oscilación.
10        """
11        super().__init__(pos, vel)
12        self.dp = dp
13        self.w = w0
14
15    def posicion(self, t):
16        """
17        Calcula la posición de la abeja borracha en el tiempo t según la ecuación
18        P(t) = Pi + V0 * t + DP * sin(w0 * t).
19
20        :param t: Tiempo en el que se quiere calcular la posición.
21        :return: Tupla con la posición (x, y, z) en el tiempo t.
22        """
23        xi, yi, zi = self.posicion_inicial
24        vx, vy, vz = self.velocidad
25        dpx, dpy, dpz = self.dp
26
27        w0 = self.w
28
29        # Calculamos la posición final de la abeja borracha en el tiempo t
30        xf = xi + vx * t + dpx * math.sin(w0 * t)
31        yf = yi + vy * t + dpy * math.sin(w0 * t)
32        zf = zi + vz * t + dpz * math.sin(w0 * t)
33
34        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

 1class Enjambre:
 2    def __init__(self, datos_abejas, datos_abejas_borrachas):
 3        """
 4        Inicializa un enjambre con una lista de abejas y abejas borrachas.
 5
 6        :param datos_abejas: Lista de tuplas con datos de las abejas (pos, vel).
 7        :param datos_abejas_borrachas: Lista de tuplas con datos de las abejas borrachas (pos, vel, dp, w0).
 8        """
 9        abejas = [Abeja(pos, vel) for pos, vel in datos_abejas]
10        abejas_borrachas = [AbejaBorracha(pos, vel, dp, w0) for pos, vel, dp, w0 in datos_abejas_borrachas]
11        self.abejas = abejas + abejas_borrachas
12
13    def centro_de_masa(self, t):
14        """
15        Calcula el centro de masa del enjambre en el tiempo t.
16
17        :param t: Tiempo en el que se quiere calcular el centro de masa.
18        :return: Tupla con las coordenadas del centro de masa (cmx, cmy, cmz).
19        """
20        m = 1  # Suponemos que todas las partículas tienen la misma masa
21        cmx, cmy, cmz = 0, 0, 0
22
23        for abeja in self.abejas:
24            xf, yf, zf = abeja.posicion(t)
25            cmx += xf * m
26            cmy += yf * m
27            cmz += zf * m
28
29        num_particulas = len(self.abejas)
30        cmx /= num_particulas
31        cmy /= num_particulas
32        cmz /= num_particulas
33
34        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

  1import math
  2
  3    class Abeja:
  4        def __init__(self, pos, vel):
  5            """
  6            Inicializa una abeja con su posición y velocidad inicial.
  7
  8            :param pos: Tupla con la posición inicial (x, y, z).
  9            :param vel: Tupla con la velocidad inicial (vx, vy, vz).
 10            """
 11            self.posicion_inicial = pos
 12            self.velocidad = vel
 13
 14        def posicion(self, t):
 15            """
 16            Calcula la posición de la abeja en el tiempo t, asumiendo movimiento lineal uniforme.
 17
 18            :param t: Tiempo en el que se quiere calcular la posición.
 19            :return: Tupla con la posición (x, y, z) en el tiempo t.
 20            """
 21            xi, yi, zi = self.posicion_inicial
 22            vx, vy, vz = self.velocidad
 23
 24            # Calculamos la posición final de la abeja en el tiempo t
 25            xf = xi + vx * t
 26            yf = yi + vy * t
 27            zf = zi + vz * t
 28            return xf, yf, zf
 29
 30    class AbejaBorracha(Abeja):
 31        def __init__(self, pos, vel, dp, w0):
 32            """
 33            Inicializa una abeja borracha con su posición, velocidad inicial, amplitud de oscilación y frecuencia angular.
 34
 35            :param pos: Tupla con la posición inicial (x, y, z).
 36            :param vel: Tupla con la velocidad inicial (vx, vy, vz).
 37            :param dp: Tupla con la amplitud de la oscilación (dpx, dpy, dpz).
 38            :param w0: Frecuencia angular de la oscilación.
 39            """
 40            super().__init__(pos, vel)
 41            self.dp = dp
 42            self.w = w0
 43
 44        def posicion(self, t):
 45            """
 46            Calcula la posición de la abeja borracha en el tiempo t según la ecuación
 47            P(t) = Pi + V0 * t + DP * sin(w0 * t).
 48
 49            :param t: Tiempo en el que se quiere calcular la posición.
 50            :return: Tupla con la posición (x, y, z) en el tiempo t.
 51            """
 52            xi, yi, zi = self.posicion_inicial
 53            vx, vy, vz = self.velocidad
 54            dpx, dpy, dpz = self.dp
 55
 56            w0 = self.w
 57
 58            # Calculamos la posición final de la abeja borracha en el tiempo t
 59            xf = xi + vx * t + dpx * math.sin(w0 * t)
 60            yf = yi + vy * t + dpy * math.sin(w0 * t)
 61            zf = zi + vz * t + dpz * math.sin(w0 * t)
 62
 63            return xf, yf, zf
 64
 65    class Enjambre:
 66        def __init__(self, datos_abejas, datos_abejas_borrachas):
 67            """
 68            Inicializa un enjambre con una lista de abejas y abejas borrachas.
 69
 70            :param datos_abejas: Lista de tuplas con datos de las abejas (pos, vel).
 71            :param datos_abejas_borrachas: Lista de tuplas con datos de las abejas borrachas (pos, vel, dp, w0).
 72            """
 73            abejas = [Abeja(pos, vel) for pos, vel in datos_abejas]
 74            abejas_borrachas = [AbejaBorracha(pos, vel, dp, w0) for pos, vel, dp, w0 in datos_abejas_borrachas]
 75            self.abejas = abejas + abejas_borrachas
 76
 77        def centro_de_masa(self, t):
 78            """
 79            Calcula el centro de masa del enjambre en el tiempo t.
 80
 81            :param t: Tiempo en el que se quiere calcular el centro de masa.
 82            :return: Tupla con las coordenadas del centro de masa (cmx, cmy, cmz).
 83            """
 84            m = 1  # Suponemos que todas las partículas tienen la misma masa
 85            cmx, cmy, cmz = 0, 0, 0
 86
 87            for abeja in self.abejas:
 88                xf, yf, zf = abeja.posicion(t)
 89                cmx += xf * m
 90                cmy += yf * m
 91                cmz += zf * m
 92
 93            num_particulas = len(self.abejas)
 94            cmx /= num_particulas
 95            cmy /= num_particulas
 96            cmz /= num_particulas
 97
 98            return cmx, cmy, cmz
 99
100    # Definimos un enjambre de partículas con sus posiciones y velocidades iniciales
101    datos_abejas = [
102        ((0, 0, 0), (0, 0, 1)),
103        ((0, 0, 0), (0, 0, -1)),
104        ((0, 0, 0), (0, 1, 0)),
105        ((0, 0, 0), (0, -1, 0)),
106        ((0, 0, 0), (1, 0, 0)),
107        ((0, 0, 0), (-1, 0, 0)),
108    ]
109
110    # Definimos un enjambre de abejas borrachas con sus posiciones, velocidades, amplitud de oscilación y frecuencia angular
111    datos_abejas_borrachas = [
112        ((0, 0, 0), (0, 0, 1), (0.1, 0, 0), 0.2),
113        ((0, 0, 0), (0, 0, -1), (-0.1, 0, 0), 0.2),
114        ((0, 0, 0), (0, 1, 0), (0, -0.1, 0), 0.2),
115        ((0, 0, 0), (0, -1, 0), (0, 0.1, 0), 0.2),
116        ((0, 0, 0), (1, 0, 0), (0, 0, 0.1), 0.2),
117        ((0, 0, 0), (-1, 0, 0), (0, 0, -0.1), 0.2),
118    ]
119
120    # Creamos el enjambre con los datos iniciales
121    primer_enjambre = Enjambre(datos_abejas, datos_abejas_borrachas)
122
123    # Imprimimos el centro de masa del enjambre en diferentes tiempos t
124    for t in range(10):
125        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:

    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²