Programación Orientada a Objetos
La Programación Orientada a Objetos (POO) es un paradigma de programación que se ha vuelto indispensable en la actualidad. Este enfoque modela elementos del mundo real como “objetos” que tienen propiedades y comportamientos, lo cual permite crear programas más intuitivos y fáciles de mantener. En este artículo veremos los conceptos básicos de POO y sus ventajas frente a otros paradigmas como la programación procedural. ¡Empecemos!
Este paradigma se basa en dos conceptos fundamentales:
- Objetos: entidades que combinan estado (datos) y comportamiento (operaciones) en una misma unidad. Por ejemplo, un objeto “coche” tendría propiedades como color, número de puertas, velocidad máxima, etc. Y comportamientos como acelerar, frenar, girar, etc.
- Clases: especificaciones que definen la estructura y comportamiento común de un grupo de objetos. La clase “coche” serviría como molde para crear objetos coche con las mismas características.
Como explica el programador Alan Kay, uno de los creadores de la POO:
“La idea central de POO es que los usuarios deben manipular objetos conceptuales más que máquinas de Turing. Las interfaces con el mundo real deben, por lo tanto, ser construidas en términos de objetos conceptuales.”
Es decir, la POO modela conceptualmente elementos del mundo real para hacer la programación más intuitiva.
Paradigmas de programación
Antes de profundizar en la POO, conviene entender que existen diferentes paradigmas o enfoques para abordar la programación. Los principales son:
Programación procedural
Secuencia ordenada de instrucciones que el programa debe seguir paso a paso. El foco está en procedimientos y funciones. Por ejemplo, C es un lenguaje orientado a la programación procedural.
La programación procedural es mejor para:
- Problemas sencillos o algoritmos secuenciales.
- Código que no necesitará reusarse ni expandirse mucho.
- Casos donde el rendimiento y eficiencia son críticos.
Programación orientada a objetos
Modelo basado en objetos que contienen datos y código en unidades cohesivas. El foco está en las clases y en la interacción entre objetos. Por ejemplo, Java y Python son lenguajes orientados a objetos.
La POO permite modelar de forma más directa elementos del mundo real, encapsular mejor los datos y reutilizar código a través de la herencia entre clases.
Las principales ventajas de POO frente a la programación procedural son:
- Modularidad: los objetos agrupan datos y operaciones relacionadas, encapsulando la complejidad interna. Esto permite trabajar con módulos independientes.
- Ocultación de información: Los objetos pueden exponer una interfaz simple y ocultar detalles de implementación internos. Esto reduce acoplamientos.
- Reusabilidad: Las clases permiten reuse de código. Una clase abstracta puede heredar a múltiples subclases.
- Extensibilidad: Podemos extender el comportamiento de clases padres creando nuevas subclases.
- Mapeo conceptual: Los objetos representan entidades del mundo real, lo cual facilita la traducción de requerimientos a código.
Sin embargo, la POO también tiene desventajas. Según el programador Paul Graham:
“La programación orientada a objetos suele ser una molestia. Hace que las cosas sean más difíciles de lo que deberían ser.”
Por ejemplo, para problemas simples la POO puede resultar excesiva. Y en proyectos grandes existe el riesgo de abusar de la herencia y el polimorfismo, volviendo el código difícil de seguir.
En definitiva, la POO es más adecuada cuando:
- El problema a modelar tiene entidades claras y estructuradas.
- Queremos reutilizar código encapsulado en clases modulares.
- Trabajamos en sistemas que deben extenderse y mantenerse con facilidad.
Mas artículos
1 - Clases y Objetos
En programación orientada a objetos, las clases y objetos son los conceptos centrales para entender cómo modelamos elementos de la realidad y definimos su estructura y comportamiento dentro del software. Veamos en detalle la anatomía de una clase, cómo crear objetos a partir de ella para usar sus propiedades y métodos, y otros detalles clave de su relación.
Anatomía de una clase
Una clase actúa como un plano o molde para construir objetos similares, definiendo sus características comunes y funcionalidades. Es similar al plano para construir casas de un mismo barrio: todas comparten ciertos atributos clave.
Los componentes típicos de una clase son:
Atributos (propiedades): Variables que caracterizan al objeto. Por ejemplo, para una clase Persona
, atributos como nombre
, edad
, DNI
, etc.
class Persona:
dni = ""
nombre = ""
edad = 0
Métodos: Funciones que definen comportamientos. Por ejemplo, una Persona
puede caminar()
, hablar()
, comer()
, etc. Acceden a los atributos para implementar dicha funcionalidad.
Constructor: Método especial __init__()
que se ejecuta al instanciar la clase y permite inicializar los atributos.
Destructor: Método __del__()
que se ejecuta al eliminar la instancia liberando recursos. Opcional en algunos lenguajes.
Creando objetos
A partir de la clase generamos objetos, que son instancias concretas con sus propios atributos definidos. Digamos que la clase Casa es el plano, y una casa específica en una calle determinada es el objeto.
En código creamos un objeto invocando la clase como si fuera un método:
# Clase Persona
class Persona:
def __init__(self, n, e):
self.nombre = n
self.edad = e
# Objeto Persona específico
pepe = Persona("Pepe", 30)
juan = Persona("Juan", 35)
Cada objeto comparte la estructura y comportamiento general, pero puede almacenar distintos datos.
Utilizando Propiedades y Métodos
Ya tenemos una clase Persona
y un objeto pepe
de tipo Persona
. ¿Cómo interactuamos con el objeto?
- Propiedades: Es posible acceder al valor de un atributo del objeto utilizando la referencia al objeto (
pepe
) y el nombre del atributo.
pepe.nombre # "Pepe"
pepe.edad # 30
- Métodos: De la misma manera en la que se accede a los atributos pero agregando un paréntesis dentro del cual se pasan los argumentos si es que recibe alguno.
# Clase Persona
class Persona:
def __init__(self, n, e):
self.nombre = n
self.edad = e
def comer(self, comida):
print(f"Comiendo {comida}")
# Objeto Persona específico
pepe = Persona("Pepe", 30)
pepe.comer("pizza") # Imprime "Comiendo pizza"
El objeto pepe tiene ahora estado (propiedades) y comportamiento (métodos) propios.
Self vs This
Un detalle importante en los métodos es cómo acceden a los atributos y otros métodos del objeto. Aquí entra otra diferencia entre lenguajes:
- Self: En Python, los atributos y métodos se acceden dentro de la clase anteponiendo
self
. Esto apunta al objeto instanciado.
class Persona:
def __init__(self, nombre):
self.nombre = nombre
def saludar(self):
print(f"Hola! Soy {self.nombre}")
juan = Persona("Juan")
juan.saludar()
# Imprime "Hola! Soy Juan"
- This: En Java o C#, se utiliza
this
en lugar de self. Cumple la misma funcionalidad de apuntar a los miembros del objeto.
public class Person {
private String nombre;
public Person(String nombre) {
this.nombre= nombre;
}
public void saludar() {
System.out.println("Hola! Soy " + this.nombre);
}
}
Person juan = new Person("Juan");
juan.saludar();
// Imprime "Hola! Soy Juan"
Conclusión
Las clases y objetos son los conceptos clave de la POO, permitiendo modelar entidades de la realidad y generar componentes modulares y genéricos de nuestro sistema para construir programas más robustos y fáciles de entender y mantener.
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples-programming.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
2 - Los cuatro pilares
En el corazón de la Programación Orientada a Objetos (POO) se encuentran cuatro conceptos fundamentales: Encapsulamiento, Herencia, Polimorfismo y Abstracción. Estos conceptos, conocidos como los “cuatro pilares” de la POO, constituyen la base sobre la cual se construyen sistemas de software complejos. En esta guía, profundizaremos en cada uno de estos conceptos, explorando sus definiciones, implementaciones y aplicaciones prácticas. Utilizaremos Python, un lenguaje reconocido por su claridad y versatilidad, para demostrar estos conceptos en acción. Ya seas un principiante que comienza su viaje en la programación o un profesional experimentado que busca refrescar sus conocimientos, este artículo tiene como objetivo proporcionar información valiosa y una comprensión más profunda de los principios de la POO.
2.1 - Encapsulamiento
El encapsulamiento es frecuentemente descripto como el primer pilar de la programación orientada a objetos. Es el mecanismo de agrupar los datos (atributos) y los métodos (funciones) que operan sobre esos datos dentro de una única unidad u objeto. Este concepto también se conoce como ocultamiento de datos, ya que la representación interna del objeto se oculta al mundo exterior.
La importancia del encapsulamiento radica en varios aspectos clave:
- Protección de datos: Al controlar el acceso a los datos del objeto a través de métodos, podemos asegurar que los datos se mantengan consistentes y válidos.
- Modularidad: El encapsulamiento permite que los objetos sean autocontenidos, facilitando la comprensión y el mantenimiento del código.
- Flexibilidad: La implementación interna puede ser modificada sin afectar otras partes del código que utilizan el objeto.
- Reducción de complejidad: Al ocultar los detalles del funcionamiento interno, el encapsulamiento reduce la complejidad del sistema desde una perspectiva externa.
Implementación en Python
Python ofrece varios mecanismos para implementar el encapsulamiento. Exploremos estos con ejemplos:
1. Uso de atributos privados
En Python, podemos crear atributos privados prefijando el nombre del atributo con doble guion bajo (__
). Esto activa el “name mangling”, que hace que el atributo sea más difícil de acceder desde fuera de la clase.
class CuentaBancaria:
def __init__(self, numero_cuenta, saldo):
self.__numero_cuenta = numero_cuenta # Atributo privado
self.__saldo = saldo # Atributo privado
def depositar(self, cantidad):
if cantidad > 0:
self.__saldo += cantidad
return True
return False
def retirar(self, cantidad):
if 0 < cantidad <= self.__saldo:
self.__saldo -= cantidad
return True
return False
def obtener_saldo(self):
return self.__saldo
# Uso
cuenta = CuentaBancaria("1234567890", 1000)
print(cuenta.obtener_saldo()) # Salida: 1000
cuenta.depositar(500)
print(cuenta.obtener_saldo()) # Salida: 1500
cuenta.retirar(200)
print(cuenta.obtener_saldo()) # Salida: 1300
# Esto generará un AttributeError
# print(cuenta.__saldo)
En este ejemplo:
__numero_cuenta
y __saldo
son atributos privados.- Proporcionamos métodos públicos (
depositar
, retirar
, obtener_saldo
) para interactuar con estos atributos privados. - El acceso directo a
__saldo
desde fuera de la clase generará una excepción AttributeError
.
2. Uso de propiedades
El decorador @property
de Python nos permite definir métodos que pueden ser accedidos como atributos, proporcionando una forma más pythonica de implementar getters y setters.
class Circulo:
def __init__(self, radio):
self._radio = radio
@property
def radio(self):
return self._radio
@radio.setter
def radio(self, valor):
if valor > 0:
self._radio = valor
else:
raise ValueError("El radio debe ser positivo")
@property
def area(self):
return 3.14159 * self._radio ** 2
# Uso
circulo = Circulo(5)
print(circulo.radio) # Salida: 5
print(circulo.area) # Salida: 78.53975
circulo.radio = 7
print(circulo.radio) # Salida: 7
print(circulo.area) # Salida: 153.93791
# Esto generará un ValueError
# circulo.radio = -1
En este ejemplo:
_radio
es un atributo protegido (el guion bajo simple es una convención para atributos protegidos en Python).- La propiedad
radio
proporciona acceso de lectura y escritura a _radio
con validación. - La propiedad
area
es de solo lectura y se calcula al vuelo.
Beneficios y mejores prácticas
Los beneficios del encapsulamiento son numerosos:
- Mejora de la mantenibilidad: Los cambios en la implementación interna no afectan al código externo que utiliza la clase.
- Mayor seguridad: Los atributos privados no pueden ser modificados accidentalmente desde fuera de la clase.
- Flexibilidad en la implementación: Puedes cambiar cómo se almacenan o calculan los datos sin cambiar la interfaz pública.
- Mejor abstracción: Los usuarios de la clase no necesitan conocer su funcionamiento interno.
Las mejores prácticas para el encapsulamiento en Python incluyen:
- Usar atributos privados (prefijo de doble guion bajo) para datos que no deben ser accedidos directamente desde fuera de la clase.
- Proporcionar métodos públicos o propiedades para el acceso controlado a los datos internos.
- Usar propiedades en lugar de métodos get/set para un enfoque más propio del paradigma Python.
- Documentar claramente la interfaz pública, incluyendo cualquier efecto secundario de los métodos.
Veamos un ejemplo más complejo que demuestra estas prácticas:
class Empleado:
def __init__(self, nombre, salario):
self.__nombre = nombre
self.__salario = salario
self.__proyectos = []
@property
def nombre(self):
return self.__nombre
@property
def salario(self):
return self.__salario
@salario.setter
def salario(self, valor):
if valor > 0:
self.__salario = valor
else:
raise ValueError("El salario debe ser positivo")
def agregar_proyecto(self, proyecto):
"""
Agrega un proyecto a la lista de proyectos del empleado.
:param proyecto: cadena que representa el nombre del proyecto
"""
self.__proyectos.append(proyecto)
def eliminar_proyecto(self, proyecto):
"""
Elimina un proyecto de la lista de proyectos del empleado.
:param proyecto: cadena que representa el nombre del proyecto
:return: True si el proyecto fue eliminado, False si no se encontró
"""
if proyecto in self.__proyectos:
self.__proyectos.remove(proyecto)
return True
return False
@property
def cantidad_proyectos(self):
return len(self.__proyectos)
def __str__(self):
return f"Empleado: {self.__nombre}, Salario: ${self.__salario}, Proyectos: {self.cantidad_proyectos}"
# Uso
emp = Empleado("Juan Pérez", 50000)
print(emp.nombre) # Salida: Juan Pérez
print(emp.salario) # Salida: 50000
emp.agregar_proyecto("Proyecto A")
emp.agregar_proyecto("Proyecto B")
print(emp.cantidad_proyectos) # Salida: 2
emp.salario = 55000
print(emp) # Salida: Empleado: Juan Pérez, Salario: $55000, Proyectos: 2
emp.eliminar_proyecto("Proyecto A")
print(emp.cantidad_proyectos) # Salida: 1
# Esto generará un AttributeError
# print(emp.__proyectos)
Este ejemplo demuestra:
- Atributos privados (
__nombre
, __salario
, __proyectos
) - Propiedades para acceso controlado (
nombre
, salario
, cantidad_proyectos
) - Métodos públicos para manipular datos privados (
agregar_proyecto
, eliminar_proyecto
) - Documentación clara del comportamiento de los métodos
- Un método
__str__
personalizado para una representación de cadena agradable del objeto
Siguiendo estas prácticas, creamos una clase que es flexible y robusta, encarnando el principio de encapsulamiento.
Referencias
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples-programming.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
2.2 - Herencia
La herencia es un concepto fundamental en la programación orientada a objetos que permite que una nueva clase se base en una clase existente. La nueva clase, conocida como clase derivada o clase hija, hereda atributos y métodos de la clase existente, llamada clase base o clase padre. Este mecanismo promueve la reutilización de código y establece una relación entre clases.
Los aspectos clave de la herencia incluyen:
- Reutilización de código: La herencia permite reutilizar código de clases existentes, reduciendo la redundancia y promoviendo un desarrollo eficiente.
- Clasificación jerárquica: Permite la creación de jerarquías de clases, representando relaciones y características comunes entre objetos.
- Extensibilidad: Se puede agregar nueva funcionalidad a las clases existentes sin modificarlas, siguiendo el principio abierto-cerrado.
- Polimorfismo: La herencia es un prerrequisito para el polimorfismo en tiempo de ejecución (que discutiremos en detalle más adelante).
Tipos de herencia
Existen varios tipos de herencia, aunque no todos los lenguajes de programación admiten todos los tipos. Los principales tipos son:
- Herencia simple: Una clase derivada hereda de una sola clase base.
- Herencia múltiple: Una clase derivada hereda de múltiples clases base.
- Herencia multinivel: Una clase derivada hereda de otra clase derivada.
- Herencia jerárquica: Múltiples clases derivadas heredan de una sola clase base.
- Herencia híbrida: Una combinación de dos o más tipos de herencia.
Python admite todos estos tipos de herencia. Exploremos cada uno con ejemplos.
Herencia simple
La herencia simple es la forma más básica de herencia, donde una clase hereda de una sola clase base.
class Animal:
def __init__(self, especie):
self.especie = especie
def hacer_sonido(self):
pass
class Perro(Animal):
def __init__(self, nombre):
super().__init__("Canino")
self.nombre = nombre
def hacer_sonido(self):
return "¡Guau!"
# Uso
perro = Perro("Buddy")
print(f"{perro.nombre} es un {perro.especie}") # Salida: Buddy es un Canino
print(perro.hacer_sonido()) # Salida: ¡Guau!
En este ejemplo:
Animal
es la clase base con un método genérico hacer_sonido
.Perro
es derivado de Animal
, heredando sus atributos y métodos.Perro
sobrescribe el método hacer_sonido
con su propia implementación.- Usamos
super().__init__()
para llamar al inicializador de la clase base.
Herencia múltiple
La herencia múltiple permite que una clase herede de múltiples clases base. Algunos lenguajes no permiten este tipo de herencia.
class Volador:
def volar(self):
return "¡Puedo volar!"
class Nadador:
def nadar(self):
return "¡Puedo nadar!"
class Pato(Animal, Volador, Nadador):
def __init__(self, nombre):
Animal.__init__(self, "Ave")
self.nombre = nombre
def hacer_sonido(self):
return "¡Cuac!"
# Uso
pato = Pato("Donald")
print(f"{pato.nombre} es un {pato.especie}") # Salida: Donald es un Ave
print(pato.hacer_sonido()) # Salida: ¡Cuac!
print(pato.volar()) # Salida: ¡Puedo volar!
print(pato.nadar()) # Salida: ¡Puedo nadar!
Aquí, Pato
hereda de Animal
, Volador
y Nadador
, combinando atributos y métodos de las tres clases.
Herencia multinivel
En la herencia multinivel, una clase derivada hereda de otra clase derivada.
class Mamifero(Animal):
def __init__(self, especie, es_de_sangre_caliente=True):
super().__init__(especie)
self.es_de_sangre_caliente = es_de_sangre_caliente
def dar_a_luz(self):
return "Dando a luz crías vivas"
class Gato(Mamifero):
def __init__(self, nombre):
super().__init__("Felino")
self.nombre = nombre
def hacer_sonido(self):
return "¡Miau!"
# Uso
gato = Gato("Bigotes")
print(f"{gato.nombre} es un {gato.especie}") # Salida: Bigotes es un Felino
print(gato.hacer_sonido()) # Salida: ¡Miau!
print(gato.dar_a_luz()) # Salida: Dando a luz crías vivas
print(f"¿Es de sangre caliente? {gato.es_de_sangre_caliente}") # Salida: ¿Es de sangre caliente? True
En este ejemplo, Gato
hereda de Mamifero
, que a su vez hereda de Animal
, formando una cadena de herencia multinivel.
Herencia jerárquica
La herencia jerárquica implica múltiples clases derivadas heredando de una sola clase base.
class Ave(Animal):
def __init__(self, especie, puede_volar=True):
super().__init__(especie)
self.puede_volar = puede_volar
class Loro(Ave):
def __init__(self, nombre):
super().__init__("Psitácido", puede_volar=True)
self.nombre = nombre
def hacer_sonido(self):
return "¡Squawk!"
class Pinguino(Ave):
def __init__(self, nombre):
super().__init__("Esfenisciforme", puede_volar=False)
self.nombre = nombre
def hacer_sonido(self):
return "¡Honk!"
# Uso
loro = Loro("Polly")
pinguino = Pinguino("Pingu")
print(f"{loro.nombre} puede volar: {loro.puede_volar}") # Salida: Polly puede volar: True
print(f"{pinguino.nombre} puede volar: {pinguino.puede_volar}") # Salida: Pingu puede volar: False
Aquí, tanto Loro
como Pinguino
heredan de Ave
, lo que demuestra la herencia jerárquica.
Herencia híbrida
La herencia híbrida es una combinación de múltiples tipos de herencia. Veamos un ejemplo más complejo para ilustrar esto:
class Terrestre:
def caminar(self):
return "Caminando en tierra"
class Acuatico:
def nadar(self):
return "Nadando en el agua"
class Anfibio(Animal, Terrestre, Acuatico):
def __init__(self, especie):
Animal.__init__(self, especie)
def adaptarse(self):
return "Puede sobrevivir tanto en tierra como en agua"
class Rana(Anfibio):
def __init__(self, nombre):
super().__init__("Anuro")
self.nombre = nombre
def hacer_sonido(self):
return "¡Croac!"
# Uso
rana = Rana("Kermit")
print(f"{rana.nombre} es un {rana.especie}") # Salida: Kermit es un Anuro
print(rana.hacer_sonido()) # Salida: ¡Croac!
print(rana.caminar()) # Salida: Caminando en tierra
print(rana.nadar()) # Salida: Nadando en el agua
print(rana.adaptarse()) # Salida: Puede sobrevivir tanto en tierra como en agua
Este ejemplo demuestra la herencia híbrida:
Rana
hereda de Anfibio
Anfibio
hereda de Animal
, Terrestre
, y Acuatico
- Esto crea una combinación de herencia multinivel y múltiple
Consideraciones
La herencia ofrece varias ventajas. Sin embargo, también hay consideraciones importantes:
- Complejidad: Las jerarquías de herencia profundas pueden volverse difíciles de entender y mantener.
- Acoplamiento fuerte: La herencia crea un acoplamiento fuerte entre las clases base y derivadas.
- Problema de la clase base frágil: Los cambios en la clase base pueden afectar inesperadamente a las clases derivadas.
- Problema del diamante: En la herencia múltiple, pueden surgir conflictos si dos clases base tienen métodos con el mismo nombre.
Para abordar estas consideraciones:
- Prefiere la composición sobre la herencia cuando sea posible.
- Mantén las jerarquías de herencia poco profundas y enfocadas.
- Utiliza clases base abstractas para definir interfaces claras.
- Ten cuidado con la herencia múltiple y resuelve los conflictos explícitamente.
Visualicemos las relaciones de herencia que hemos discutido utilizando un diagrama de clases UML:
classDiagram
Animal <|-- Mamífero
Animal <|-- Ave
Mamífero <|-- Perro
Mamífero <|-- Gato
Ave <|-- Loro
Ave <|-- Pingüino
Animal <|-- Anfibio
Terrestre <|-- Anfibio
Acuático <|-- Anfibio
Anfibio <|-- Rana
class Animal {
+especie: str
+hacer_sonido()
}
class Mamífero {
+es_de_sangre_caliente: bool
+dar_a_luz()
}
class Ave {
+puede_volar: bool
}
class Anfibio {
+adaptarse()
}
class Terrestre {
+caminar()
}
class Acuático {
+nadar()
}
Este diagrama ilustra las relaciones de herencia entre las clases que hemos discutido, mostrando tanto la herencia simple como la múltiple.
Referencias
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples-programming.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
2.3 - Polimorfismo
El polimorfismo es un concepto fundamental en la programación orientada a objetos que permite que objetos de diferentes clases sean tratados como objetos de una clase base común. El término “polimorfismo” proviene del griego y significa “muchas formas”. En POO, se refiere a la capacidad de una única interfaz para representar diferentes formas subyacentes (tipos de datos o clases).
El polimorfismo permite escribir código flexible y reutilizable al permitirnos trabajar con objetos a un nivel más abstracto, sin necesidad de conocer sus tipos específicos.
Existen dos tipos principales de polimorfismo en la programación orientada a objetos:
Polimorfismo en tiempo de compilación (Polimorfismo estático)
- Se logra a través de la sobrecarga de métodos.
- Se resuelve en tiempo de compilación.
Polimorfismo en tiempo de ejecución (Polimorfismo dinámico)
- Se logra a través de la sobrescritura de métodos.
- Se resuelve en tiempo de ejecución.
Python admite principalmente el polimorfismo en tiempo de ejecución, ya que es un lenguaje de tipado dinámico. Sin embargo, podemos demostrar conceptos similares al polimorfismo en tiempo de compilación también.
Exploremos diferentes aspectos del polimorfismo en Python:
Duck typing
Python utiliza el duck typing, que es una forma de polimorfismo. La idea es: “Si camina como un pato y grazna como un pato, entonces debe ser un pato”. En otras palabras, Python se preocupa más por los métodos que tiene un objeto que por el tipo del objeto en sí.
class Pato:
def hablar(self):
return "¡Cuac cuac!"
class Perro:
def hablar(self):
return "¡Guau guau!"
class Gato:
def hablar(self):
return "¡Miau miau!"
def sonido_animal(animal):
return animal.hablar()
# Uso
pato = Pato()
perro = Perro()
gato = Gato()
print(sonido_animal(pato)) # Salida: ¡Cuac cuac!
print(sonido_animal(perro)) # Salida: ¡Guau guau!
print(sonido_animal(gato)) # Salida: ¡Miau miau!
En este ejemplo, sonido_animal()
funciona con cualquier objeto que tenga un método hablar()
, independientemente de su clase.
Sobrescritura de métodos
La sobrescritura de métodos es un aspecto clave del polimorfismo en tiempo de ejecución. Ocurre cuando una clase derivada define un método con el mismo nombre que un método en su clase base.
class Figura:
def area(self):
pass
class Rectangulo(Figura):
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
def area(self):
return self.ancho * self.alto
class Circulo(Figura):
def __init__(self, radio):
self.radio = radio
def area(self):
return 3.14159 * self.radio ** 2
# Uso
figuras = [Rectangulo(5, 4), Circulo(3)]
for figura in figuras:
print(f"Área: {figura.area()}")
# Salida:
# Área: 20
# Área: 28.27431
Aquí, Rectangulo
y Circulo
sobrescriben el método area()
de la clase Figura
.
Sobrecarga de operadores
Python permite la sobrecarga de operadores, que es una forma de polimorfismo en tiempo de compilación. Permite que el mismo operador tenga diferentes significados según los operandos.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, otro):
return Vector(self.x + otro.x, self.y + otro.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
# Uso
v1 = Vector(2, 3)
v2 = Vector(3, 4)
v3 = v1 + v2
print(v3) # Salida: Vector(5, 7)
Aquí, hemos sobrecargado el operador +
para nuestra clase Vector
.
Clases base abstractas
El módulo abc
de Python proporciona infraestructura para definir clases base abstractas, que son una forma poderosa de definir interfaces en Python.
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def hacer_sonido(self):
pass
class Perro(Animal):
def hacer_sonido(self):
return "¡Guau!"
class Gato(Animal):
def hacer_sonido(self):
return "¡Miau!"
# Uso
def sonido_animal(animal):
return animal.hacer_sonido()
perro = Perro()
gato = Gato()
print(sonido_animal(perro)) # Salida: ¡Guau!
print(sonido_animal(gato)) # Salida: ¡Miau!
# Esto generará un TypeError
# animal = Animal()
Las clases base abstractas no pueden ser instanciadas y obligan a las clases derivadas a implementar ciertos métodos, asegurando una interfaz consistente.
Aplicaciones en el mundo real
El polimorfismo se utiliza ampliamente en aplicaciones del mundo real:
- Frameworks de GUI: Diferentes widgets (botones, cajas de texto) pueden responder a eventos comunes (clic, hover) de sus propias maneras.
- Interfaces de bases de datos: Diferentes sistemas de bases de datos pueden implementar una interfaz común para consultas, permitiendo que las aplicaciones trabajen con varias bases de datos sin cambiar el código.
- Sistemas de plugins: Las aplicaciones pueden trabajar con plugins a través de una interfaz común, independientemente de la implementación específica de cada plugin.
- Desarrollo de juegos: Diferentes entidades del juego pueden compartir comportamientos comunes (mover, colisionar) pero implementarlos de manera diferente.
Aquí hay un ejemplo simple de un sistema de plugins:
class Plugin(ABC):
@abstractmethod
def procesar(self, datos):
pass
class PluginMayusculas(Plugin):
def procesar(self, datos):
return datos.upper()
class PluginInvertir(Plugin):
def procesar(self, datos):
return datos[::-1]
class Aplicacion:
def __init__(self):
self.plugins = []
def agregar_plugin(self, plugin):
self.plugins.append(plugin)
def procesar_datos(self, datos):
for plugin in self.plugins:
datos = plugin.procesar(datos)
return datos
# Uso
app = Aplicacion()
app.agregar_plugin(PluginMayusculas())
app.agregar_plugin(PluginInvertir())
resultado = app.procesar_datos("Hola, Mundo!")
print(resultado) # Salida: !ODNUM ,ALOH
Este ejemplo demuestra cómo el polimorfismo permite que la clase Aplicacion
trabaje con diferentes plugins a través de una interfaz común.
Referencias
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples-programming.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
2.4 - Abstracción
La abstracción es el proceso de ocultar los detalles complejos de implementación y mostrar solo las características necesarias de un objeto. Se trata de crear una vista simplificada de un objeto que represente sus características esenciales sin incluir detalles de fondo o explicaciones.
Los aspectos clave de la abstracción incluyen:
- Simplificación: La abstracción reduce la complejidad ocultando detalles innecesarios.
- Enfoque en características esenciales: Enfatiza lo que hace un objeto en lugar de cómo lo hace.
- Separación de preocupaciones: Permite separar la interfaz de una clase de su implementación.
- Modularidad: La abstracción promueve el diseño modular al definir límites claros entre componentes.
Clases abstractas e interfaces
En muchos lenguajes orientados a objetos, la abstracción se implementa a través de clases abstractas e interfaces. Aunque Python no tiene un concepto integrado de interfaz, podemos lograr una funcionalidad similar usando clases base abstractas. El módulo abc
de Python proporciona infraestructura para definir clases base abstractas:
from abc import ABC, abstractmethod
class Figura(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimetro(self):
pass
class Rectangulo(Figura):
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
def area(self):
return self.ancho * self.alto
def perimetro(self):
return 2 * (self.ancho + self.alto)
class Circulo(Figura):
def __init__(self, radio):
self.radio = radio
def area(self):
return 3.14159 * self.radio ** 2
def perimetro(self):
return 2 * 3.14159 * self.radio
# Uso
# figuras = [Figura()] # Esto generaría TypeError
figuras = [Rectangulo(5, 4), Circulo(3)]
for figura in figuras:
print(f"Área: {figura.area()}, Perímetro: {figura.perimetro()}")
# Salida:
# Área: 20, Perímetro: 18
# Área: 28.27431, Perímetro: 18.84954
En este ejemplo:
Figura
es una clase base abstracta que define la interfaz para todas las figuras.Rectangulo
y Circulo
son clases concretas que implementan la interfaz Figura
.- No podemos instanciar
Figura
directamente, pero podemos usarla como un tipo común para todas las figuras.
Implementando abstracción en Python
Aunque Python proporciona clases base abstractas para definir interfaces formalmente, también podemos lograr la abstracción mediante convenciones y documentación. Veamos un ejemplo sin utilizar ABC
:
class BaseDeDatos:
def conectar(self):
raise NotImplementedError("La subclase debe implementar este método abstracto")
def ejecutar(self, consulta):
raise NotImplementedError("La subclase debe implementar este método abstracto")
class BaseDeDatosMySQL(BaseDeDatos):
def conectar(self):
print("Conectando a la base de datos MySQL...")
def ejecutar(self, consulta):
print(f"Ejecutando consulta MySQL: {consulta}")
class BaseDeDatosPostgreSQL(BaseDeDatos):
def conectar(self):
print("Conectando a la base de datos PostgreSQL...")
def ejecutar(self, consulta):
print(f"Ejecutando consulta PostgreSQL: {consulta}")
def realizar_operacion_en_bd(base_de_datos):
base_de_datos.conectar()
base_de_datos.ejecutar("SELECT * FROM usuarios")
# Uso
bd_mysql = BaseDeDatosMySQL()
bd_postgres = BaseDeDatosPostgreSQL()
realizar_operacion_en_bd(bd_mysql)
realizar_operacion_en_bd(bd_postgres)
# Salida:
# Conectando a la base de datos MySQL...
# Ejecutando consulta MySQL: SELECT * FROM usuarios
# Conectando a la base de datos PostgreSQL...
# Ejecutando consulta PostgreSQL: SELECT * FROM usuarios
En este ejemplo:
BaseDeDatos
es una clase base abstracta (aunque no usa ABC
) que define la interfaz para todos los tipos de bases de datos.BaseDeDatosMySQL
y BaseDeDatosPostgreSQL
son implementaciones concretas.realizar_operacion_en_bd
trabaja con cualquier objeto que cumpla con la interfaz de BaseDeDatos
.
Principios de diseño y patrones
La abstracción es un componente clave de varios principios y patrones de diseño importantes:
Principios SOLID:
- Principio de Responsabilidad Única (SRP).
- Principio de Abierto/Cerrado (OCP).
- Principio de Sustitución de Liskov (LSP).
- Principio de Segregación de Interfaces (ISP).
- Principio de Inversión de Dependencias (DIP).
Patrones de Diseño:
- Patrón método de fábrica.
- Patrón fábrica abstracta.
- Patrón estrategia.
- Patrón método plantilla.
Veamos una implementación del patrón estrategia:
from abc import ABC, abstractmethod
class EstrategiaOrdenamiento(ABC):
@abstractmethod
def ordenar(self, datos):
pass
class OrdenamientoBurbuja(EstrategiaOrdenamiento):
def ordenar(self, datos):
print("Realizando ordenamiento de burbuja")
return sorted(datos) # Usamos sorted() de Python por simplicidad
class OrdenamientoRapido(EstrategiaOrdenamiento):
def ordenar(self, datos):
print("Realizando ordenamiento rápido")
return sorted(datos) # Usamos sorted() de Python por simplicidad
class Ordenador:
def __init__(self, estrategia):
self.estrategia = estrategia
def ordenar(self, datos):
return self.estrategia.ordenar(datos)
# Uso
datos = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
ordenador_burbuja = Ordenador(OrdenamientoBurbuja())
print(ordenador_burbuja.ordenar(datos))
ordenador_rapido = Ordenador(OrdenamientoRapido())
print(ordenador_rapido.ordenar(datos))
# Salida:
# Realizando ordenamiento de burbuja
# [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
# Realizando ordenamiento rápido
# [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
Este ejemplo del Patrón Estrategia muestra cómo la abstracción nos permite definir una familia de algoritmos, encapsular cada uno y hacerlos intercambiables. La clase Ordenador
no necesita conocer los detalles de cómo funciona cada algoritmo de ordenamiento; solo sabe que puede llamar al método ordenar
en cualquier objeto EstrategiaOrdenamiento
.
Referencias
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples-programming.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!
2.5 - Conclusión
La Programación Orientada a Objetos es un paradigma poderoso que proporciona una forma de estructurar el código que refleja estrechamente las entidades del mundo real y sus interacciones. Los cuatro conceptos fundamentales que hemos explorado - encapsulamiento, herencia, polimorfismo y abstracción - trabajan juntos para crear código flexible, mantenible y reutilizable.
- El encapsulamiento nos permite agrupar datos y métodos, ocultando detalles internos y protegiendo la integridad de los datos.
- La herencia permite la reutilización de código y la creación de relaciones jerárquicas entre clases.
- El polimorfismo proporciona una forma de usar objetos de diferentes tipos a través de una interfaz común, mejorando la flexibilidad y extensibilidad.
- La abstracción nos permite crear modelos simplificados de sistemas complejos, enfocándonos en las características esenciales y ocultando los detalles innecesarios.
A medida que continúes tu viaje en el desarrollo de software, descubrirás que dominar estos conceptos abre nuevas formas de pensar y resolver problemas. Recuerda que la POO no se trata solo de sintaxis o características del lenguaje; es una mentalidad para modelar sistemas complejos y gestionar la complejidad en el software.
Referencias
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- Phillips, D. (2010). Python 3 Object Oriented Programming. Packt Publishing.
- Lutz, M. (2013). Learning Python: Powerful Object-Oriented Programming. O’Reilly Media.
- Ramalho, L. (2015). Fluent Python: Clear, Concise, and Effective Programming. O’Reilly Media.
- Van Rossum, G., Warsaw, B., & Coghlan, N. (2001). PEP 8 – Style Guide for Python Code. Python.org. https://www.python.org/dev/peps/pep-0008/
- Python Software Foundation. (n.d.). The Python Standard Library. Python.org. https://docs.python.org/3/library/
¡Felicitaciones por llegar hasta acá! Espero que este recorrido por el universo de la programación te haya resultado tan interesante como lo fue para mí al escribirlo.
Queremos conocer tu opinión, así que no dudes en compartir tus comentarios, sugerencias y esas ideas brillantes que seguro tenés.
Además, para explorar más allá de estas líneas, date una vuelta por los ejemplos prácticos que armamos para vos. Todo el código y los proyectos los encontrarás en nuestro repositorio de GitHub learn-software-engineering/examples-programming.
Gracias por ser parte de esta comunidad de aprendizaje. ¡Seguí programando y explorando nuevas areas en este fascinante mundo del software!