Programación orientada a objetos en Python: clases, objetos y herencia
- Por qué existe la POO: agrupar datos y comportamiento
- Tu primera clase: atributos y métodos
- self y __init__: la identidad del objeto
- Atributos de clase vs atributos de instancia
- Herencia y super(): reutilizar sin copiar
- Polimorfismo: misma interfaz, comportamientos distintos
- @property: encapsulación con estilo Python
- Métodos especiales: hacer objetos nativos de Python
- @classmethod y @staticmethod
- Programa completo: sistema de biblioteca
- Errores clásicos con clases
- Preguntas frecuentes
La programación orientada a objetos no es una complicación innecesaria: es una forma de organizar el código que imita cómo pensamos sobre el mundo. Un usuario tiene nombre, email y contraseña — y puede iniciar sesión, cambiar su perfil o darse de baja. Esos datos y esas acciones van juntos. Una clase los agrupa en un solo lugar.
Python es un lenguaje multiparadigma: puedes programar con funciones, con clases, o mezclar ambas según lo que cada parte del proyecto necesite. Pero entender la POO es indispensable porque toda la biblioteca estándar y los frameworks más usados están construidos sobre clases.
🏗️ Tu primera clase: atributos y métodos
class Persona:
"""Representa a una persona con nombre y edad."""
def saludar(self):
return "Hola, soy una persona."
# Crear instancias (objetos)
p1 = Persona()
p2 = Persona()
print(p1.saludar()) # "Hola, soy una persona."
print(p1 is p2) # False — son objetos distintos
print(type(p1)) #
🔑 self y __init__: la identidad del objeto
__init__ es el constructor: se ejecuta automáticamente al crear cada instancia. self es la referencia al objeto concreto — permite que cada instancia tenga sus propios datos.
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre # atributo de instancia
self.edad = edad
def presentarse(self):
return f"Soy {self.nombre}, tengo {self.edad} años."
def cumpleanos(self):
self.edad += 1
return f"¡Ahora tengo {self.edad}!"
ana = Persona("Ana", 28)
luis = Persona("Luis", 35)
print(ana.presentarse()) # "Soy Ana, tengo 28 años."
print(luis.presentarse()) # "Soy Luis, tengo 35 años."
ana.cumpleanos()
print(ana.edad) # 29 — solo afecta a ana
print(luis.edad) # 35 — luis no cambia
📊 Atributos de clase vs atributos de instancia
class Producto:
iva = 0.21 # atributo de CLASE: compartido por todas las instancias
def __init__(self, nombre, precio_base):
self.nombre = nombre # atributo de INSTANCIA
self.precio_base = precio_base
def precio_final(self):
return round(self.precio_base * (1 + Producto.iva), 2)
p1 = Producto("Teclado", 50)
p2 = Producto("Ratón", 25)
print(p1.precio_final()) # 60.5
print(p2.precio_final()) # 30.25
# Cambiar el IVA para TODOS:
Producto.iva = 0.10
print(p1.precio_final()) # 55.0
# Sobrescribir solo para una instancia (crea atributo propio):
p1.iva = 0.04 # solo p1 tiene su propio iva
print(p1.iva) # 0.04
print(p2.iva) # 0.10 — sigue usando el de la clase
🌳 Herencia y super(): reutilizar sin copiar
La herencia permite que una clase hija reutilice todo el código de la clase padre y añada o modifique lo que necesite. super() llama al método del padre sin hardcodear su nombre.
class Animal:
def __init__(self, nombre, especie):
self.nombre = nombre
self.especie = especie
self.vivo = True
def respirar(self):
return f"{self.nombre} respira."
def info(self):
return f"{self.nombre} ({self.especie})"
class Perro(Animal):
def __init__(self, nombre, raza):
super().__init__(nombre, especie="Canis lupus") # llama al padre
self.raza = raza
def ladrar(self):
return "¡Guau!"
def respirar(self): # sobrescribe el método padre
return f"{self.nombre} jadea feliz."
class Gato(Animal):
def __init__(self, nombre, indoor=True):
super().__init__(nombre, especie="Felis catus")
self.indoor = indoor
def maullar(self):
return "¡Miau!"
rex = Perro("Rex", "Labrador")
luna = Gato("Luna")
print(rex.info()) # "Rex (Canis lupus)" ← heredado
print(rex.ladrar()) # "¡Guau!" ← propio
print(rex.respirar()) # "Rex jadea feliz." ← sobrescrito
print(luna.maullar()) # "¡Miau!"
print(luna.vivo) # True ← heredado
# Comprobar herencia:
isinstance(rex, Perro) # True
isinstance(rex, Animal) # True ← también es Animal
issubclass(Perro, Animal)# True
🎭 Polimorfismo: misma interfaz, comportamientos distintos
class Forma:
def area(self):
raise NotImplementedError("Las subclases deben implementar area()")
def describir(self):
return f"Soy un(a) {type(self).__name__} con área {self.area():.2f}"
class Circulo(Forma):
def __init__(self, radio):
self.radio = radio
def area(self):
import math
return math.pi * self.radio ** 2
class Rectangulo(Forma):
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
def area(self):
return self.ancho * self.alto
class Triangulo(Forma):
def __init__(self, base, altura):
self.base = base
self.altura = altura
def area(self):
return 0.5 * self.base * self.altura
# Polimorfismo: la misma función trabaja con cualquier Forma
formas = [Circulo(5), Rectangulo(4, 6), Triangulo(3, 8)]
for f in formas:
print(f.describir()) # cada una calcula su propia área
# Área total — no importa qué tipo de forma sea:
total = sum(f.area() for f in formas)
print(f"Área total: {total:.2f}")
🔒 @property: encapsulación con estilo Python
En lugar de métodos get_algo() y set_algo() al estilo Java, Python usa el decorador @property para acceder a atributos con validación como si fueran atributos normales.
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0):
self.titular = titular
self._saldo = saldo_inicial # _ indica "uso interno"
@property
def saldo(self): # getter: cuenta.saldo
return self._saldo
@saldo.setter
def saldo(self, valor): # setter: cuenta.saldo = 500
if valor < 0:
raise ValueError("El saldo no puede ser negativo.")
self._saldo = valor
def ingresar(self, cantidad):
if cantidad <= 0:
raise ValueError("La cantidad debe ser positiva.")
self._saldo += cantidad
return self._saldo
def retirar(self, cantidad):
if cantidad > self._saldo:
raise ValueError("Saldo insuficiente.")
self._saldo -= cantidad
return self._saldo
def __str__(self):
return f"Cuenta de {self.titular}: {self._saldo:.2f} €"
cuenta = CuentaBancaria("Ana", 1000)
print(cuenta.saldo) # 1000 ← usa el getter
cuenta.saldo = 1500 # ← usa el setter (valida)
cuenta.ingresar(200)
cuenta.retirar(300)
print(cuenta) # "Cuenta de Ana: 1400.00 €"
try:
cuenta.saldo = -100 # ValueError
except ValueError as e:
print(e)
✨ Métodos especiales: hacer objetos nativos de Python
class Vector:
"""Vector 2D con operaciones aritméticas."""
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Vector({self.x}, {self.y})"
def __repr__(self):
return f"Vector(x={self.x}, y={self.y})"
def __add__(self, otro):
return Vector(self.x + otro.x, self.y + otro.y)
def __sub__(self, otro):
return Vector(self.x - otro.x, self.y - otro.y)
def __mul__(self, escalar):
return Vector(self.x * escalar, self.y * escalar)
def __eq__(self, otro):
return self.x == otro.x and self.y == otro.y
def __len__(self):
import math
return int(math.sqrt(self.x**2 + self.y**2))
def __abs__(self):
import math
return math.sqrt(self.x**2 + self.y**2)
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1) # "Vector(1, 2)" ← __str__
print(v1 + v2) # "Vector(4, 6)" ← __add__
print(v2 - v1) # "Vector(2, 2)" ← __sub__
print(v1 * 3) # "Vector(3, 6)" ← __mul__
print(v1 == v2) # False ← __eq__
print(abs(v2)) # 5.0 ← __abs__
⚙️ @classmethod y @staticmethod
class Fecha:
def __init__(self, dia, mes, anno):
self.dia = dia
self.mes = mes
self.anno = anno
def __str__(self):
return f"{self.dia:02d}/{self.mes:02d}/{self.anno}"
@classmethod
def desde_string(cls, cadena):
"""Constructor alternativo desde 'DD/MM/AAAA'."""
dia, mes, anno = map(int, cadena.split("/"))
return cls(dia, mes, anno) # cls = Fecha
@classmethod
def hoy(cls):
"""Devuelve la fecha actual."""
from datetime import date
d = date.today()
return cls(d.day, d.month, d.year)
@staticmethod
def es_bisiesto(anno):
"""No necesita acceso a la instancia ni a la clase."""
return anno % 4 == 0 and (anno % 100 != 0 or anno % 400 == 0)
# @classmethod — constructores alternativos
f1 = Fecha(6, 3, 2026)
f2 = Fecha.desde_string("15/08/2025")
f3 = Fecha.hoy()
print(f1) # "06/03/2026"
print(f2) # "15/08/2025"
# @staticmethod — utilidad sin estado
Fecha.es_bisiesto(2024) # True
Fecha.es_bisiesto(2026) # False
🛠️ Programa completo: sistema de biblioteca
from datetime import date, timedelta
class Libro:
def __init__(self, titulo, autor, isbn):
self.titulo = titulo
self.autor = autor
self.isbn = isbn
self._prestado = False
@property
def disponible(self):
return not self._prestado
def __str__(self):
estado = "disponible" if self.disponible else "prestado"
return f'"{self.titulo}" — {self.autor} [{estado}]'
def __repr__(self):
return f"Libro(isbn={self.isbn!r})"
class Prestamo:
DIAS_PRESTAMO = 14
def __init__(self, libro, socio):
if not libro.disponible:
raise ValueError(f"{libro.titulo} ya está prestado.")
self.libro = libro
self.socio = socio
self.fecha_inicio = date.today()
self.fecha_limite = self.fecha_inicio + timedelta(days=self.DIAS_PRESTAMO)
libro._prestado = True
def devolver(self):
self.libro._prestado = False
dias_retraso = (date.today() - self.fecha_limite).days
if dias_retraso > 0:
return f"Devuelto con {dias_retraso} días de retraso."
return "Devuelto a tiempo. ¡Gracias!"
def __str__(self):
return (f"Préstamo: {self.libro.titulo} → {self.socio} "
f"(hasta {self.fecha_limite.strftime('%d/%m/%Y')})")
class Biblioteca:
def __init__(self, nombre):
self.nombre = nombre
self._libros = {} # isbn → Libro
def registrar(self, libro):
self._libros[libro.isbn] = libro
print(f"✓ Registrado: {libro}")
def buscar(self, termino):
termino = termino.lower()
return [l for l in self._libros.values()
if termino in l.titulo.lower() or termino in l.autor.lower()]
def disponibles(self):
return [l for l in self._libros.values() if l.disponible]
def __len__(self):
return len(self._libros)
def __str__(self):
return f"Biblioteca {self.nombre} ({len(self)} libros)"
# ── Uso del sistema ─────────────────────────────────
bib = Biblioteca("Ciberaula")
b1 = Libro("Python Crash Course", "Eric Matthes", "978-1593279288")
b2 = Libro("Clean Code", "Robert C. Martin", "978-0132350884")
b3 = Libro("Automate the Boring Stuff", "Al Sweigart", "978-1593275990")
for libro in [b1, b2, b3]:
bib.registrar(libro)
print(bib) # "Biblioteca Ciberaula (3 libros)"
p1 = Prestamo(b1, "Ana García")
print(p1)
resultados = bib.buscar("python")
for r in resultados:
print(r)
print(f"\nDisponibles: {len(bib.disponibles())}/3")
print(p1.devolver())
🐛 Errores clásicos con clases
1. Olvidar self en los métodos
# ❌
class Contador:
def incrementar(): # falta self
self.n += 1 # NameError: name 'self' is not defined
# ✅
class Contador:
def __init__(self):
self.n = 0
def incrementar(self):
self.n += 1
2. Atributo mutable compartido como atributo de clase
# ❌ Todos los objetos comparten la misma lista
class Alumno:
notas = [] # atributo de clase — ¡peligro!
def agregar(self, nota):
self.notas.append(nota)
a1 = Alumno(); a2 = Alumno()
a1.agregar(9)
print(a2.notas) # [9] — a2 también tiene la nota de a1
# ✅ Lista como atributo de instancia
class Alumno:
def __init__(self):
self.notas = [] # cada instancia tiene su propia lista
3. Llamar al padre sin super()
# ❌ Rompe con herencia múltiple y es frágil
class Perro(Animal):
def __init__(self, nombre, raza):
Animal.__init__(self, nombre) # hardcoded
# ✅
class Perro(Animal):
def __init__(self, nombre, raza):
super().__init__(nombre) # flexible y correcto
4. Modificar atributos "privados" directamente desde fuera
# ❌ Salta la validación del setter
cuenta._saldo = -9999 # no usa el setter, rompe la lógica interna
# ✅ Usar siempre la interfaz pública
cuenta.saldo = 500 # usa el setter con validación
✅ Resumen y próximos pasos
Una clase agrupa datos (atributos) y comportamiento (métodos). __init__ inicializa cada instancia. self es la referencia al objeto concreto. La herencia reutiliza código sin copiarlo — usa siempre super(). El polimorfismo permite tratar objetos distintos con la misma interfaz. @property añade validación manteniendo la sintaxis limpia. Los métodos especiales (dunder) hacen que tus objetos funcionen de forma nativa con operadores, len(), str() y más.
La siguiente lección: iteradores y generadores — cómo crear objetos iterables propios, la palabra clave yield y por qué los generadores son más eficientes que las listas para datos grandes.
❓ Preguntas frecuentes
❓ Preguntas frecuentes sobre Programación orientada a objetos en Python: clases, objetos y herencia
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Programación orientada a objetos en Python: clases, objetos y herencia? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!