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

print("hola")
hola

mientras que, si escribimos algo que no está permitido en el lenguaje

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

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.

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.

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:

import math
numeros = [i**2 for i in range(10)]
numeros.append(-1)
numeros.append("121")
for j in numeros:
    print(f"La raíz cuadrada de {j} es {math.sqrt(j)}")
print("Terminé de calcular todo")
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:

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")
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:

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")
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:

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")
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
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")
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:

import math
def mi_sqrt(x):
  if x < 0:
    raise ValueError(f"x = {x}, debería ser positivo")
  return math.sqrt(x)
mi_sqrt(12)
3.4641016151377544
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:

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:

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)
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
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:

try:
    mi_sqrt(-2)
except(ValueError):
    print("Argumento negativo!!!")
Argumento negativo!!!
try:
    mi_sqrt(2+2j)
except(TypeError):
    print("Tipo incorrecto!!!")
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:

class NuevaClase:
    pass
c1 = NuevaClase()
c1
<__main__.NuevaClase at 0x7f37f0bd4050>
help(c1)
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
dir(c1)
['__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__']
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
P1 = Punto(0.5, 0.7, 0)
P1
<__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”:

P1.x, P1.z
(0.5, 0)
print(P1)
<__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.

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:

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))
P1 = Punto(0.5, 0.5)
P1.angulo_azimuthal()
45.0
P2 = Punto()
P2.x
0
help(P1)
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:

P1.__dict__
{'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:

pp = Punto(0.1, "s", [1,2])
pp
<__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:

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
print(P1.angulo_azimuthal())
print(Punto.angulo_azimuthal(P1))
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.

Nota

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:

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.

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:

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:

v1 = Vector(2,3.1)
v2 = Vector()
v1
<__main__.Vector at 0x7f37f0bd67b0>
v1.x, v1.y, v1.z
(2, 3.1, 0)
v2.x, v2.y, v2.z
(0, 0, 0)
v1.angulo_azimuthal()
57.171458208587474
v = v1.suma(v2)
Aún no implementada la suma de dos vectores
print(v)
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:

      \[\sqrt{x^2 + y^2 + z^2}\]

    Su uso será el siguiente:

    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

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)
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:

print(p1.x, p2.x)
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

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.

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)
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)

del p1
print(p1)
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

Cell In[7], line 1
----> 1 print(p1)


NameError: name 'p1' is not defined
print(Punto.num_puntos)
2
del p2
print(Punto.num_puntos)
2

Nuestra implementación tiene una falla, al borrar los objetos no actualiza el contador, descontando uno cada vez.

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.

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)
Número de puntos: 0
<__main__.Punto object at 0x7f0194749010> <__main__.Punto object at 0x7f01946c3610>
Número de puntos: 2
print('Número de puntos:', Punto.num_puntos)

p1.borrar()
print('Número de puntos:', Punto.num_puntos)
Número de puntos: 2
Número de puntos: 1
p1, p2
(<__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:

print(f"{p1 = }")
print(f"{p1.x = }")
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.

del p1
del p2

Podemos utilizar esto para implementar la actualización del contador de puntos

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
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)
Número de puntos: 2
Número de puntos: 1
p1
<__main__.Punto at 0x7f019474a120>
p2
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

Cell In[20], line 1
----> 1 p2


NameError: name 'p2' is not defined
print(p1)
<__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.

p1 = Punto(1,1,1)
print(p1)
<__main__.Punto object at 0x7f01946c39d0>

Rehagamos la clase para definir puntos

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
p1 = Punto(1,1,0)
print(p1)
Punto en el espacio con coordenadas: x = 1, y = 1, z = 0
ss = 'p1 = {}'.format(p1)
ss
'p1 = Punto en el espacio con coordenadas: x = 1, y = 1, z = 0'
p1.x, p1.y, p1.z
(1, 1, 0)
p1
<__main__.Punto at 0x7f019474a270>
str(p1)
'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.

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})"
p2 = Punto(0.3, 0.3, 1)
p2
Punto(x = 0.3, y = 0.3, z = 1)
p2.x = 5
p2
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:

p2()
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

Cell In[35], line 1
----> 1 p2()


TypeError: 'Punto' object is not callable
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")
p3 = Punto(1,3,4)
p3
Punto(x = 1, y = 3, z = 4)
p3()
'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

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()
v1 = Vector(1,3,3)
v2 = Vector(2,-1,-2)
print(v1.suma(v2))
print(v1 + v2)
Aún no implementada la suma de dos vectores
None
Aún no implementada la suma de dos vectores
None
print(v1.producto(v2))
print(v1 * v2)
Aún no implementado el producto interno de dos vectores
None
Aún no implementado el producto interno de dos vectores
None
print(v1.norma())
print(abs(v1))
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

v3 = Vector(1,2,3)
v4 = Vector(1,2,3)
print(v3 == v4)
False
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()
v3 = Vector(1,2,3)
v4 = Vector(1,2,3)
print(v3 == v4)
True

Ejercicios 06 (b)

  1. Utilizando la definición de la clase Punto

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

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")
  1. 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).

    Nota

    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 \(4 x^3 + 3 x^2 + 2 x + 1\) usaríamos:

>>> p = Polinomio([1,2,3,4])
  • Un método grado que devuelva el orden del polinomio

>>> p = Polinomio([1,2,3,4])
>>> p.grado()
3
  • Un método get_coeficientes, que devuelva una lista con los coeficientes:

>>> p.get_coeficientes()
[1, 2, 3, 4]
  • Un método set_coeficientes, que fije los coeficientes de la lista:

>>> 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:

>>> 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):

>>> 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).

>>> 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:

>>> 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á:

>>> 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

>>> p = Polinomio([1,2,3,4])
>>> p(x=2)
49
>>>
>>> p(0.5)
3.25