La programación orientada a objetos (POO) es una forma de organizar tu código agrupando datos y funciones relacionadas en una misma estructura llamada clase. En lugar de tener variables sueltas y funciones dispersas, modelas el mundo real en objetos que tienen características y comportamientos propios.
Python fue diseñado desde el principio con la POO en mente. Todo en Python es un objeto: los números, las cadenas, las listas. Entender cómo funcionan las clases te permitirá escribir código más organizado, reutilizable y fácil de mantener.
¿Qué es una clase y qué es un objeto?
Una clase es una plantilla o molde. Un objeto es una instancia concreta creada a partir de esa plantilla. La relación es la misma que entre un plano de arquitectura y una casa construida a partir de él: puedes construir muchas casas diferentes usando el mismo plano.
Por ejemplo, la clase Perro define qué es un perro en tu programa (tiene nombre, raza, puede ladrar). Cada perro concreto que crees (Firulais, Rex, Lola) es un objeto distinto con sus propios valores.
# Esto es una clase (el molde)
class Perro:
pass
# Estos son objetos (instancias del molde)
perro1 = Perro()
perro2 = Perro()
print(type(perro1)) # <class '__main__.Perro'>
Crear tu primera clase: atributos y el método __init__
Una clase útil necesita datos. Los datos que pertenecen a cada objeto se llaman atributos. El método especial __init__ es el constructor: se ejecuta automáticamente cada vez que creas un nuevo objeto y sirve para asignarle sus valores iniciales.
class Perro:
def __init__(self, nombre, raza, edad):
self.nombre = nombre # atributo de instancia
self.raza = raza
self.edad = edad
# Crear objetos pasando los argumentos del constructor
perro1 = Perro("Firulais", "Labrador", 3)
perro2 = Perro("Rex", "Pastor Alemán", 5)
# Acceder a los atributos con el punto
print(perro1.nombre) # Firulais
print(perro2.edad) # 5
El parámetro self es una referencia al objeto que se está creando. Siempre es el primer parámetro de cualquier método de una clase, aunque al llamar el método no lo escribes tú: Python lo pasa automáticamente.
Métodos: el comportamiento de los objetos
Los métodos son funciones definidas dentro de una clase. Describen lo que un objeto puede hacer. Se definen igual que una función normal, pero siempre reciben self como primer parámetro para poder acceder a los atributos del propio objeto.
class Perro:
def __init__(self, nombre, raza, edad):
self.nombre = nombre
self.raza = raza
self.edad = edad
def ladrar(self):
print(f"{self.nombre} dice: ¡Guau!")
def presentarse(self):
print(f"Soy {self.nombre}, un {self.raza} de {self.edad} años.")
def cumplir_anios(self):
self.edad += 1
print(f"¡{self.nombre} cumple {self.edad} años!")
perro1 = Perro("Firulais", "Labrador", 3)
perro1.ladrar() # Firulais dice: ¡Guau!
perro1.presentarse() # Soy Firulais, un Labrador de 3 años.
perro1.cumplir_anios() # ¡Firulais cumple 4 años!
print(perro1.edad) # 4
Atributos de clase vs atributos de instancia
Hasta ahora todos los atributos que vimos son atributos de instancia: cada objeto tiene su propia copia con su propio valor. Pero también existen los atributos de clase: son compartidos por todos los objetos de esa clase.
class Perro:
especie = "Canis lupus familiaris" # atributo de clase (compartido)
def __init__(self, nombre, raza):
self.nombre = nombre # atributo de instancia (propio de cada objeto)
self.raza = raza
perro1 = Perro("Firulais", "Labrador")
perro2 = Perro("Rex", "Pastor Alemán")
# Todos comparten el atributo de clase
print(perro1.especie) # Canis lupus familiaris
print(perro2.especie) # Canis lupus familiaris
print(Perro.especie) # también accesible desde la clase directamente
# Cada uno tiene su propio atributo de instancia
print(perro1.nombre) # Firulais
print(perro2.nombre) # Rex
Los atributos de clase son útiles para valores que no cambian entre instancias: constantes, contadores compartidos, configuraciones por defecto.
class Perro:
cantidad = 0 # lleva la cuenta de cuántos perros se han creado
def __init__(self, nombre):
self.nombre = nombre
Perro.cantidad += 1
p1 = Perro("Firulais")
p2 = Perro("Rex")
p3 = Perro("Lola")
print(Perro.cantidad) # 3
El método __str__: cómo se muestra un objeto
Cuando haces print(objeto), Python busca el método especial __str__ para saber cómo mostrarlo. Si no lo defines, obtienes algo poco útil como <__main__.Perro object at 0x7f3a...>. Definirlo es una buena práctica.
class Perro:
def __init__(self, nombre, raza, edad):
self.nombre = nombre
self.raza = raza
self.edad = edad
def __str__(self):
return f"Perro(nombre={self.nombre}, raza={self.raza}, edad={self.edad})"
perro = Perro("Firulais", "Labrador", 3)
print(perro) # Perro(nombre=Firulais, raza=Labrador, edad=3)
Existe también __repr__, que se usa en la consola interactiva y para depuración. La convención es que __repr__ devuelva una cadena que permita recrear el objeto, mientras que __str__ devuelve una cadena legible para el usuario.
Herencia: reutilizar y extender clases
La herencia permite crear una clase nueva a partir de una existente. La clase nueva (clase hija o subclase) hereda todos los atributos y métodos de la clase original (clase padre o superclase) y puede añadir los suyos propios o modificar los heredados.
class Animal:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def respirar(self):
print(f"{self.nombre} respira.")
def __str__(self):
return f"{self.nombre} ({self.edad} años)"
class Perro(Animal): # Perro hereda de Animal
def __init__(self, nombre, edad, raza):
super().__init__(nombre, edad) # llama al constructor del padre
self.raza = raza
def ladrar(self):
print(f"{self.nombre} dice: ¡Guau!")
class Gato(Animal): # Gato hereda de Animal
def __init__(self, nombre, edad, interior):
super().__init__(nombre, edad)
self.interior = interior # True si vive en casa
def maullar(self):
print(f"{self.nombre} dice: ¡Miau!")
perro = Perro("Firulais", 3, "Labrador")
gato = Gato("Misi", 2, True)
perro.respirar() # Firulais respira. (método heredado de Animal)
perro.ladrar() # Firulais dice: ¡Guau!
gato.maullar() # Misi dice: ¡Miau!
print(perro) # Firulais (3 años) (usa __str__ del padre)
La función super() da acceso a los métodos de la clase padre. Es especialmente importante en __init__ para no tener que repetir la lógica de inicialización.
Sobreescritura de métodos (overriding)
Una subclase puede sobreescribir un método del padre para cambiar su comportamiento. Solo tienes que definir un método con el mismo nombre en la clase hija.
class Animal:
def hablar(self):
print("...")
class Perro(Animal):
def hablar(self): # sobreescribe el método del padre
print("¡Guau!")
class Gato(Animal):
def hablar(self): # sobreescribe el método del padre
print("¡Miau!")
class Vaca(Animal):
pass # no sobreescribe: usa el del padre
animales = [Perro(), Gato(), Vaca()]
for animal in animales:
animal.hablar()
# ¡Guau!
# ¡Miau!
# ...
Este comportamiento —donde el mismo método produce resultados diferentes según el tipo de objeto— se llama polimorfismo. Es uno de los conceptos más poderosos de la POO y permite escribir código genérico que funciona con distintos tipos de objetos.
Encapsulamiento: controlar el acceso a los datos
El encapsulamiento consiste en proteger los datos internos de un objeto para que no puedan modificarse desde fuera de forma arbitraria. En Python no existe un mecanismo de acceso estricto como en Java o C++, pero sí hay convenciones:
- Un atributo con un guión bajo
_nombreindica que es de uso interno. No está prohibido acceder a él, pero la convención dice que no deberías. - Un atributo con doble guión bajo
__nombreactiva el name mangling: Python lo renombra internamente para dificultar el acceso desde fuera.
class CuentaBancaria:
def __init__(self, titular, saldo):
self.titular = titular
self.__saldo = saldo # atributo "privado"
def depositar(self, monto):
if monto <= 0:
raise ValueError("El monto debe ser positivo.")
self.__saldo += monto
def retirar(self, monto):
if monto > self.__saldo:
raise ValueError("Saldo insuficiente.")
self.__saldo -= monto
def ver_saldo(self):
return self.__saldo
cuenta = CuentaBancaria("Laura", 500)
cuenta.depositar(200)
cuenta.retirar(100)
print(cuenta.ver_saldo()) # 600
# Intentar acceder directamente al atributo privado falla
# print(cuenta.__saldo) # AttributeError
Propiedades con @property
Una forma más elegante de controlar el acceso a atributos es usar el decorador @property. Permite que un método se comporte como un atributo, lo que da control total sobre cómo se lee y escribe un valor.
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:
raise ValueError("El radio debe ser mayor que cero.")
self._radio = valor
@property
def area(self):
import math
return math.pi * self._radio ** 2
c = Circulo(5)
print(c.radio) # 5 (llama al getter)
print(c.area) # 78.53... (propiedad calculada, sin setter)
c.radio = 10 # llama al setter con validación
print(c.area) # 314.15...
# c.radio = -3 # ValueError: El radio debe ser mayor que cero.
Métodos de clase y métodos estáticos
Además de los métodos de instancia (que reciben self), Python tiene dos tipos más:
Los métodos de clase reciben la clase como primer argumento (por convención cls) en lugar del objeto. Se definen con @classmethod y se usan para crear objetos de formas alternativas o para lógica que afecta a la clase entera.
Los métodos estáticos no reciben ni self ni cls. Son funciones normales agrupadas dentro de la clase por coherencia lógica. Se definen con @staticmethod.
class Fecha:
def __init__(self, dia, mes, anio):
self.dia = dia
self.mes = mes
self.anio = anio
@classmethod
def desde_cadena(cls, cadena):
"""Constructor alternativo: acepta '15/06/2026'"""
dia, mes, anio = map(int, cadena.split("/"))
return cls(dia, mes, anio)
@staticmethod
def es_bisiesto(anio):
"""Utilidad relacionada con fechas, pero no necesita la instancia"""
return anio % 4 == 0 and (anio % 100 != 0 or anio % 400 == 0)
def __str__(self):
return f"{self.dia:02d}/{self.mes:02d}/{self.anio}"
# Método de clase: forma alternativa de crear objetos
f1 = Fecha(15, 6, 2026)
f2 = Fecha.desde_cadena("20/08/2026")
print(f1) # 15/06/2026
print(f2) # 20/08/2026
# Método estático: no necesita una instancia
print(Fecha.es_bisiesto(2024)) # True
print(Fecha.es_bisiesto(2026)) # False
Herencia múltiple y mixins
Python permite que una clase herede de más de una clase al mismo tiempo. Esto se llama herencia múltiple y debe usarse con cuidado, pero tiene un patrón muy útil: los mixins.
Un mixin es una clase pequeña que añade una capacidad específica sin pretender ser una clase base completa. Se combina con otras clases para agregar funcionalidad de forma modular.
class SerializableMixin:
"""Añade la capacidad de convertir el objeto a diccionario"""
def to_dict(self):
return self.__dict__
class LogMixin:
"""Añade la capacidad de registrar acciones"""
def log(self, mensaje):
print(f"[LOG] {self.__class__.__name__}: {mensaje}")
class Usuario(SerializableMixin, LogMixin):
def __init__(self, nombre, email):
self.nombre = nombre
self.email = email
def cambiar_email(self, nuevo_email):
self.email = nuevo_email
self.log(f"Email actualizado a {nuevo_email}")
usuario = Usuario("Ana", "ana@mail.com")
usuario.cambiar_email("nueva@mail.com") # [LOG] Usuario: Email actualizado a nueva@mail.com
print(usuario.to_dict()) # {'nombre': 'Ana', 'email': 'nueva@mail.com'}
Verificar tipos e instancias
Python ofrece dos funciones integradas muy útiles cuando trabajas con clases y herencia:
class Animal: pass
class Perro(Animal): pass
perro = Perro()
# isinstance: verifica si un objeto es instancia de una clase (o subclase)
print(isinstance(perro, Perro)) # True
print(isinstance(perro, Animal)) # True (Perro hereda de Animal)
print(isinstance(perro, str)) # False
# issubclass: verifica la relación entre clases
print(issubclass(Perro, Animal)) # True
print(issubclass(Animal, Perro)) # False
Usa isinstance() en lugar de type(obj) == Clase: es más correcto porque respeta la herencia y es la forma idiomática en Python.
Ejemplo completo: un sistema de tienda
Para cerrar, un ejemplo que integra todo lo visto: atributos de instancia y de clase, métodos, herencia, propiedades y __str__.
class Producto:
_total_productos = 0
def __init__(self, nombre, precio):
self.nombre = nombre
self._precio = precio
Producto._total_productos += 1
@property
def precio(self):
return self._precio
@precio.setter
def precio(self, valor):
if valor < 0:
raise ValueError("El precio no puede ser negativo.")
self._precio = valor
@classmethod
def total_productos(cls):
return cls._total_productos
def __str__(self):
return f"{self.nombre}: ${self._precio:.2f}"
class ProductoDigital(Producto):
def __init__(self, nombre, precio, url_descarga):
super().__init__(nombre, precio)
self.url_descarga = url_descarga
def descargar(self):
print(f"Descargando {self.nombre} desde {self.url_descarga}...")
def __str__(self):
return f"[Digital] {super().__str__()}"
class Carrito:
def __init__(self):
self._items = []
def agregar(self, producto, cantidad=1):
self._items.append((producto, cantidad))
@property
def total(self):
return sum(p.precio * c for p, c in self._items)
def mostrar(self):
print("--- Carrito ---")
for producto, cantidad in self._items:
print(f" {producto} x{cantidad}")
print(f" Total: ${self.total:.2f}")
# Uso
libro = Producto("Libro Python", 29.99)
curso = ProductoDigital("Curso POO", 49.99, "https://ejemplo.com/curso")
camisa = Producto("Camisa", 19.99)
carrito = Carrito()
carrito.agregar(libro, 2)
carrito.agregar(curso)
carrito.agregar(camisa, 3)
carrito.mostrar()
# --- Carrito ---
# Libro Python: $29.99 x2
# [Digital] Curso POO: $49.99 x1
# Camisa: $19.99 x3
# Total: $159.94
print(f"Productos registrados: {Producto.total_productos()}") # 3
Resumen
La programación orientada a objetos en Python se construye sobre cuatro conceptos fundamentales:
- Encapsulamiento: agrupa datos y comportamientos en una clase, controlando qué es accesible desde fuera con convenciones de nombre y
@property. - Herencia: una clase hija reutiliza y extiende el comportamiento de una clase padre.
super()da acceso a los métodos heredados. - Polimorfismo: distintos objetos pueden responder al mismo método de formas diferentes, lo que permite escribir código genérico y flexible.
- Abstracción: modelas solo los aspectos relevantes del mundo real, ocultando los detalles de implementación innecesarios.
No necesitas usar POO en cada script de Python. Para tareas simples, las funciones son suficientes. Pero cuando tu programa crece, cuando tienes datos y comportamientos que van juntos de forma natural, o cuando necesitas reutilizar código con variaciones, las clases son la herramienta adecuada.
Denisse Alvarado
2 months ago