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.
En el proceso de abstracción, vamos a identificar los siguientes componentes:
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.
Ver un ejemplo
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.
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
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
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.
... seguimos...
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.
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?
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.
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...
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.
super: se usa para invocar a los métodos o atributos de la superclase.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.
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:
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.
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:
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:
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.
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.
A continuación veremos algunos ejemplos para compararlos
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
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
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.
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.
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.
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.
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.
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.
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.
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.
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")
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.
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.