Introducción
Python soporta varios paradigmas de programación, pero la Programación Orientada a Objetos es la más usada en proyectos medianos y grandes. Django, Flask y FastAPI organizan su código entero alrededor de clases.
A diferencia de Java, en Python la POO es flexible: puedes tener clases sin encapsulamiento estricto, herencia múltiple, y los métodos especiales (__dunder__) hacen que tus objetos se comporten como tipos nativos del lenguaje.
¿Qué vas a aprender en este lab?
- Crear clases y objetos con
__init__ - Usar atributos de instancia y de clase
- Controlar el acceso con propiedades (
@property) - Reutilizar código con herencia
- Sobreescribir métodos especiales (
__str__,__eq__,__len__) - Simplificar clases de datos con
dataclass
Requisitos previos: fundamentos de Python (variables, funciones, condicionales, bucles).
1. Clases y Objetos
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def saludar(self):
print(f"Hola, soy {self.nombre} y tengo {self.edad} años.")
__init__ es el constructor: se llama automáticamente al crear un objeto con Persona(...). El primer parámetro de cada método es self — la referencia al propio objeto (equivalente a this en Java).
julian = Persona("Julian", 25)
ana = Persona("Ana", 30)
julian.saludar() # Hola, soy Julian y tengo 25 años.
ana.saludar() # Hola, soy Ana y tengo 30 años.
print(julian.nombre) # Julian
julian.edad = 26 # los atributos son públicos por defecto
Atributos de clase vs de instancia
class Contador:
total = 0 # atributo de clase: compartido por todas las instancias
def __init__(self, nombre):
self.nombre = nombre # atributo de instancia: único por objeto
Contador.total += 1
c1 = Contador("A")
c2 = Contador("B")
print(Contador.total) # 2
Ejercicio 1
Crea una clase Rectangulo con atributos ancho y alto. Añade métodos area(), perimetro() y es_cuadrado(). Crea tres rectángulos distintos y prueba todos los métodos.
2. Métodos especiales (dunder methods)
Los métodos con doble guión bajo al inicio y al final (__nombre__) permiten que tus objetos se comporten como tipos nativos de Python.
class Producto:
def __init__(self, nombre, precio):
self.nombre = nombre
self.precio = precio
def __str__(self):
return f"{self.nombre} (${self.precio:.2f})"
def __repr__(self):
return f"Producto(nombre={self.nombre!r}, precio={self.precio})"
def __eq__(self, otro):
return self.nombre == otro.nombre and self.precio == otro.precio
def __lt__(self, otro):
return self.precio < otro.precio
p1 = Producto("Laptop", 1200.0)
p2 = Producto("Mouse", 25.0)
p3 = Producto("Laptop", 1200.0)
print(p1) # Laptop ($1200.00) — usa __str__
print(repr(p1)) # Producto(nombre='Laptop', precio=1200.0) — usa __repr__
print(p1 == p3) # True — usa __eq__
print(p2 < p1) # True — usa __lt__
productos = [p1, p2, Producto("Teclado", 80.0)]
print(sorted(productos)) # [Mouse ($25.00), Teclado ($80.00), Laptop ($1200.00)]
Métodos especiales más usados
| Método | Se invoca con | Para qué |
|---|---|---|
__init__ |
Clase() |
Constructor |
__str__ |
str(obj), print(obj) |
Representación legible |
__repr__ |
repr(obj), consola REPL |
Representación de depuración |
__eq__ |
obj1 == obj2 |
Comparación de igualdad |
__lt__ |
obj1 < obj2 |
Comparación de orden |
__len__ |
len(obj) |
Longitud |
__contains__ |
x in obj |
Pertenencia |
3. Propiedades — control de acceso idiomático
Python no tiene private estricto. Por convención:
_nombre— interno, no uses fuera de la clase (puede cambiar)__nombre— name mangling: Python lo renombra a_Clase__nombre
Para validar al asignar valores, usa @property en lugar de setters/getters al estilo Java.
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0.0):
self.titular = titular
self._saldo = saldo_inicial # _saldo: convención "privado"
@property
def saldo(self):
return self._saldo
@saldo.setter
def saldo(self, valor):
if valor < 0:
raise ValueError("El saldo no puede ser negativo")
self._saldo = valor
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(f"Saldo insuficiente. Disponible: ${self._saldo:.2f}")
self._saldo -= monto
def __str__(self):
return f"Cuenta de {self.titular}: ${self._saldo:.2f}"
cuenta = CuentaBancaria("Julian", 1000.0)
cuenta.depositar(500.0)
print(cuenta.saldo) # 1500.0 — usa el getter
cuenta.saldo = 2000.0 # usa el setter (validado)
print(cuenta) # Cuenta de Julian: $2000.00
cuenta.saldo = -100 # ValueError: El saldo no puede ser negativo
Ejercicio 2
Crea una clase Temperatura con atributo interno _celsius. Añade propiedades celsius, fahrenheit y kelvin — las dos últimas son solo lectura y se calculan a partir de _celsius. El setter de celsius rechaza valores menores a −273.15 (cero absoluto).
4. Herencia
class Animal:
def __init__(self, nombre, especie):
self.nombre = nombre
self.especie = especie
def hacer_sonido(self):
print(f"{self.nombre} hace un sonido.")
def __str__(self):
return f"{self.nombre} ({self.especie})"
class Perro(Animal):
def __init__(self, nombre, raza):
super().__init__(nombre, "Canis lupus familiaris")
self.raza = raza
def hacer_sonido(self):
print(f"{self.nombre} ladra: ¡Guau!")
def traer(self, objeto):
print(f"{self.nombre} trae el {objeto}.")
class Gato(Animal):
def __init__(self, nombre, es_domestico=True):
super().__init__(nombre, "Felis catus")
self.es_domestico = es_domestico
def hacer_sonido(self):
print(f"{self.nombre} maúlla: ¡Miau!")
rex = Perro("Rex", "Labrador")
luna = Gato("Luna")
rex.hacer_sonido() # Rex ladra: ¡Guau!
luna.hacer_sonido() # Luna maúlla: ¡Miau!
rex.traer("palo") # Rex trae el palo.
print(isinstance(rex, Perro)) # True
print(isinstance(rex, Animal)) # True — un Perro también es un Animal
Herencia múltiple
Python permite heredar de más de una clase (a diferencia de Java):
class Nadador:
def nadar(self):
print(f"{self.nombre} nada.")
class Volador:
def volar(self):
print(f"{self.nombre} vuela.")
class Pato(Animal, Nadador, Volador):
def __init__(self, nombre):
super().__init__(nombre, "Anas platyrhynchos")
def hacer_sonido(self):
print(f"{self.nombre}: ¡Cuac!")
pato = Pato("Donald")
pato.hacer_sonido() # Donald: ¡Cuac!
pato.nadar() # Donald nada.
pato.volar() # Donald vuela.
Ejercicio 3
Crea una jerarquía de clases para figuras geométricas: clase base Figura con método abstracto area() y perimetro(). Implementa Circulo, Rectangulo y Triangulo. Usa from abc import ABC, abstractmethod para hacer los métodos abstractos.
5. Clases abstractas e interfaces
from abc import ABC, abstractmethod
import math
class Figura(ABC):
def __init__(self, color="blanco"):
self.color = color
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def perimetro(self) -> float:
pass
def describir(self):
print(f"Figura de color {self.color}")
print(f" Área: {self.area():.2f}")
print(f" Perímetro: {self.perimetro():.2f}")
class Circulo(Figura):
def __init__(self, radio, color="blanco"):
super().__init__(color)
self.radio = radio
def area(self):
return math.pi * self.radio ** 2
def perimetro(self):
return 2 * math.pi * self.radio
class Rectangulo(Figura):
def __init__(self, ancho, alto, color="blanco"):
super().__init__(color)
self.ancho = ancho
self.alto = alto
def area(self):
return self.ancho * self.alto
def perimetro(self):
return 2 * (self.ancho + self.alto)
# Figura() # TypeError: Can't instantiate abstract class Figura
c = Circulo(5, "rojo")
r = Rectangulo(4, 6, "azul")
figuras = [c, r]
for f in figuras:
f.describir()
print()
6. Dataclasses
Para clases que principalmente almacenan datos, @dataclass genera automáticamente __init__, __repr__ y __eq__:
from dataclasses import dataclass, field
@dataclass
class Punto:
x: float
y: float
def distancia_origen(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
@dataclass
class Estudiante:
nombre: str
edad: int
notas: list[float] = field(default_factory=list) # lista vacía por defecto
@property
def promedio(self) -> float:
return sum(self.notas) / len(self.notas) if self.notas else 0.0
def agregar_nota(self, nota: float):
self.notas.append(nota)
p = Punto(3.0, 4.0)
print(p) # Punto(x=3.0, y=4.0)
print(p.distancia_origen()) # 5.0
e = Estudiante("Julian", 25)
e.agregar_nota(8.5)
e.agregar_nota(9.0)
e.agregar_nota(7.5)
print(e.promedio) # 8.333...
@dataclass es la forma idiomática en Python moderno (3.7+) para clases de datos simples — evita repetir código de inicialización.
7. Errores Comunes
Error 1 — Olvidar self en los parámetros del método
# Incorrecto: Python no sabe cuál es la instancia
class Circulo:
def __init__(radio): # falta self
self.radio = radio # NameError: name 'self' is not defined
# Correcto
class Circulo:
def __init__(self, radio):
self.radio = radio
Error 2 — Atributo mutable por defecto en la clase
# Todos los objetos comparten la MISMA lista
class Estudiante:
notas = [] # atributo de clase — ¡peligro!
e1 = Estudiante()
e2 = Estudiante()
e1.notas.append(10)
print(e2.notas) # [10] — ¡compartido!
# Atributo de instancia en __init__
class Estudiante:
def __init__(self):
self.notas = [] # cada objeto tiene su propia lista
Error 3 — Olvidar llamar a super().__init__()
# El padre no se inicializa
class Perro(Animal):
def __init__(self, nombre, raza):
self.raza = raza
# Animal.__init__ nunca se llama
# self.nombre no existe → AttributeError
# Siempre llama al constructor del padre
class Perro(Animal):
def __init__(self, nombre, raza):
super().__init__(nombre, "Canis lupus familiaris")
self.raza = raza
Resumen
| Concepto | Python |
|---|---|
| Constructor | def __init__(self, ...) |
| Representación | def __str__(self) |
| Herencia | class Hijo(Padre): |
| Llamar al padre | super().__init__(...) |
| Propiedad | @property + @nombre.setter |
| Clase abstracta | class X(ABC): + @abstractmethod |
| Clase de datos | @dataclass |
Próximos pasos
- Colecciones y comprehensions: listas, diccionarios, conjuntos y generadores
- Módulos y paquetes: organizar código en múltiples archivos
- Decoradores:
@staticmethod,@classmethody decoradores personalizados