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 conlen()
.__getitem__(self, key)
: Permite el acceso por índice conobj[key]
.__setitem__(self, key, value)
: Permite asignar valores por índice conobj[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.