.. _clase_06: ============================================= Clase 6: Objetos y administración de errores ============================================= =================================== Atrapar y administrar errores ============================= **Python** tiene incorporado un mecanismo para atrapar errores de distintos tipos, así como para generar errores que den información al usuario sobre usos incorrectos del código. En primer lugar consideremos lo que se llama un error de sintaxis. El siguiente comando es sintácticamente correcto y el intérprete sabe como leerlo .. code:: python print("hola") .. parsed-literal:: hola mientras que, si escribimos algo que no está permitido en el lenguaje .. code:: python print("hola")) :: Cell In[2], line 1 print("hola")) ^ SyntaxError: unmatched ')' El intérprete detecta el error y repite la línea donde lo identifica. Este tipo de errores debe corregirse para poder seguir con el programa. Consideremos ahora el código siguiente, que es sintácticamente correcto pero igualmente causa un error .. code:: python a = 1 b = 0 z = a / b :: --------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) Cell In[3], line 3 1 a = 1 2 b = 0 ----> 3 z = a / b ZeroDivisionError: division by zero Cuando se encuentra un error, **Python** muestra el lugar en que ocurre y de qué tipo de error se trata. .. code:: python print(hola) :: --------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[4], line 1 ----> 1 print(hola) NameError: name 'hola' is not defined Este mensaje da un tipo de error diferente. Ambos: ``ZeroDivisionError`` y ``NameError`` son tipos de errores (o excepciones). Hay una larga lista de tipos de errores que son parte del lenguaje y puede consultarse en la documentación de `Built-in Exceptions <https://docs.python.org/3/library/exceptions.html#bltin-exceptions>`__. Administración de excepciones ----------------------------- Cuando nuestro programa aumenta en complejidad, aumenta la posibilidad de encontrar errores. Esto se incrementa si se tiene que interactuar con otros usuarios o con datos externos. Consideremos el siguiente ejemplo simple: .. code:: python import math numeros = [i**2 for i in range(10)] numeros.append(-1) numeros.append("121") .. code:: python for j in numeros: print(f"La raíz cuadrada de {j} es {math.sqrt(j)}") print("Terminé de calcular todo") .. parsed-literal:: La raíz cuadrada de 0 es 0.0 La raíz cuadrada de 1 es 1.0 La raíz cuadrada de 4 es 2.0 La raíz cuadrada de 9 es 3.0 La raíz cuadrada de 16 es 4.0 La raíz cuadrada de 25 es 5.0 La raíz cuadrada de 36 es 6.0 La raíz cuadrada de 49 es 7.0 La raíz cuadrada de 64 es 8.0 La raíz cuadrada de 81 es 9.0 :: --------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[6], line 2 1 for j in numeros: ----> 2 print(f"La raíz cuadrada de {j} es {math.sqrt(j)}") 3 print("Terminé de calcular todo") ValueError: math domain error En este caso se “levanta” una excepción del tipo ``ValueError`` debido a que el módulo ``math`` solo trabaja con números reales, y no puede calcular la raíz cuadrada de ``-1``. En ``Python`` podemos modificar nuestro programa para manejar este error: .. code:: python for j in numeros: try: print(f"La raíz cuadrada de {j} es {math.sqrt(j)}") except: print(f"No se puede calcular la raíz cuadrada del valor {j}") print("Terminé de calcular todo") .. parsed-literal:: La raíz cuadrada de 0 es 0.0 La raíz cuadrada de 1 es 1.0 La raíz cuadrada de 4 es 2.0 La raíz cuadrada de 9 es 3.0 La raíz cuadrada de 16 es 4.0 La raíz cuadrada de 25 es 5.0 La raíz cuadrada de 36 es 6.0 La raíz cuadrada de 49 es 7.0 La raíz cuadrada de 64 es 8.0 La raíz cuadrada de 81 es 9.0 No se puede calcular la raíz cuadrada del valor -1 No se puede calcular la raíz cuadrada del valor 121 Terminé de calcular todo Notar que en los dos casos, el mensaje de error es el mismo. Sin embargo, los dos casos -si bien se ven similares- son diferentes. En el caso del número entero ``-1`` no puede calcularse utilizando el módulo ``math`` pero en principio hay una respuesta. En el segundo caso, 121 no es un número sino un *string*. Podemos distinguir cada caso. Veamos la siguiente modificación: .. code:: python for j in numeros: try: print(f"La raíz cuadrada de {j} es {math.sqrt(j)}") except(ValueError): print(f"No se puede calcular la raíz cuadrada del valor {j}") print("Terminé de calcular todo") .. parsed-literal:: La raíz cuadrada de 0 es 0.0 La raíz cuadrada de 1 es 1.0 La raíz cuadrada de 4 es 2.0 La raíz cuadrada de 9 es 3.0 La raíz cuadrada de 16 es 4.0 La raíz cuadrada de 25 es 5.0 La raíz cuadrada de 36 es 6.0 La raíz cuadrada de 49 es 7.0 La raíz cuadrada de 64 es 8.0 La raíz cuadrada de 81 es 9.0 No se puede calcular la raíz cuadrada del valor -1 :: --------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[8], line 3 1 for j in numeros: 2 try: ----> 3 print(f"La raíz cuadrada de {j} es {math.sqrt(j)}") 4 except(ValueError): 5 print(f"No se puede calcular la raíz cuadrada del valor {j}") TypeError: must be real number, not str Vemos que, como esperábamos no es un problema de valor sino de tipo del argumento. Agreguemos este caso: .. code:: python for j in numeros: try: print(f"La raíz cuadrada de {j} es {math.sqrt(j)}") except(ValueError): print(f"No se puede calcular la raíz cuadrada del valor {j}") except(TypeError): print(f"No está definida la raíz cuadrada para tipos {type(j)}") print("Terminé de calcular todo") .. parsed-literal:: La raíz cuadrada de 0 es 0.0 La raíz cuadrada de 1 es 1.0 La raíz cuadrada de 4 es 2.0 La raíz cuadrada de 9 es 3.0 La raíz cuadrada de 16 es 4.0 La raíz cuadrada de 25 es 5.0 La raíz cuadrada de 36 es 6.0 La raíz cuadrada de 49 es 7.0 La raíz cuadrada de 64 es 8.0 La raíz cuadrada de 81 es 9.0 No se puede calcular la raíz cuadrada del valor -1 No está definida la raíz cuadrada para tipos <class 'str'> Terminé de calcular todo .. code:: python for j in numeros: try: print(f"La raíz cuadrada de {j} es {math.sqrt(j)}") except(ValueError): print(f"No se puede calcular la raíz cuadrada del valor {j}") except(TypeError): print(f"No está definida la raíz cuadrada para tipos {type(j)}") except: print(f"Otro error para {j} de tipo {type(j)}") print("Terminé de calcular todo") .. parsed-literal:: La raíz cuadrada de 0 es 0.0 La raíz cuadrada de 1 es 1.0 La raíz cuadrada de 4 es 2.0 La raíz cuadrada de 9 es 3.0 La raíz cuadrada de 16 es 4.0 La raíz cuadrada de 25 es 5.0 La raíz cuadrada de 36 es 6.0 La raíz cuadrada de 49 es 7.0 La raíz cuadrada de 64 es 8.0 La raíz cuadrada de 81 es 9.0 No se puede calcular la raíz cuadrada del valor -1 No está definida la raíz cuadrada para tipos <class 'str'> Terminé de calcular todo En esta forma sencilla, la declaración ``try`` funciona de la siguiente manera: - Primero, se ejecuta el *bloque try* (el código entre las declaración ``try`` y ``except``). - Si no ocurre ninguna excepción, el *bloque except* se saltea y termina la ejecución de la declaración ``try``. - Si ocurre una excepción durante la ejecución del *bloque try*, el resto del bloque se saltea. Luego, si su tipo coincide con la excepción nombrada luego de la palabra reservada ``except``, se ejecuta el *bloque except*, y la ejecución continúa luego de la declaración ``try``. - Si ocurre una excepción que no coincide con la excepción nombrada en el ``except``, esta se pasa a declaraciones ``try`` de más afuera; si no se encuentra nada que la maneje, es una *excepción no manejada*, y la ejecución se frena con un mensaje como los mostrados arriba. El mecanismo es un poco más complejo, y contiene otros elementos que permiten un control más fino que lo descripto aquí. “Crear” excepciones ------------------- Podemos forzar a que nuestro código cree una excepción usando ``raise``. Por ejemplo: .. code:: python import math def mi_sqrt(x): if x < 0: raise ValueError(f"x = {x}, debería ser positivo") return math.sqrt(x) .. code:: python mi_sqrt(12) .. parsed-literal:: 3.4641016151377544 .. code:: python mi_sqrt(-2) :: --------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[13], line 1 ----> 1 mi_sqrt(-2) Cell In[11], line 4, in mi_sqrt(x) 2 def mi_sqrt(x): 3 if x < 0: ----> 4 raise ValueError(f"x = {x}, debería ser positivo") 5 return math.sqrt(x) ValueError: x = -2, debería ser positivo Vemos que así nuestra función da un error que el intérprete muestra al usuario. En este caso porque el valor no es positivo. Un error diferente aparece si le damos números complejos: .. code:: python mi_sqrt(1+2j) :: --------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[14], line 1 ----> 1 mi_sqrt(1+2j) Cell In[11], line 3, in mi_sqrt(x) 2 def mi_sqrt(x): ----> 3 if x < 0: 4 raise ValueError(f"x = {x}, debería ser positivo") 5 return math.sqrt(x) TypeError: '<' not supported between instances of 'complex' and 'int' En este caso, el error aparece en la comparación. Corrijamos este caso: .. code:: python import math def mi_sqrt(x): if not isinstance(x,(int,float)): raise TypeError(f"x debe ser un tipo describiendo un número real") if x < 0: raise ValueError(f"x = {x}, debería ser positivo") return math.sqrt(x) .. code:: python mi_sqrt(1+2j) :: --------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[16], line 1 ----> 1 mi_sqrt(1+2j) Cell In[15], line 4, in mi_sqrt(x) 2 def mi_sqrt(x): 3 if not isinstance(x,(int,float)): ----> 4 raise TypeError(f"x debe ser un tipo describiendo un número real") 5 if x < 0: 6 raise ValueError(f"x = {x}, debería ser positivo") TypeError: x debe ser un tipo describiendo un número real .. code:: python mi_sqrt(-2) :: --------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[17], line 1 ----> 1 mi_sqrt(-2) Cell In[15], line 6, in mi_sqrt(x) 4 raise TypeError(f"x debe ser un tipo describiendo un número real") 5 if x < 0: ----> 6 raise ValueError(f"x = {x}, debería ser positivo") 7 return math.sqrt(x) ValueError: x = -2, debería ser positivo Esta función podemos utilizarla en nuestro código de la siguiente manera: .. code:: python try: mi_sqrt(-2) except(ValueError): print("Argumento negativo!!!") .. parsed-literal:: Argumento negativo!!! .. code:: python try: mi_sqrt(2+2j) except(TypeError): print("Tipo incorrecto!!!") .. parsed-literal:: Tipo incorrecto!!! Programación Orientada a Objetos ================================= Breve introducción a Programación Orientada a Objetos ----------------------------------------------------- Vimos como escribir funciones que realizan un trabajo específico y nos devuelven un resultado. La mayor parte de nuestros programas van a estar diseñados con un hilo conductor principal, que utiliza una serie de funciones para realizar el cálculo. De esta manera, el código es altamente reusable. Hay otras maneras de organizar el código, una de ellas es particularmente útil cuando un conjunto de rutinas comparte un dado conjunto de datos. En ese caso, puede ser adecuado utilizar un esquema de programación orientada a objetos. En esta modalidad programamos distintas entidades, donde cada una tiene un comportamiento, y determinamos una manera de interactuar entre ellas. Clases y Objetos ---------------- Una ``Clase`` define características que tienen los ``objetos`` de dicha clase. En general la clase tiene: un nombre y características (campos o atributos y métodos). Un Objeto, en programación, puede pensarse como la representación de un objeto real, de una dada clase. Un objeto real tiene una composición y características, y además puede realizar un conjunto de actividades (tiene un comportamiento). Cuando programamos, las “partes” son los datos, y el “comportamiento” son los métodos. Ejemplos de la vida diaria serían: Una clase *Bicicleta*, y muchos objetos del tipo bicicleta (mi bicicleta, la suya, etc). La definición de la clase debe contener la información de qué es una bicicleta (dos ruedas, manubrio, etc) y luego se realizan muchas copias del tipo bicicleta (los objetos). Se dice que los **objetos** son instancias de una **clase**, por ejemplo ya vimos los números enteros. Cuando definimos: ``a = 3`` estamos diciendo que ``a`` es una instancia (objeto) de la clase ``int``. Los objetos pueden guardar datos (en este caso ``a`` guarda el valor ``3``). Las variables que contienen los datos de los objetos se llaman usualmente campos o atributos. Las acciones que tienen asociadas los objetos se realizan a través de funciones internas, que se llaman métodos. Las clases se definen con la palabra reservada ``class``, veamos un ejemplo simple: .. code:: python class NuevaClase: pass .. code:: python c1 = NuevaClase() .. code:: python c1 .. parsed-literal:: <__main__.NuevaClase at 0x7f37f0bd4050> .. code:: python help(c1) .. parsed-literal:: Help on NuevaClase in module __main__ object: class NuevaClase(builtins.object) | Data descriptors defined here: | | __dict__ | dictionary for instance variables | | __weakref__ | list of weak references to the object .. code:: python dir(c1) .. parsed-literal:: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__'] .. code:: python class Punto(object): "Clase para describir un punto en el espacio" def __init__(self, x, y, z): self.x = x self.y = y self.z = z .. code:: python P1 = Punto(0.5, 0.7, 0) .. code:: python P1 .. parsed-literal:: <__main__.Punto at 0x7f37f0bd5160> Vemos que ``P1`` es un objeto del tipo ``Punto`` que está alojado en una dada dirección de memoria (dada por ese número largo hexadecimal). Para referirnos a los *atributos* de ``P1`` se utiliza notación “de punto”: .. code:: python P1.x, P1.z .. parsed-literal:: (0.5, 0) .. code:: python print(P1) .. parsed-literal:: <__main__.Punto object at 0x7f37f0bd5160> Como vemos, acabamos de definir una clase de tipo Punto. A continuación definimos un *método* ``__init__`` que hace el trabajo de inicializar el objeto. Algunos puntos a notar: - La línea ``P1 = Punto(0.5, 0.5, 0)`` crea un nuevo objeto del tipo ``Punto``. Notar que usamos paréntesis como cuando llamamos a una función pero Python sabe que estamos “llamando” a una clase y creando un objeto. - El método ``__init__`` es especial y es el Constructor de objetos de la clase. Es llamado automáticamente al definir un nuevo objeto de esa clase. Por esa razón, le pasamos los dos argumentos al crear el objeto. - El primer argumento del método, ``self``, debe estar presente en la definición de todos los métodos pero no lo pasamos como argumento cuando hacemos una llamada a la función. **Python** se encarga de pasarlo en forma automática. Lo único relevante de este argumento es que es el primero para todos los métodos, el nombre ``self`` puede cambiarse por cualquier otro **pero, por convención, no se hace**. .. code:: python P2 = Punto() :: --------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[11], line 1 ----> 1 P2 = Punto() TypeError: Punto.__init__() missing 3 required positional arguments: 'x', 'y', and 'z' Por supuesto la creación del objeto falla si no le damos ningún argumento porque los argumentos de ``__init__`` no son opcionales. Modifiquemos eso y aprovechamos para definir algunos otros métodos que pueden ser útiles: .. code:: python from math import atan2, pi class Punto: "Clase para describir un punto en el espacio" def __init__(self, x=0, y=0, z=0): "Inicializa un punto en el espacio" self.x = x self.y = y self.z = z def angulo_azimuthal(self): "Devuelve el ángulo que forma con el eje x, en grados" return 180/pi*(atan2(self.y, self.x)) .. code:: python P1 = Punto(0.5, 0.5) .. code:: python P1.angulo_azimuthal() .. parsed-literal:: 45.0 .. code:: python P2 = Punto() .. code:: python P2.x .. parsed-literal:: 0 .. code:: python help(P1) .. parsed-literal:: Help on Punto in module __main__ object: class Punto(builtins.object) | Punto(x=0, y=0, z=0) | | Clase para describir un punto en el espacio | | Methods defined here: | | __init__(self, x=0, y=0, z=0) | Inicializa un punto en el espacio | | angulo_azimuthal(self) | Devuelve el ángulo que forma con el eje x, en grados | | ---------------------------------------------------------------------- | Data descriptors defined here: | | __dict__ | dictionary for instance variables | | __weakref__ | list of weak references to the object El objeto ``P1`` es del tipo ``Punto`` y tiene definidos los métodos ``__init__()`` (el constructor) y el método ``angulo_azimuthal()`` que programamos para obtener el ángulo. Además tiene el método ``__dict__`` que provee un diccionario con los datos del objeto: .. code:: python P1.__dict__ .. parsed-literal:: {'x': 0.5, 'y': 0.5, 'z': 0} Cuando ejecutamos uno de los métodos de un objeto, es equivalente a hacer la llamada al método de la clase, dando como primer argumento el objeto en cuestión: .. code:: python pp = Punto(0.1, "s", [1,2]) .. code:: python pp .. parsed-literal:: <__main__.Punto at 0x7f37f0b53b10> Evidentemente, al ser Python un lenguaje de tipos dinámicos, no hay forma de prevenir que se use la clase ``Punto`` con otros tipos de variables que no sean números, lo cual puede tener consecuencias: .. code:: python pp.angulo_azimuthal() :: --------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[21], line 1 ----> 1 pp.angulo_azimuthal() Cell In[12], line 14, in Punto.angulo_azimuthal(self) 12 def angulo_azimuthal(self): 13 "Devuelve el ángulo que forma con el eje x, en grados" ---> 14 return 180/pi*(atan2(self.y, self.x)) TypeError: must be real number, not str .. code:: python print(P1.angulo_azimuthal()) print(Punto.angulo_azimuthal(P1)) .. parsed-literal:: 45.0 45.0 Al hacer la llamada a un método de una “instancia de la Clase” (o un objeto), omitimos el argumento ``self``. El lenguaje traduce nuestro llamado: ``P1.angulo_azimuthal()`` como ``Punto.angulo_azimuthal(P1)`` ya que ``self`` se refiere al objeto que llama al método. .. note:: Es responsabilidad de quien programa establecer las restricciones de los valores que se pueden asignar a los atributos de un objeto. Por ejemplo, si se quiere que los valores de x, y, y z sean siempre números reales, se puede hacer lo siguiente: .. code:: python class Punto: "Clase para describir un punto en el espacio" def __init__(self, x=0, y=0, z=0): "Inicializa un punto en el espacio" if not (isinstance(x, (int, float)) and isinstance(y, (int, float)) and isinstance(z, (int, float))): raise TypeError("x, y, z deben ser números enteros o flotantes") self.x = x self.y = y self.z = z def angulo_azimuthal(self): "Devuelve el ángulo que forma con el eje x, en grados" return 180/pi*(atan2(self.y, self.x)) Acá usamos la función ``isinstance`` para chequear que la variable ``x`` (que a su vez es un objeto) sea de alguna de las clases ``int`` o ``float``. .. code:: python pv = Punto(0.1, "s", [1,2]) :: --------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[26], line 1 ----> 1 pv = Punto(0.1, "s", [1,2]) Cell In[23], line 7, in Punto.__init__(self, x, y, z) 5 "Inicializa un punto en el espacio" 6 if not (isinstance(x, (int, float)) and isinstance(y, (int, float)) and isinstance(z, (int, float))): ----> 7 raise TypeError("x, y, z deben ser números enteros o flotantes") 8 self.x = x 9 self.y = y TypeError: x, y, z deben ser números enteros o flotantes Métodos especiales ~~~~~~~~~~~~~~~~~~ Volviendo a mirar la definición de la clase, vemos que ``__init__()`` es un método “especial”. No necesitamos ejecutarlo explícitamente ya que Python lo hace automáticamente al crear cada objeto de la clase dada. En *Python* el usuario/programador tiene acceso a todos los métodos y atributos. Por convención los nombres que inician con guión bajo se presupone que no son para ser utilizados directamente. En particular, los que están rodeados por dos guiones bajos tienen significado especial y *Python* los va a utilizar en forma autómatica en distintas ocasiones. Herencia -------- Una de las características de la programación orientada a objetos es la facilidad de reutilización de código. Uno de los mecanismos más importantes es a través de la herencia. Cuando definimos una nueva clase, podemos crearla a partir de un objeto que ya exista. Por ejemplo, utilizando la clase ``Punto`` podemos definir una nueva clase para describir un vector en el espacio: .. code:: python class Vector(Punto): "Representa un vector en el espacio" def suma(self, v2): "Calcula un vector que contiene la suma de dos vectores" print("Aún no implementada la suma de dos vectores") # código calculando v = suma de self + v2 # ... def producto(self, v2): "Calcula el producto interno entre dos vectores" print("Aún no implementado el producto interno de dos vectores") # código calculando el producto interno pr = v1 . v2 def norma(self): "Devuelve la distancia del punto al origen" print("Aún no implementado la norma del vector") # código calculando el producto interno pr = v1 . v2 Acá hemos definido un nuevo tipo de objeto, llamado ``Vector`` que se deriva de la clase ``Punto``. Veamos cómo funciona: .. code:: python v1 = Vector(2,3.1) v2 = Vector() .. code:: python v1 .. parsed-literal:: <__main__.Vector at 0x7f37f0bd67b0> .. code:: python v1.x, v1.y, v1.z .. parsed-literal:: (2, 3.1, 0) .. code:: python v2.x, v2.y, v2.z .. parsed-literal:: (0, 0, 0) .. code:: python v1.angulo_azimuthal() .. parsed-literal:: 57.171458208587474 .. code:: python v = v1.suma(v2) .. parsed-literal:: Aún no implementada la suma de dos vectores .. code:: python print(v) .. parsed-literal:: None Los métodos que habíamos definido para los puntos del espacio, son accesibles para el nuevo objeto. Además podemos agregar (extender) el nuevo objeto con otros atributos y métodos. Como vemos, aún no está implementado el cálculo de las distintas funciones, eso forma parte del siguiente … -------------- Ejercicios 06 (a) ================= 1. Implemente los métodos ``suma``, ``producto`` y ``norma`` - ``suma`` debe retornar un objeto del tipo ``Vector`` y contener en cada componente la suma de las componentes de los dos vectores que toma como argumento. - ``producto`` toma como argumentos dos vectores y retorna un número real con el valor del producto interno - ``norma`` toma como argumentos el propio objeto y retorna el número real correspondiente: .. math:: \sqrt{x^2 + y^2 + z^2} Su uso será el siguiente: .. code:: python v1 = Vector(1,2,3) v2 = Vector(3,2,1) vs1 = v1.suma(v2) vs2 = v2.suma(v1) print(type(vs2)) # print(vs1 == vs2) pr = v1.producto(v2) a = v1.norma() -------------- Atributos de clases y de instancias =================================== Las variables que hemos definido pertenecen a cada objeto. Por ejemplo cuando hacemos .. code:: python class Punto: "Clase para describir un punto en el espacio" def __init__(self, x=0, y=0, z=0): "Inicializa un punto en el espacio" self.x = x self.y = y self.z = z return None def angulo_azimuthal(self): "Devuelve el ángulo que forma con el eje x, en radianes" return atan2(self.y, self.x) .. code:: python p1 = Punto(1,2,3) p2 = Punto(4,5,6) cada vez que creamos un objeto de una dada clase, tiene un dato que corresponde al objeto. En este caso tanto ``p1`` como ``p2`` tienen un atributo llamado ``x``, y cada uno de ellos tiene su propio valor: .. code:: python print(p1.x, p2.x) .. parsed-literal:: 1 4 De la misma manera, en la definición de la clase nos referimos a estas variables como ``self.x``, indicando que pertenecen a una instancia de una clase (o, lo que es lo mismo: un objeto específico). También existe la posibilidad de asociar variables (datos) con la clase y no con una instancia de esa clase (objeto). En el siguiente ejemplo, la variable ``num_puntos`` no pertenece a un ``punto`` en particular sino a la clase del tipo ``Punto`` .. code:: python class Punto: "Clase para describir un punto en el espacio" num_puntos = 0 def __init__(self, x=0, y=0, z=0): "Inicializa un punto en el espacio" self.x = x self.y = y self.z = z Punto.num_puntos += 1 return None En este ejemplo estamos creando el objeto ``Punto`` y en la variable ``num_puntos`` de la clase estamos llevando la cuenta de cuantos puntos hemos creado. Al crear un nuevo punto (con el método ``__init__()``) aumentamos el valor de la variable en uno. .. code:: python print('Número de puntos:', Punto.num_puntos) p1 = Punto(1,1,1) p2 = Punto() print(p1, p2) print('Número de puntos:', Punto.num_puntos) .. parsed-literal:: Número de puntos: 0 <__main__.Punto object at 0x7f019471fe00> <__main__.Punto object at 0x7f01946c3610> Número de puntos: 2 Si estamos contando el número de puntos que tenemos, podemos crear métodos para acceder a ellos y/o manipularlos. Estos métodos no se refieren a una instancia en particular (``p1`` o ``p2`` en este ejemplo) sino al tipo de objeto ``Punto`` (a la clase) .. code:: python del p1 .. code:: python print(p1) :: --------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[7], line 1 ----> 1 print(p1) NameError: name 'p1' is not defined .. code:: python print(Punto.num_puntos) .. parsed-literal:: 2 .. code:: python del p2 .. code:: python print(Punto.num_puntos) .. parsed-literal:: 2 Nuestra implementación tiene una falla, al borrar los objetos no actualiza el contador, descontando uno cada vez. .. code:: python class Punto: "Clase para describir un punto en el espacio" num_puntos = 0 def __init__(self, x=0, y=0, z=0): "Inicializa un punto en el espacio" self.x = x self.y = y self.z = z Punto.num_puntos += 1 return None def borrar(self): "Borra el punto" Punto.num_puntos -= 1 En esta versión agregamos un método para actualizar el contador (``borrar()``) y además agregamos un método para imprimir el número de puntos total definidos. .. code:: python print('Número de puntos:', Punto.num_puntos) p1 = Punto(1,1,1) p2 = Punto() print(p1, p2) print('Número de puntos:', Punto.num_puntos) .. parsed-literal:: Número de puntos: 0 <__main__.Punto object at 0x7f0194749010> <__main__.Punto object at 0x7f01946c3610> Número de puntos: 2 .. code:: python print('Número de puntos:', Punto.num_puntos) p1.borrar() print('Número de puntos:', Punto.num_puntos) .. parsed-literal:: Número de puntos: 2 Número de puntos: 1 .. code:: python p1, p2 .. parsed-literal:: (<__main__.Punto at 0x7f0194749010>, <__main__.Punto at 0x7f01946c3610>) Sin embargo, en esta implementación no estamos realmente removiendo ``p1``, sólo estamos actualizando el contador: .. code:: python print(f"{p1 = }") print(f"{p1.x = }") .. parsed-literal:: p1 = <__main__.Punto object at 0x7f0194749010> p1.x = 1 Algunos métodos “especiales” ============================ Hay algunos métodos que **Python** interpreta de manera especial. Ya vimos uno de ellos: ``__init__()``, que es llamado automáticamente cuando se crea una instancia de la clase. Método ``__del__()`` -------------------- Similarmente, existe un método ``__del__()`` que Python llama automáticamente cuando borramos un objeto. .. code:: python del p1 del p2 Podemos utilizar esto para implementar la actualización del contador de puntos .. code:: python class Punto: "Clase para describir un punto en el espacio" num_puntos = 0 def __init__(self, x=0, y=0, z=0): "Inicializa un punto en el espacio" self.x = x self.y = y self.z = z Punto.num_puntos += 1 return None def __del__(self): "Borra el punto y actualiza el contador" Punto.num_puntos -= 1 .. code:: python p1 = Punto(1,1,1) p2 = Punto() print('Número de puntos:', Punto.num_puntos) del p2 print('Número de puntos:', Punto.num_puntos) .. parsed-literal:: Número de puntos: 2 Número de puntos: 1 .. code:: python p1 .. parsed-literal:: <__main__.Punto at 0x7f019474a120> .. code:: python p2 :: --------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[20], line 1 ----> 1 p2 NameError: name 'p2' is not defined .. code:: python print(p1) .. parsed-literal:: <__main__.Punto object at 0x7f019474a120> Como vemos, al borrar el objeto, automáticamente se actualiza el contador. Métodos ``__str__`` y ``__repr__`` ---------------------------------- El método ``__str__`` también es especial, en el sentido en que puede ser utilizado aunque no lo llamemos explícitamente en nuestro código. En particular, es llamado cuando usamos expresiones del tipo ``str(objeto)`` o automáticamente cuando se utilizan las funciones ``format`` y ``print()``. El objetivo de este método es que sea legible para los usuarios. .. code:: python p1 = Punto(1,1,1) .. code:: python print(p1) .. parsed-literal:: <__main__.Punto object at 0x7f01946c39d0> Rehagamos la clase para definir puntos .. code:: python class Punto: "Clase para describir un punto en el espacio" num_puntos = 0 def __init__(self, x=0, y=0, z=0): "Inicializa un punto en el espacio" self.x = x self.y = y self.z = z Punto.num_puntos += 1 return None def __del__(self): "Borra el punto y actualiza el contador" Punto.num_puntos -= 1 def __str__(self): s = f"Punto en el espacio con coordenadas: x = {self.x}, y = {self.y}, z = {self.z}" return s .. code:: python p1 = Punto(1,1,0) .. code:: python print(p1) .. parsed-literal:: Punto en el espacio con coordenadas: x = 1, y = 1, z = 0 .. code:: python ss = 'p1 = {}'.format(p1) ss .. parsed-literal:: 'p1 = Punto en el espacio con coordenadas: x = 1, y = 1, z = 0' .. code:: python p1.x, p1.y, p1.z .. parsed-literal:: (1, 1, 0) .. code:: python p1 .. parsed-literal:: <__main__.Punto at 0x7f019474a270> .. code:: python str(p1) .. parsed-literal:: 'Punto en el espacio con coordenadas: x = 1, y = 1, z = 0' Como vemos, si no usamos la función ``print()`` o ``format()`` sigue mostrándonos el objeto (que no es muy informativo). Esto puede remediarse agregando el método especial ``__repr__``. Este método es el que se llama cuando queremos inspeccionar un objeto. El objetivo de este método es que de información sin ambigüedades. .. code:: python class Punto: "Clase para describir un punto en el espacio" num_puntos = 0 def __init__(self, x=0, y=0, z=0): "Inicializa un punto en el espacio" self.x = x self.y = y self.z = z Punto.num_puntos += 1 return None def __del__(self): "Borra el punto y actualiza el contador" Punto.num_puntos -= 1 def __str__(self): return f"Punto en el espacio con coordenadas: x = {self.x}, y = {self.y}, z = {self.z}" def __repr__(self): return f"Punto(x = {self.x}, y = {self.y}, z = {self.z})" .. code:: python p2 = Punto(0.3, 0.3, 1) .. code:: python p2 .. parsed-literal:: Punto(x = 0.3, y = 0.3, z = 1) .. code:: python p2.x = 5 p2 .. parsed-literal:: Punto(x = 5, y = 0.3, z = 1) Como vemos ahora tenemos una representación del objeto, que nos da información precisa. Método ``__call__`` ------------------- Este método, si existe es ejecutado cuando llamamos al objeto. Si no existe, es un error llamar al objeto: .. code:: python p2() :: --------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[35], line 1 ----> 1 p2() TypeError: 'Punto' object is not callable .. code:: python class Punto: "Clase para describir un punto en el espacio" num_puntos = 0 def __init__(self, x=0, y=0, z=0): "Inicializa un punto en el espacio" self.x = x self.y = y self.z = z Punto.num_puntos += 1 return None def __del__(self): "Borra el punto y actualiza el contador" Punto.num_puntos -= 1 def __str__(self): return f"Punto en el espacio con coordenadas: x = {self.x}, y = {self.y}, z = {self.z}" def __repr__(self): return f"Punto(x = {self.x}, y = {self.y}, z = {self.z})" def __call__(self): return "Ejecuté el objeto: {}".format(self) # return str(self) # return "{}".format(self) @classmethod def total(cls): "Imprime el número total de puntos" print(f"En total hay {cls.num_puntos} puntos definidos") .. code:: python p3 = Punto(1,3,4) p3 .. parsed-literal:: Punto(x = 1, y = 3, z = 4) .. code:: python p3() .. parsed-literal:: 'Ejecuté el objeto: Punto en el espacio con coordenadas: x = 1, y = 3, z = 4' Métodos ``__add__``, ``__mul__`` y ``__abs__`` ---------------------------------------------- El método ``__add__()`` -si existe- es llamado automáticamente cuando se utiliza la operación suma. De la misma manera cuando existe el método ``__mul__()``\ se ejecuta al utilizar la operación multiplicación. El método especial ``__abs__`` se ejecuta cuando utilizamos la función de python ``abs()`` Retomemos el ejemplo de la clase ``Vector`` que definimos anteriormente, y agreguemos estos métodos especiales, utilizando el código de los métodos ``suma`` y ``producto`` .. code:: python class Vector(Punto): "Representa un vector en el espacio" def suma(self, v2): "Calcula un vector que contiene la suma de dos vectores" print("Aún no implementada la suma de dos vectores") # código calculando v = suma de self + v2 # ... def producto(self, v2): "Calcula el producto interno entre dos vectores" print("Aún no implementado el producto interno de dos vectores") # código calculando el producto interno pr = v1 . v2 def norma(self): "Devuelve la del vector" print("Aún no implementada la norma del vector") # código calculando el producto interno pr = v1 . v2 def __add__(self, v2): return self.suma(v2) def __mul__(self, v2): return self.producto(v2) def __abs__(self): return self.norma() .. code:: python v1 = Vector(1,3,3) v2 = Vector(2,-1,-2) .. code:: python print(v1.suma(v2)) print(v1 + v2) .. parsed-literal:: Aún no implementada la suma de dos vectores None Aún no implementada la suma de dos vectores None .. code:: python print(v1.producto(v2)) print(v1 * v2) .. parsed-literal:: Aún no implementado el producto interno de dos vectores None Aún no implementado el producto interno de dos vectores None .. code:: python print(v1.norma()) print(abs(v1)) .. parsed-literal:: Aún no implementada la norma del vector None Aún no implementada la norma del vector None Comparación ----------- Para comparar dos objetos usamos el método ``__eq__()`` para igualdad, ``__gt__()`` para ver si uno es mayor que otro, etc. En el siguiente ejemplo implementamos el método ``__eq__()`` y vemos su efecto. Antes de implementarlo tendremos .. code:: python v3 = Vector(1,2,3) v4 = Vector(1,2,3) print(v3 == v4) .. parsed-literal:: False .. code:: python class Vector(Punto): "Representa un vector en el espacio" def suma(self, v2): "Calcula un vector que contiene la suma de dos vectores" print("Aún no implementada la suma de dos vectores") # código calculando v = suma de self + v2 # ... def producto(self, v2): "Calcula el producto interno entre dos vectores" print("Aún no implementado el producto interno de dos vectores") # código calculando el producto interno pr = v1 . v2 def norma(self): "Devuelve la del vector" print("Aún no implementada la norma del vector") # código calculando el producto interno pr = v1 . v2 def __eq__(self, v2): return self.x == v2.x and self.y == v2.y and self.z == v2.z def __add__(self, v2): return self.suma(v2) def __mul__(self, v2): return self.producto(v2) def __abs__(self): return self.norma() .. code:: python v3 = Vector(1,2,3) v4 = Vector(1,2,3) print(v3 == v4) .. parsed-literal:: True -------------- Ejercicios 06 (b) ================= 2. Utilizando la definición de la clase ``Punto`` .. code:: python class Punto: "Clase para describir un punto en el espacio" num_puntos = 0 def __init__(self, x=0, y=0, z=0): "Inicializa un punto en el espacio" self.x = x self.y = y self.z = z Punto.num_puntos += 1 return None def __del__(self): "Borra el punto y actualiza el contador" Punto.num_puntos -= 1 def __str__(self): return f"Punto en el espacio con coordenadas: x = {self.x}, y = {self.y}, z = {self.z}" def __repr__(self): return f"Punto(x = {self.x}, y = {self.y}, z = {self.z})" def __call__(self): return self.__str__() @classmethod def total(cls): "Imprime el número total de puntos" print(f"En total hay {cls.num_puntos} puntos definidos") Complete la implementación de la clase ``Vector`` con los métodos pedidos .. code:: python class Vector(Punto): "Representa un vector en el espacio" def __add__(self, v2): "Calcula un vector que contiene la suma de dos vectores" print("Aún no implementada la suma de dos vectores") # código calculando v = suma de self + v2 # ... def __mul__(self, v2): "Calcula el producto interno entre dos vectores" print("Aún no implementado el producto interno de dos vectores") # código calculando el producto interno pr = v1 . v2 def __abs__(self): "Devuelve la distancia del punto al origen" print("Aún no implementado la norma del vector") # código calculando la magnitud del vector def angulo_entre_vectores(self, v2): "Calcula el ángulo entre dos vectores" print("Aún no implementado el ángulo entre dos vectores") angulo = 0 # código calculando angulo = arccos(v1 * v2 / (|v1||v2|)) return angulo def coordenadas_cilindricas(self): "Devuelve las coordenadas cilindricas del vector como una tupla (r, theta, z)" print("No implementada") def coordenadas_esfericas(self): "Devuelve las coordenadas esféricas del vector como una tupla (r, theta, phi)" print("No implementada") 3. **PARA ENTREGAR:** Cree una clase ``Polinomio`` para representar polinomios. La clase debe guardar los datos representando todos los coeficientes. El grado del polinomio será *menor o igual a 9* (un dígito). .. note:: Utilice el archivo **06_polinomio.py** en el directorio **data**, que renombrará de la forma usual ``06_Apellido.py``. Se le pide que programe: - Un método de inicialización ``__init__`` que acepte una lista de coeficientes. Por ejemplo para el polinomio :math:`4 x^3 + 3 x^2 + 2 x + 1` usaríamos: .. code:: python >>> p = Polinomio([1,2,3,4]) - Un método ``grado`` que devuelva el orden del polinomio .. code:: python >>> p = Polinomio([1,2,3,4]) >>> p.grado() 3 - Un método ``get_coeficientes``, que devuelva una lista con los coeficientes: .. code:: python >>> p.get_coeficientes() [1, 2, 3, 4] - Un método ``set_coeficientes``, que fije los coeficientes de la lista: .. code:: python >>> p1 = Polinomio() >>> p1.get_coeficientes() [] >>> p1.set_coeficientes([1, 2, 3, 4]) >>> p1.get_coeficientes() [1, 2, 3, 4] - El método ``__add__(self,pol2)`` que le sume otro polinomio y devuelva un polinomio (objeto del mismo tipo) Eso permitirá usar la operación de suma en la forma: .. code:: python >>> p1 = Polinomio([1,2,3,4]) >>> p2 = Polinomio([1,2,3,4]) >>> p1 + p2 - El método ``kmul(self, k)`` que multiplica al polinomio por una constante ``k`` y devuelve un nuevo polinomio - El método ``__mul__(self, pol2)`` que multiplica dos polinomios y devuelve un nuevo polinomio - Un método, ``derivada(n)``, que devuelva la derivada de orden ``n`` del polinomio (otro polinomio): .. code:: python >>> p1 = p.derivada() >>> p1.get_coeficientes() [2, 6, 12] >>> p2 = p.derivada(n=2) >>> p2.get_coeficientes() [6, 24] - Un método que devuelva la integral (antiderivada) del polinomio de orden ``n``, con constante de integración ``cte`` (otro polinomio). .. code:: python >>> p1 = p.integrada() >>> p1.get_coeficientes() [0, 1, 1, 1, 1] >>> >>> p2 = p.integrada(cte=2) >>> p2.get_coeficientes() [2, 1, 1, 1, 1] >>> >>> p3 = p.integrada(n=3, cte=1.5) >>> p3.get_coeficientes() [1.5, 1.5, 0.75, 0.16666666666666666, 0.08333333333333333, 0.05] - Un método ``from_string(expr)`` (pida ayuda si se le complica) que crea un polinomio desde un string en la forma: .. code:: python >>> p = Polinomio() >>> p.from_string('x^5 + 3x^3 - 2 x+x^2 + 3 - x') >>> p.get_coeficientes() [3, -3, 1, 3, 0, 1] >>> >>> p1 = Polinomio() >>> p1.from_string('y^5 + 3y^3 - 2 y + y^2+3', var='y') >>> p1.get_coeficientes() [3, -2, 1, 3, 0, 1] - Escriba un método llamado ``__str__``, que devuelva un string (que define cómo se va a imprimir el polinomio). Un ejemplo de salida será: .. code:: python >>> p = Polinomio([1,2.1,3,4]) >>> print(p) 4 x^3 + 3 x^2 + 2.1 x + 1 - Escriba un método llamado ``__call__``, de manera tal que al llamar al objeto, evalúe el polinomio en un dado valor de ``x`` .. code:: python >>> p = Polinomio([1,2,3,4]) >>> p(x=2) 49 >>> >>> p(0.5) 3.25 --------------