9. Programación con Objetos y Clases

Contenidos

  1. Modelación del mundo real con objetos.
  2. Definiendo y utilizando una nueva clase de objetos.
  3. Estructura general de un programa orientado a objetos.
  4. Método constructor: Creación de objetos/instancias de una clase.
  5. Creación de métodos e invocación de ellos.
  6. Sobrecarga de operadores.
  7. Ejemplos
  8. Ejercicios

9.1 Modelación del mundo real con objetos

En el mundo real no solo hay números enteros y texto; la realidad es más compleja y los objetos existentes muchas veces tienen varias características al mismo tiempo: por ejemplo, para describir una persona, no usaríamos solamente su edad, sino que querríamos también describirla según su nombre, su estatura y peso, su dirección, su género, su RUT, etc. Cada uno de esos datos puede ser guardado en una variable de Python. Por ejemplo, mira la siguiente figura:

Figura 9.1
Figura 9.1. Ejemplo de objetos.

Tenemos una persona llamada Fernanda, de 28 años, que vive en Los Jacintos 1329, de estatura 1.65 metros y que usa lentes. Cada uno de esos datos puede ser guardado en una variable, y la figura muestra de qué tipo sería la variable. Podríamos entonces, decir que la persona descrita se representa por esas 5 variables, algo así:

persona_nombre = “Fernanda”
persona_edad = 28
persona_direccion = “Los Jacintos 1329”
persona_estatura = 1.65
persona_lentes = True

Podríamos describir a otra persona usando las mismas características, pero cambiando los valores de cada variable, por ejemplo, a Héctor, de 20 años, que vive en Maratón 94 Dp. 31, mide 1.76 y no usa lentes.

Figura 9.2
Figura 9.2. Ejemplo de objetos 2.

Entonces, estamos representando a las personas por una agrupación de datos, o características, o atributos, sobre ellos. (Por supuesto, podríamos agregar una larga lista de atributos más, desde los mencionados antes, como peso, RUT, etc, hasta el grupo sanguíneo, sus gustos musicales, su puntaje de la PSU y todo lo que queramos incluir). La Programación Orientada a Objetos (Object-Oriented Programming, OOP) nos permite definir objetos que agrupen un conjunto de atributos. Por ejemplo, para el caso anterior, podremos definir una nueva clase de elemento de Python, que se llamará Persona, y que contendrá 5 atributos (nombre, edad, dirección, estatura, y uso de lentes). Un objeto es una instancia de la clase Persona, por lo tanto, Fernanda será un objeto de la clase Persona, y Héctor será otro objeto de la misma clase. ¡Podremos crear cuantos objetos queramos! Ademtodos), uqe permiten que los objetos hagan ciertas acciones. os queramos!rplo, para el caso anterior, podremos definir un nuevo ás de los atributos, los objetos tienen funciones (llamadas métodos), que permiten que los objetos hagan ciertas acciones. Por ejemplo, una Persona podría hacer la acción de cumplir años, por lo que tendría un método cumplir_años que aumentara su edad en 1. Es decir, si al objeto Fernanda le aplico el método cumplir_años, ahora Fernanda tendrá 29 años. Ahora que ya sabes de qué se tratan en general las clases y objetos, veamos un ejemplo más concreto.

9.2 Definiendo y utilizando una nueva clase de objetos

Una fracción es un número de partes de un entero, o “la expresión de una cantidad dividida entre otra cantidad” (Wikipedia). Por ejemplo, la de la figura siguiente es tres cuartos (3/4) de una torta.

Figura 9.3
Figura 9.3. Ejemplo de objetos 3.

Para definir una clase Fracción en Python, debemos preguntarnos dos cosas:

  1. ¿Cuáles son las características, o atributos, que tienen las fracciones? Es decir, si nos imaginamos cualquier fracción posible, ¿qué tienen todas en común?. La respuesta es: cualquier fracción tiene, por definición, un numerador y un denominador (y ambos son números enteros).
  2. ¿Qué acciones, o métodos, puede realizar una fracción? La respuesta aquí podría ser larga: las fracciones se pueden sumar, restar, multiplicar, dividir, simplificar, etc. Nuestra respuesta aquí depende, entonces, de cuál es el propósito que tiene el programa que estemos escribiendo. Por ahora, nos ocuparemos de los métodos sumar una fracción con otra y simplificar una fracción.

Ahora que ya sabemos lo que queremos hacer, veamos cómo escribirlo en Python. En este caso nuestro programa tendrá dos partes: la definición de la clase Fraccion, y el programa principal que usará la clase Fraccion para sumar algunas fracciones. Comenzaremos, entonces, por imaginarnos que ya tenemos una clase Fraccion que permite sumar y simplificar fracciones. Lo que nos gustaría es tener la capacidad de escribir un código como el siguiente:

f1 = Fraccion(1, 6)
f2 = Fraccion(3, 4)
h1 = f.sumar(g)
h2 = h1.simplificar()
print(h1)
print(h2)

Revisemos este código línea por línea, ya que es distinto a lo que hemos visto hasta ahora.

f1 = Fraccion(1,6) Fraccion(1,6): Crea un objeto de tipo Fraccion recibiendo los parámetros 1 y 6 (numerador y denominador respectivamente). f1 = Fraccion(1,6): Asigna el objeto recién creado a la variable f1
f2 = Fraccion(1,3) Al igual que la línea anterior, crea un segundo objeto de tipo Fraccion, pero ahora con los valores 3 y 4, por lo que representa la Fraccion 3/4. Luego, asigna ese objeto a la variable f2.
h1 = f1.sumar(f2) f1.sumar(f2): Aplica el método sumar a f1, recibiendo como parámetro f2, entregando como resultado una nueva Fraccion. h1 = f1.sumar(f2): Asigna el resultado de la suma a la variable h1, que debería ser un nuevo objeto de la clase Fraccion.
h2 = h1.simplificar() h1.simplificar(): Aplica el método simplificar a h1, entregando como resultado una nueva Fraccion. No recibe parámetros, pues al aplicar simplificar a h1, tendremos acceso a la información de h1, la fracción que queremos simplificar.
print(h1) Imprime h1, es decir, las fracciones f1 y f2 sumadas pero sin simplificar. En este caso sería 22/24.
print(h2) Imprime h2, es decir, h1 simplificado. En este caso sería 11/12.

Es importante notar que para utilizar un método de la clase Fraccion, siempre hay que aplicarlo a un objeto de la clase Fraccion, de la siguiente manera:

resultado = objeto.método(parámetros)

Por ejemplo, para h1 = f1.sumar(f2), f1 es el objeto sobre el cual se aplica el método sumar, recibiendo como parámetro la función f2, y entregando como resultado un nuevo objeto de tipo Fracción al que llamaremos h1. Ahora escribiremos paso por paso la clase Fraccion, que nos permitirá ejecutar el código anterior.

9.3 Estructura general de un programa orientado a objetos

Nuestro archivo Python tendrá dos partes: la primera, que es la definición de la clase Fraccion (que parte por “class Fraccion: … “ y que veremos a continuación cómo definir) y la segunda, que es el uso de la clase Fraccion que acabamos de revisar.

Figura 9.4
Figura 9.4. Revisión código objetos.

Ahora veremos la definición de la clase: sus atributos, su método constructor, todos los métodos que queramos definir, y la sobrecarga de operadores. Entonces, lo primero que habrá que escribir en el archivo Python es “class Fraccion:” (o el nombre de la clase que queramos definir) y luego, indentado, viene todo el código que pertenece a la clase y que discutiremos a continuación.

Una Fracción, como dijimos, tiene dos características o atributos: un numerador y un denominador, ambos de tipo número entero (int). Por ejemplo, la Fraccion 1/2 sería:

Figura 9.5
Figura 9.5. Ejemplo fracciones 1.

Y la fracción 23/73, sería:

Figura 9.6
Figura 9.6. Ejemplo fracciones 2.

9.4 Método constructor: Creación de objetos/instancias de una clase

Para crear un objeto de tipo Fracción, necesitamos de un método especial, llamado constructor, en el que definiremos todos los atributos que queremos que tengan los objetos que estamos creando (en el caso de la Fraccion, numerador y denominador). El método constructor es el que es llamado cuando en el código principal, decimos f1=Fraccion(1,2), es decir, ¡se llama utilizando el mismo nombre de la clase! Éste, como vemos, es un método muy especial, y todas las clases que vayas a crear deberían tenerlo, pues es el que dirá cómo se crean los objetos de la clase y cuáles atributos tendrán. Además, el método constructor tiene un nombre especial: siempre se llama __init__. Y, al igual que todos los métodos que pertenecen a una clase, su primer parámetro siempre será self, una referencia al objeto completo sobre el que estamos trabajando. El código del constructor sería el siguiente, y a continuación lo explicaremos línea por línea.

def __init__(self, n, d):
  self.numerador = n
  self.denominador = d
def __init__(self, n, d) El método constructor siempre se llama __init__ y siempre recibe self (una referencia al objeto completo) como primer parámetro. Además, recibiremos todos los parámetros necesarios para darle valores iniciales al objeto. Por ejemplo, al decir f1 = Fraccion(1,2), se recibe f1 como self, 1 como n, y 2 como d. Hasta este momento, self es un objeto que no contiene nada.
self.numerador = n Con esta línea, crearemos dentro de self una nueva variable llamada numerador, y le daremos el valor n (recibido como parámetro). Desde ahora en adelante, numerador es el nombre de un atributo del objeto self.
self.denominador = d Con esta línea, crearemos dentro de self una nueva variable llamada denominador, y le daremos el valor d (recibido como parámetro). Desde ahora en adelante, denominador es el nombre de un atributo del objeto self.

Entonces, con la instrucción, dentro del código principal, de f1 = Fraccion(1,6), lo que estamos creando es un objeto de tipo Fraccion, llamado f1, que internamente tiene dos atributos, numerador y denominador, y a los que podremos acceder utilizando el código f1.numerador y f1.denominador. Hasta el momento, podríamos tener el siguiente código completo:

class Fraccion:
    def __init__(self, n, d):
        self.numerador = n
        self.denominador = d

f = Fraccion(1, 6)
print(f.numerador) # imprime 1
print(f.denominador) # imprime 6

9.5 Creación de métodos e invocación de ellos

Los métodos que queramos crear vienen a continuación del constructor, y se definen de manera similar, pero ahora utilizaremos como nombre del método, el que nosotros hayamos elegido (por ejemplo, sumar, o simplificar). Dentro del método, al igual que una función, podemos incluir el código que se requiera (esto incluye cualquier instrucción válida de Python). El método sumar recibe (como todos los métodos de una clase) primero el parámetro self, que representa el objeto sobre el cual aplicaremos el método. Segundo, recibe other, que es una variable donde se recibirá la fracción que queremos sumar con self. Por ejemplo, si la llamada es f1.sumar(f2), dentro de la función, f1 se recibirá como self y f2 como other. En cambio, si la llamada es f2.sumar(f1), f2 se recibiría como self, y f1 como other. Dentro de la función, calcularemos el numerador y denominador del resultado (sin hacer ninguna simplificación) y luego, crearemos un nuevo objeto de tipo Fracción para retornar el resultado. Es importante notar que no es lo mismo retornar un objeto Fracción que retornar, por ejemplo, un string que contenga denominador y numerador, pues un objeto Fracción se podrá luego utilizar tal como una Fracción, es decir, se podrá sumar, simplificar, etc. El código de sumar podría ser el siguiente:

def sumar(self,other):
    resultado_numerador = self.numerador*other.denominador+self.denominador*other.numerador
    resultado.denominador = self.denominador*other.denominador
    resultado = Fraccion(resultado_numerador,resultado_denominador)
    return resultado

9.6 Sobrecarga de operadores

Ahora, ¿qué pasa, con todo el código anterior, se intentamos imprimir un objeto de la clase Fracción?. La verdad es que podría parecer decepcionante, pues Python no imprime nada legible que incluya ni el numerador ni el denominador (¡pruébalo!). Para que Python “sepa” imprimir una Fracción, debemos sobreescribir un método especial llamado :__str__, que solamente recibe el objeto self y debe retornar solamente un string, con la representación en texto del objeto. Por ejemplo, para que la fracción se imprima en el formato “numerador/denominador” (ejemplo: “1/2”, “56/57”), el código sería:

def __str__(self):
  return str(self.numerador)+”/”+str(self.denominador)

Si quisiéramos que la representación en texto fuera algo distinto, tendríamos que cambiar el código de __str__, por ejemplo, para escribir la fracción utilizando tres líneas:

def __str__(self):
  return str(self.numerador)+”\n---\n”+str(self.denominador)

Finalmente, recuerda que para sumar fracciones, requeríamos usar el código g=f1.sumar(f2). ¿No sería mucho mejor poder escribir la operación matemática directamente, por ejemplo g=f1+f2? Python permite esto, que se llama sobrecarga de operadores (operator overloading). Lo único que hay que hacer es cambiar el nombre del método sumar por un nombre predefinido, que en el caso de la suma es __add__. Puedes ver la lista de funciones en: https://docs.python.org/3.6/reference/datamodel.html#object.__add__ La función quedaría como sigue (¡nota que el código al interior de la función es el mismo que antes!):

def __add__(self,other):
  resultado_numerador = self.numerador * other.denominador + self.denominador * other.numerador
  resultado.denominador = self.denominador * other.denominador
  resultado = Fraccion(resultado_numerador, resultado_denominador)
  return resultado

9.7 Ejemplos

Hemos hablado durante este capítulo mayoritariamente de objetos de tipo Fraccion. Ahora, revisaremos ejemplos sobre lo aprendido considerando una clase para números complejos.

Ejemplo 9.1

En este ejemplo crearemos un nuevo tipo llamado NumeroComplejo. Este tipo tiene un atributo x para la coordenada en x e y para la coordenada en y. Representa un número complejo de la forma (x, y).

Ejemplo 9.2

Definir para la clase NumeroComplejo un método que permita imprimir una instancia de la clase. Recordemos que al intentar imprimir un tipo definido por nosotros, se imprime la dirección de memoria.

Ejemplo 9.3

En este ejemplo definiremos la función __str__ para la clase NumeroComplejo para poder imprimir usando la función print.

Ejemplo 9.4

En este ejemplo definiremos una función que compara dos números complejos, ya que si dos objetos distintos tienen sus atributos iguales, no se consideran iguales.

Ejemplo 9.5

En este ejemplo haremos un método que sume dos numeros complejos sin modificiar los objetos originales, ya que se retorna un nuevo numero NumeroComplejo.

Ahora, imagina que quieres implementar un objeto Bus, que lleva a un grupo de pasajeros y además tiene un chofer. Los pasajeros y el chofer son todos personas, por lo que primero crearemos una clase Persona para representarlos.

Ejemplo 9.6

Crea una clase persona. Sus atributos deben ser su nombre y su edad. Además crea un método cumpleaños, que aumente en 1 la edad de la persona.

Ejemplo 9.7

Para la clase anterior definir el método __str__. Debe retornar al menos el nombre de la persona.

Ejemplo 9.8

Extender la clase persona agregando un atributo saldo y un método transferencia(self, persona2, monto). El saldo representa el dinero que tiene la persona. El método transferencia hace que la Persona que llama el método le transfiera la cantidad monto al objeto persona2. Si no tiene el dinero suficiente no se ejecuta la acción.

Ejemplo 9.9

En este ejemplo extenderemos el programa anterior con una clase Bus. Como atributo tiene un arreglo de pasajeros (inicialmente vacío), una capacidad (se ingresa en el constructor) y un chofer. Debes implementar el método ingresar_chofer(self, persona), que recibe una persona y queda como chofer del Bus si es mayor de 18 años. Cabe destacar que el chofer no se ingresa en el constructor.

Ejemplo 9.10

Extender la clase Bus con el método subir_pasajero(self, persona). Este método sube a la persona al bus (i.e. La agrega al arreglo de asientos) siempre que el número de pasajeros en el Bus sea menor que la capcidad total.

9.8 Ejercicios

A continuación, te proponemos varios ejercicios para que los resuelvas en tu libro interactivo.

Ejercicio 9.1

Extiende la clase NumeroComplejo con el método resta, que retorna la resta de dos complejos.

Ejercicio 9.2

Crea los métodos get_x y get_y. El primero retorna el valor de x para un número complejo. El segundo retorna el valor de y

Ejercicio 9.3

Implementa la clase Recta. Esta clase debe tener dos atributos m y n que representan la pendiente y el coeficiente de posición respectivamente. Implementa además la función __str__ que imprima la recta. La recta es de la forma y = mx + n.

Ejercicio 9.4

Para la clase definida en el ejercicio anterior, implementar el método evaluar(self, x) que entrega el valor de y asociado a ese x. Debes copiar tu código de la respuesta anterior.

Ejercicio 9.5

Para la clase definida en el ejercicio anterior, implementar el método esta_en(self, x, y) que retorna True si el punto (x,y) es parte de la recta. False en caso contrario. Recuerda pegar el código de tu programa anterior.

Ejercicio 9.6

Crear una clase Vector, que recibe como atributo una lista de números que representa un vector. Implementa el método sumar_vector(self, vector2), que retorna un nuevo vector equivalente a la suma del vector que representa el objeto que llama al método y el vector representado por el objeto que es recibido como parámetro. En caso de no ser del mismo largo, se debe retornar None.

Ejercicio 9.7

Para la clase del ejercicio anterior implementar un método que retorne un número que representa el producto punto de dos vectores. Uno es quien llama al método, y el otro es el que se ingresa como parámetro. Recuerda volver a pegar el código de tu programa.

Ejercicio 9.8

Implementar una clase Matriz que como atributo posea una lista de listas de números. Debe contener un método sumar matrices que retorne un objeto de clase Matriz que sume dos matrices.

Ejercicio 9.9

Para la clase anterior, implementar un método iguales(self, m2) que determina si la Matriz que llama al método y la que es entregada como argumento representan la misma matriz. Recuerda pegar el programa de la pregunta anterior

Ejercicio 9.10

Para el programa del ejemplo de Personas y Buses (Que se encuentra abajo), se ha añadido un costo al Bus, que representa el costo del pasaje. Modifica el método subir_pasajero para que solo puedan subir personas con el dinero suficiente. El costo del pasaje se debe descontar del saldo.

Next Section - 10. Búsqueda y Ordenamiento