Curso de programación

Clase 11 : Programación orientada a objetos. Parte II

Clase 11

Contenido de la clase

Programación orientada a objetos

Repaso de conceptos vistos

  • ClaseEs como una una plantilla para crear objetos a partir de ella. En la clase de definen los métodos y variables.
  • ObjetoEs una instancia (como una imágen) de una clase que tendrá el comportamiento y los datos definidos en la clase de la que es instancia.
  • MétodosEs el código que implementa el comportamiento que tendrá el objeto. A fines prácticos, son funciones.
  • VariablesPueden ser variables de instancia o variables de clase. Contienen información que puede ser almacenada por las clases y/o las instancias.

Abstracción

Abstracción: Definición

La abstracción puede entenderse como el mecanismo que se usa al momento de analizar un elemento particular (puede ser un elemento de la vida real), desechando los aspectos no relevantes y considerando solo las propiedades esenciales para nuestro análisis. Es una forma de simplificar la complejidad de la realidad para facilitar el diseño y la implementación de sistemas de software.

Durante las prácticas hemos realizado una y otra vez el proceso de abstracción para poder entender un algo y poder así resaltar las características que son relevantes representar en un programa de computadora.

La abstracción nos ayuda también a desechar como se hacen procesos complejos y centrarnos en qué es que hacen.

Cuanto ya hemos identificado las características de un objeto que queremos representar (abstraer), nos queda concentrarnos en la interfaz, es decir, en como un objeto puede ser usado sin necesidad de conocer los detalles de su implementación.

Proceso de abstraccion

En el proceso de abstracción, vamos a identificar los siguientes componentes:

  • Clase y objeto
    • Una clase es una plantilla que define las propiedades y métodos que un tipo particular de objeto tendrá.
    • Un objeto es una instancia de una clase. Representa un ejemplo particular de un concepto abstracto.
  • Atributos y Métodos
    • Los atributos son las propiedades o características de un objeto. Representan el estado de un objeto.
    • Los métodos son las funciones o acciones que un objeto puede realizar. Representan el comportamiento de un objeto.

Proceso de abstraccion

  • Encapsulamiento
    • El encapsulamiento es una técnica de abstracción que implica ocultar los detalles internos de un objeto y proporcionar una interfaz controlada para interactuar con él.
  • Herencia
    • La herencia es una relación entre clases donde una clase (subclase o clase hija) puede heredar atributos y métodos de otra clase (superclase o clase padre).
    • La herencia permite la creación de jerarquías de clases que reflejan relaciones del mundo real.
  • Polimorfismo
    • El polimorfismo permite que objetos de diferentes clases respondan a métodos con el mismo nombre de manera específica para cada clase.
    • Esto simplifica la manipulación de objetos y permite tratarlos de manera más genérica.

Abstracción: En la práctica

Imaginemos que estamos creando un programa para gestionar una biblioteca. En este contexto, los libros son objetos importantes. Cada libro tiene un título, un autor, una fecha de publicación y una ubicación en la biblioteca. Acá es donde entra en juego la abstracción.

La abstracción en este caso consiste en identificar las propiedades y comportamientos esenciales de los libros, mientras se omiten los detalles innecesarios. Los detalles como el número de páginas, el tipo de encuadernación o la fecha exacta de adquisición podrían no ser relevantes en este contexto.

Abstracción: En la práctica

  • Identificar atributos relevantes:Título, autor, fecha de publicación, ubicación en la biblioteca
  • Identificar comportamientos relevantes:Prestar, devolver, mostrar detalles
  • Creación de una Clase "Libro":Basándonos en los atributos y comportamientos identificados, creamos una clase llamada "Libro" que encapsula estos aspectos. Los detalles como el número de páginas o la fecha de adquisición pueden no estar presentes en esta clase, ya que no son esenciales para el contexto de una biblioteca.

Ver un ejemplo

¿Preguntas?

Herencia

Herencia

En POO, se define herencia a la capacidad de crear clases que hereden el comportamiento (métodos) y atributos (variables) de otra clase. De esta manera, se introducen nuevos conceptos, clases padres o superclases y clases hijas o subclases.

Podemos decir entonces que la clase hija hereda los métodos y atributos de la clase padre. Aunque también puede tener atributos y métodos propios o los métodos de la clase padre reescritos como veremos mas adelante. Esto hace que las clases tengan una relación jerárquica entre las que comparten la herencia.

Imaginemos que podríamos tener una clase Animal para representar animales. Y dotar a la clase con atributos como nombre, especie, etc. y como métodos podemos tener comer, dormir, caminar, correr, etc.

A su vez podemos ser más específicos y definir la clase Gato para representar gatos en nuestro programa. La clase Gato puede ser una subclase de la clase Animal. Como un gato es a su vez un animal, es fácil imaginarnos que heredará los atributos y métodos de la superclase.

Uso de herencia

Hasta este punto podemos preguntarnos para qué sirve realmente la herencia o nos puede parecer de poca utilidad. Sin embargo, aporta mucha claridad y evita código repetido.

Puede ser útil cuando tengamos clases que se parecen entre sí pero tienen ciertas particularidades.

En vez de definir una clase por cada "animal", podemos tomar los elementos en común y crear una clase Animal de la que hereden el resto de los animales.

Esto, ayuda a aplicar el concepto de DRY (Don't Repeat Yourself) que consiste en no repetir código de manera innecesaria. Cuanto más código repetido haya, será mas difícil de mantener y habrá mas posibilidad de inconsistencia.

Veamos un ejemplo para profundizar el concepto

Herencia - Ejemplo

Los diferentes lenguajes tienen sus formas de indicar que una clase es hija de otra clase previamente definida. Se usan palabras claves como extends, subclass, inherits u otras similares.

En el caso de Python, lenguaje de referencia en este curso, simplemente se envía como parámetro la clase padre.


						# Superclase
						class Animal:
							pass

						# Subclase 1
						class Gato(Animal):
							pass

						# Subclase 2
						class Perro(Animal):
							pass
					

Herencia múltiple

El concepto de herencia múltiple, es que una clase puede heredar (o ser hija) de más de una clase. No todos los lenguajes soportan esta característica. Python, por su parte sí soporta herencia múltiple. La sintaxis es la siguiente:


						class Gato(Animal, Mamifero):
							pass
					

Otros lenguajes, como Java, al no soportar herencia múltiple suelen utilizar mecanismos como el concepto de interface. Una clase puede implementar múltiples interfaces, para lograr ciertos aspectos de herencia múltiple.

Se requiere de un buen análisis y diseño de clases para implementar herencia múltiple para no generar ambigüedad en el código.

¿Preguntas?

... seguimos...

El método especial super()

Algunas veces nos es útil acceder a un método de la superclase desde una subclase. Para eso existe un método especial llamado super().

Es algo muy habitual en la POO y los lenguajes con dichas características soportan un mecanismo para interactuar con la clase padre. Se suele usar el nombre super().metodo_padre() para referirnos al método de la superclase.

Ejemplo


						class Animal():
							def __init__(self, una_especie):
								self.especie = una_especie
					
						class Gato(Animal):
							def __init__(self):
								super().__init__('gato')
					

¿Qué pasa en este ejemplo? Ver código funcionando. ¿Vemos otro ejemplo?

Métodos y atributos en subclases

Las subclases pueden tener métodos o atributos que la superclase no tiene. Por ejemplo, la subclase Gato puede tener un método maullar() que la clase Animal no tiene.


						class Animal():
							pass
					
						class Gato(Animal):
							def maullar(self):
								print("¡Miau!")
					

Importante: La visibilidad de los métodos y atributos, es siempre desde arriba hacia abajo. Es decir, las subclases tienen acceso a los métodos y variables de las superclases, pero no es igual a la inversa.

Veamos un ejemplo mas completo.

La superclase padre de todas

En varios lenguajes de programación orientados a objetos, existe una clase de la cuál heredan todas las clases que puedan existir aunque no se especifiquen. Por lo general, esa clase se llama Object.

Dicha clase object tiene ciertos métodos y atributos definidos, como __str__, __repr__, __eq__, entre otros de las cuales podemos hacer uso.

También es común que las subclases de Object tengan sus propias implementación de dichos métodos y/o atributos.

Es posible que las subclases tengan sus propias implementaciones para los métodos como veremos a continuación...

Sobreescribiendo metodos

Una de las cualidades mas interesantes de la POO, además que las clases hijas pueden heredar los atributos y comportamiento de las clases padres, es que las clases hijas pueden realizar ciertas acciones a su manera. Es decir, no tiene por qué realizar una acción igual que la clase padre.

Volviendo a la clase Animal, en el siguiente ejemplo intentamos describir este concepto.


						class Animal():
							def hablar(self):
								pass
					
						class Perro(Animal):
							def hablar(self):
								print("¡Guau!")
					

Algo muy interesante que podemos hacer es sobreescribir el método __str__() en una clase. Lo veremos dos ejemplos; uno sin reescribir el método y otro ejemplo reescribiendo el método. Ref.

Esto nos da el pie para continuar con el próximo principio de la POO, polimorfismo.

Resumen de Herencia

  • Herencia: es un concepto que permite que una clase adquiera atributos y métodos de otra clase, lo que facilita la reutilización de código y la creación de relaciones jerárquicas entre clases.
  • Herencia múltiple: la capacidad que una clase herede de más de una clase. No todos los lenguajes lo soportan.
  • Método super: se usa para invocar a los métodos o atributos de la superclase.
  • Sobre escritura de métodos: las subclases pueden tener su propia implementación de los métodos. A veces pueden usar parcialmente la implementación de la superclase completando con una porción de código propia.

¿Preguntas?

Encapsulamiento

Encapsulamiento

El encapsulamiento o encapsulación en programación es un concepto relacionado con la POO, y hace referencia al ocultamiento de los estado internos (variables y métodos) de una clase hacia al exterior de la misma.

En Python, como en muchos lenguajes de programación orientados a objetos, el encapsulamiento se logra utilizando modificadores de acceso y propiedades.

Como hemos mencionado anteriormente, no es una buena práctica de programación acceder y modificar directamente los estados internos de una clase fuera de la misma.

Por lo tanto veremos como proteger los estados internos de una clase para que no sea posible modificarlos y proteger la integridad de nuestros objetos.

Modificadores de Acceso

En ciertos lenguajes es posible ocultar información que puede ser vista por otras clases, incluso con sus clases hijas. En C++ y Java se especifica usando las palabras claves:

  • Public: El atributo o método será accesible desde fuera de la clase.
  • Private: El atributo o método NO será accesible desde fuera de la clase.
  • Protected: El atributo o método será accesible solo desde las subclases.

Si bien, el curso se centra en Python, dichas palabras claves son importantes para entender el concepto.

Entonces.. ¿Qué quiere decir?. Básicamente es, podemos proteger por ejemplo un atributo de la clase, para que, una vez instanciada no pueda ser accedida ni modificada por un programa y solo se modifique de una manera controlada definida por nosotros en el código de la clase.

Modificadores de Acceso en Python

Python no tiene modificadores de acceso tradicionales como los que hemos mencionado. Sin embargo, sigue una convención para indicar la visibilidad de los atributos y métodos:

  • Atributos y métodos que comienzan con un guión bajo (_) se consideran "protected" y deben ser tratados como internos, aunque aún son accesibles desde fuera de la clase.
  • Atributos y métodos que comienzan con dos guiones bajos (__) se consideran "private" y se someten a un mecanismo de "name mangling", lo que hace que sean más difíciles de acceder desde fuera de la clase.

						class Prueba:
							def __init__(self):
								self.atributo_publico = 0
								self._atributo_protegido = 1
								self.__atributo_privado = 2
						p = Prueba()
						print (p.atributo_publico) # 0
						print (p._atributo_protegido) # 1
						print (p.__atributo_privado) # AttributeError:
							
					

Propiedades

Las propiedades son una forma de encapsulación que permite definir métodos especiales para acceder y modificar atributos de un objeto. Se utilizan para mantener el control sobre la forma en que los atributos son accedidos y modificados.

Las propiedades se definen utilizando los decoradores @property, @atributo.setter y @atributo.deleter.

Si bien los decoradores son parte mas avanzada de programación en Python que no entraremos en detalle en este momento, puede ver algo de información en este link.

Getters y Setters

Son un mecanismo para acceder a los atributos de la clase mediante métodos definidos por el programador. Esto le provee de la seguridad de que los datos serán correctos manteniendo la integridad de los objetos.

Aunque las propiedades son una forma más elegante de lograrlo, en Python también puedes utilizar métodos "getter" y "setter" para acceder y modificar atributos privados o protegidos. Esto permite un mayor control sobre la validación y manipulación de los valores.

  • getter: Viene del inglés get (obtener) y se usa para obtener el atributo que está encapsulado.
  • setter: Viene del inglés set (establecer o setear) y se usa para establecer el valor al atributo que está encapsulado.

A continuación veremos algunos ejemplos para compararlos

Getters y Setters (Ejemplo)


						class Persona:
							def __init__(self, nombre, edad):
								self.__nombre = nombre
								self.__edad = edad

							def get_edad(self):
								return self.__edad
								
							def set_edad(self, nueva_edad):
								if nueva_edad >= 0:
									self.__edad = nueva_edad
								else:
									print("La edad no puede ser negativa.")
					

Ver el ejemplo completo

Usando propiedades


						class Persona:
							def __init__(self, nombre, edad):
								self.__nombre = nombre
								self.__edad = edad
							@property
							def edad(self):
								return self.__edad	
							@edad.setter
							def edad(self, nueva_edad):
								if nueva_edad >= 0:
									self.__edad = nueva_edad
								else:
									print("La edad no puede ser negativa.")	
					

Ver el ejemplo completo

Beneficios del encapsulamiento

  • Ocultamiento de Detalles: Los detalles internos de la implementación se mantienen ocultos, lo que facilita cambios futuros sin afectar el código externo.
  • Control y Validación: Podemos aplicar lógica de validación y control en los métodos "setter" para garantizar que los valores asignados sean válidos.
  • Reusabilidad: Los cambios internos no afectan la interfaz pública, lo que mejora la reutilización de clases en diferentes contextos.

En resumen, el encapsulamiento en Python y en la POO en general se trata de limitar el acceso directo a los detalles internos de una clase y proporcionar métodos controlados para interactuar con esos detalles.

¿Preguntas?

Polimorfismo

Polimorfismo

El término polimorfismo tiene origen en las palabras poly (muchos) y morfo (formas) y aplicado a la programación hace referencia a que los objetos pueden tomar diferentes formas.

Se refiere a la capacidad de diferentes clases de responder a un mismo método de forma única, permitiendo tratar objetos de distintas clases de manera uniforme a través de una interfaz común.

Polimorfismo

Yendo a lo práctico, el polimorfismo nos dice que podemos tratar de la misma manera a varios objetos independientemente del tipo o instancia de la clase que sea.


						class Perro(Animal):
							def hacer_sonido(self):
								print("Guau!")
						class Gato(Animal):
							def hacer_sonido(self):
								print("Miau!")
					

						p = Perro()
						g = Gato()
						p.hacer_sonido() # Guau!
						g.hacer_sonido() # Miau!
					

Cada animal hace un sonido distinto pero el método tiene el mismo nombre. En lenguajes como Java, el polimorfismo es más estructurado y a menudo se apoya en la herencia para facilitar la interacción entre objetos de distintas clases. En Python también se puede lograr por herencia, pero su naturaleza dinámica el uso del polimorfismo es mas flexible.

Algunas ventajas del polimorfismo

  • Reutilización del código: El polimorfismo permite definir métodos genéricos que pueden ser utilizados por múltiples clases, lo que reduce la necesidad de duplicar código similar en diferentes lugares.
  • Eficacia en el Desarrollo: Al reducir la duplicación de código y permitir la reutilización, el polimorfismo agiliza el proceso de desarrollo y reduce la posibilidad de errores.
  • Mantenibilidad: Al utilizar el polimorfismo, si es necesario hacer cambios en la lógica de un método, solo se requiere hacerlo en un lugar, lo que facilita la actualización y mantenimiento del código.

Resumen

El polimorfismo en la programación orientada a objetos (POO) se refiere a la capacidad de diferentes clases de responder al mismo método de manera única, permitiendo tratar objetos de diferentes tipos a través de una interfaz común. Esto simplifica la reutilización de código, mejora la modularidad y permite la extensibilidad del software. Puede ser logrado mediante la herencia y la implementación de interfaces en lenguajes como Java, o de manera más flexible en lenguajes dinámicos como Python. El polimorfismo fomenta un diseño orientado a interfaces y abstracciones, lo que facilita la gestión de programas complejos y su adaptación a futuros cambios.

¿Preguntas?

Clases y métodos abstractos

Clases y métodos abstractos

Se suman nuevos conceptos relacionados con la abstracción como lo son clases abstractas y métodos abstractos.

Se define como método abstracto a un método que ha sido declarado pero no implementado. Es decir, que no tiene código.

Se define como clase abstracta la que contiene métodos abstractos y esta NO puede ser instanciada.

¿Y para qué puede servir una clase abstracta si no puede ser instanciada? ¿Para qué puede servir un método que no tiene código?

Una clase abstracta sirve para definir la interfaz de las clases hijas al igual que los métodos abstractos, sirven para estandarizar la interfaz de los objetos obligándonos a implementar los métodos abstractos en las clases hijas.

Clases abstractas - Ejemplo

Supongamos que tenemos una superclase Vehículo con tres subclases, Auto, Camioneta y Moto.

Si no definimos la clase Vehículo como abstracta podrán haber instancias de Vehículo que NO serán un Auto, o Camioneta o Moto y tal vez no queramos que eso ocurra.

Clases abstractas en Python

Para poder crear clases abstractas en Python es necesario importar la clase ABC y el decorador abstractmethod del módulo abc (Abstract Base Classes). Ref.


						from abc import ABC, abstractmethod
						class Vehiculo(ABC):
							
							@abstractmethod
							def arrancar(self):
								pass

						v = Vehiculo() # Error!
					

						TypeError: Can't instantiate abstract class Vehiculo with abstract methods arrancar
					

En Python, una clase abstracta es la que contiene métodos abstractos. Las clases abstractas no se pueden instanciar.

Métodos abstractos

Los métodos abstractos nos obligan a implementar los métodos en cada subclase. Si no los implementamos, dará error.


						from abc import ABC, abstractmethod
						class Vehiculo(ABC):

							@abstractmethod
							def arrancar(self):
								pass

						class Auto(Vehiculo):
							pass # Error!
					

En ese ejemplo el intérprete dará error porque el método arrancar() no está implementado en la subclase Auto.

Métodos abstractos (P2)

Para implementar un método abstracto, en la subclase debe ir el nombre del método con sus parámetros sin el decorator @abstractmethod.


						from abc import ABC, abstractmethod
						class Vehiculo(ABC):

							@abstractmethod
							def arrancar(self):
								pass

						class Auto(Vehiculo):
							# Se implementa el método abstracto
							def arrancar(self):  
								print("ruuunnn") 
					

¿Preguntas?

Ejercicios en clase

Animales y Sonidos

Crea una jerarquía de clases para representar diferentes animales. Define una clase base "Animal" con atributos como nombre y tipo. Luego, crea subclases como "Perro" y "Gato" que hereden de la clase base. Implementa un método "hacer_sonido" en cada subclase y utiliza el polimorfismo para que cada tipo de animal haga un sonido único cuando se llama a este método.

Figuras geométricas

Crea una jerarquía de clases para representar diferentes figuras geométricas (por ejemplo, círculos y rectángulos). Utiliza herencia para crear una clase base "Figura" y luego subclases específicas como "Círculo" y "Rectángulo". Implementa métodos para calcular áreas y perímetros, y utiliza propiedades para acceder a atributos como el radio y el lado.

¡Fin de la clase!

Ir al inicio
Exportar clase a PDF

Bibliografía