Python es un lenguaje fuertemente centrado en el paradigma de la programación orientada a objetos. Todas las variables, incluso las básicas como las numéricas o strings son objetos.

¿Qué es la Programación Orientada a Objetos?

La Programación Orientada a Objetos (POO) es un paradigma de programación que utiliza objetos para organizar el código. Este paradigma se basa en cuatro conceptos fundamentales: clases, objetos, herencia, polimorfismo y encapsulamiento.

Conceptos fundamentales

Clase

Una clase es una plantilla o modelo que especifica cómo deben ser los objetos de un tipo específico. Define variables propias de la clase, a los cuales se les llama “atributos” o “propiedades” y también funciones, que son llamados “métodos”.

classDiagram
    class Animal
    Animal: +str especie
    Animal: +int edad
    Animal: +str color
    Animal: +hacer_sonido()

Objeto

Un objeto es una instancia de una clase, el cual posee valores asociados a los atributos. Cada objeto es independiente, pero tienen en común de que poseen la misma estructura y funciones, que está dada por la clase.

classDiagram
    class Animal
    Animal : +str especie
    Animal : +int edad
    Animal : +str color
    Animal : +hacer_sonido()
    Animal -- animal_1 : instancia
    Animal -- animal_2 : instancia
    animal_1 : especie="perro"
    animal_1 : edad=5
    animal_1 : color="blanco"
    animal_2 : especie="puma"
    animal_2 : edad=10
    animal_2 : color="café"

Definición de clases

Para definir una clase, utiliza la palabra reservada class seguido por el nombre, y en un nivel de indentanción declaras los métodos asociados a la clase. Para los nombres de las clases utilizas la notación tipo pascal case, es decir, la primera letra de cada palabra en mayúscula (por ejemplo Animal, AmpolletaInteligente, MonitorDeProcesos).

Estilos de nomenclatura

Estilos de nomenclatura Los lenguajes de programación utilizan diferentes estilos de nomenclatura para las variables y otros elementos. Algunos de los estilos más comunes incluyen:

  • camelCase
  • snake_case
  • PascalCase
  • kebab-case
  • UPPER_SNAKE_CASE

En Python, es una práctica recomendada utilizar PascalCase para los nombres de clases y snake_case para nombres de funciones y variables.

class Animal:
    def __init__(self, especie, edad, color):
        self.especie = especie
        self.edad = edad
        self.color = color
 
    def hacer_sonido(self):
        return f"El animal de especie {self.especie} hace un sonido"
 
    def describir(self):
        return f"Este es un {self.especie} {self.color} de {self.edad} años"

El método __init__() es conocido como el constructor de la clase y es responsable de inicializar los atributos del objeto cuando se crea una nueva instancia. El primer parámetro de todos los métodos de una clase es self, que se refiere a la instancia del objeto actual, permitiendo acceder a sus atributos y otros métodos.

Creación de objetos

Para crear un objeto de una clase, simplemente se llama a la clase como si fuese una función, pasando los parámetros requeridos por el constructor.

animal_1 = Animal(especie="perro", edad=5, color="blanco")
animal_1.hacer_sonido()  # El animal de especie perro hace un sonido

Herencia

La herencia permite que una clase herede propiedades y métodos de otra clase, con la posibilidad de añadir nuevos atributos, nuevos métodos o modificar los existentes. Esto permite reutilizar código existente, evitando la redundancia.

classDiagram
    class Animal
    Animal : +str especie
    Animal : +int edad
    Animal : +str color
    Animal : +hacer_sonido()
    Animal --|> Perro : hereda de
    Animal --|> Gato : hereda de
    class Perro
    Perro : +str especie = "perro"
    Perro : +int edad
    Perro : +str color
    Perro : +hacer_sonido()
    Perro : +buscar_hueso()
    class Gato
    Gato : +str especie = "gato"
    Gato : +int edad
    Gato : +str color
    Gato : +int vidas
    Gato : +hacer_sonido()
    Gato : +ronronear()

Implementación en Python

Para heredar de una clase en Python, se indica el nombre de la clase base entre paréntesis en la definición de la nueva clase.

class Perro(Animal):
    def __init__(self, edad, color):
        super().__init__(especie="perro", edad=edad, color=color)
 
    def hacer_sonido(self):
        return "El perro ladra"
 
 
perro_1 = Perro(color="gris", edad=7)
print(perro_1.hacer_sonido())  # Salida: El perro ladra
print(perro_1.describir())  # Salida: Este es un perro gris de 7 años

La función super() permite acceder a los métodos y atributos de la clase padre, lo cual es útil para extender o modificar su comportamiento.

Herencia múltiple

Python también permite la herencia múltiple, donde una clase puede heredar de más de una clase base.

class Mascota:
    def __init__(self, nombre):
        self.nombre = nombre
 
    def presentar(self):
        return f"Esta es mi mascota {self.nombre}"
 
 
class PerroMascota(Animal, Mascota):
    def __init__(self, nombre, edad, color):
        Animal.__init__(self, especie="perro", edad=edad, color=color)
        Mascota.__init__(self, nombre=nombre)
 
    def hacer_sonido(self):
        return f"{self.nombre} ladra"
 
 
perro_mascota_1 = PerroMascota(nombre="Fido", edad=4, color="marrón")
print(perro_mascota_1.presentar())  # Salida: Esta es mi mascota Fido
print(perro_mascota_1.hacer_sonido())  # Salida: Fido ladra
print(perro_mascota_1.describir())  # Salida: Este es un perro marrón de 4 años

Si bien la herencia múltiple puede ser poderosa, también puede hacer el código más complejo, especialmente si las clases base comparten métodos con el mismo nombre. Para evitar confusiones, se suelen usar clases diseñadas para ser utilizadas en herencia múltiple llamadas “Mixins”.

Encapsulamiento

El encapsulamiento se refiere a proteger los atributos internos de una clase y sólo permitir su acceso a través de métodos públicos. Esto asegura que el estado interno de la clase se mantenga seguro.

Implementación en Python

En Python, el encapsulamiento se implementa mediante la restricción del acceso directo a los atributos y métodos de una clase, permitiendo controlar cómo se interactúa con ellos.

Atributos protegidos y privados

Python utiliza convenciones de nomenclatura para indicar el nivel de acceso a los atributos:

  • Atributo protegido (_atributo): Un solo guion bajo indica que el atributo es de uso interno y no debería ser accedido directamente desde fuera de la clase o sus subclases.
  • Atributo privado (__atributo): Dos guiones bajos activan el “name mangling” de Python, donde el intérprete modifica internamente el nombre del atributo para hacerlo menos accesible.
class Ejemplo:
    def __init__(self):
        self.publico = "Acceso libre"
        self._protegido = "Solo uso interno"
        self.__privado = "Name mangling activado"
 
    def _metodo_protegido(self):
        return "Método protegido"
 
    def __metodo_privado(self):
        return "Método privado"
 
 
# Ejemplo de uso:
obj = Ejemplo()
print(obj.publico)  # ✓ Funciona normal
print(obj._protegido)  # ✓ Funciona, pero no se recomienda
print(obj.__privado)  # ✗ Error: AttributeError
print(obj._Ejemplo__privado)  # ✓ Funciona (name mangling)

Propiedades calculadas

Las propiedades calculadas permiten definir métodos que se comportan como atributos, útiles para valores que se calculan dinámicamente o que requieren validación:

class Rectangulo:
    def __init__(self, ancho, alto):
        self._ancho = ancho
        self._alto = alto
 
    @property
    def area(self):
        """Propiedad calculada: área del rectángulo"""
        return self._ancho * self._alto
 
    @property
    def ancho(self):
        return self._ancho
 
    @ancho.setter
    def ancho(self, valor):
        if valor <= 0:
            raise ValueError("El ancho debe ser positivo")
        self._ancho = valor
 
 
# Ejemplo de uso:
rect = Rectangulo(5, 3)
print(rect.area)  # 15 (calculado automáticamente)
rect.ancho = 10  # Usa el setter con validación
print(rect.area)  # 30 (recalculado automáticamente)

Es importante destacar que en Python estas convenciones no impiden realmente el acceso a estos atributos o métodos, sino que son prácticas recomendadas para indicar la intención del programador.

Polimorfismo

El polimorfismo se refiere a que diferentes clases puedan ser tratadas como si fueran de la misma clase base. De esta manera, se pueden crear funciones que puedan trabajar con múltiples tipos de objetos, evitando tener que crear múltiples funciones que cumplen el mismo objetivo pero trabajan con distintos tipos de objetos.

Implementación en Python

En Python, el polimorfismo permite que diferentes clases implementen métodos con el mismo nombre, y que los objetos de estas clases puedan ser utilizados de manera intercambiable en funciones o métodos que esperan cierto comportamiento.

def mostrar_saludo(persona):
    print(persona.saludo())
 
 
mostrar_saludo(juan)  # Utilizando la instancia de Persona
mostrar_saludo(empleado)  # Utilizando la instancia de Empleado

Métodos especiales

Python tiene métodos especiales, también conocidos como “dunder methods” (por los dobles guiones bajos), que permiten personalizar el comportamiento de los objetos. Estos métodos permiten definir cómo los objetos se comportan en operaciones comunes como sumar, comparar o convertir a cadena.

Algunos ejemplos de métodos especiales incluyen:

  • __init__(self, ...): Constructor de la clase.
  • __str__(self): Representación en cadena del objeto.
  • __repr__(self): Representación no ambigua del objeto.
  • __add__(self, other): Comportamiento de suma con +.
  • __eq__(self, other): Comparación de igualdad con ==.
  • __len__(self): Retorna la longitud del objeto con len().
  • __getitem__(self, key): Permite el acceso por índice con obj[key].
  • __setitem__(self, key, value): Permite asignar valores por índice con obj[key] = value.
class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y
 
    def __str__(self):
        return f"({self.x}, {self.y})"
 
    def __add__(self, otro_punto):
        return Punto(self.x + otro_punto.x, self.y + otro_punto.y)
 
    def __eq__(self, otro_punto):
        return (self.x == otro_punto.x) and (self.y == otro_punto.y)
 
 
# Ejemplos de uso:
p1 = Punto(3, 4)
p2 = Punto(1, 2)
p3 = Punto(3, 4)
 
# Uso de __str__
print(p1)  # Salida: "(3, 4)"
 
# Uso de __add__
suma = p1 + p2
print(suma)  # Salida: "(4, 6)"
 
# Uso de __eq__
print(p1 == p3)  # Salida: True
print(p1 == p2)  # Salida: False

Estos métodos especiales permiten a los objetos de Python comportarse de maneras más naturales y hacerlos compatibles con las operaciones estándar del lenguaje.