Volver a Labs
Intermedio

Programación Orientada a Objetos en Python

2–3 horas Actualizado Junio 2026
Python POO Backend Fundamentos

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, @classmethod y decoradores personalizados