Clases y Objetos en Python

La Programación Orientada a Objetos (POO) organiza el código en torno a objetos que combinan datos y comportamiento. Es el paradigma más usado en proyectos reales.

¿Qué es una clase?

Una clase es un molde o plano para crear objetos. Un objeto es una instancia concreta de esa clase. Cada objeto tiene sus propios atributos (datos) y métodos (funciones).

class Persona:
    def __init__(self, nombre, edad):
        self.nombre = nombre   # atributo
        self.edad   = edad     # atributo

    def saludar(self):         # método
        print(f"Hola, soy {self.nombre} y tengo {self.edad} años")

# Crear objetos (instanciar la clase)
ana    = Persona("Ana", 28)
carlos = Persona("Carlos", 35)

ana.saludar()     # Hola, soy Ana y tengo 28 años
carlos.saludar()  # Hola, soy Carlos y tengo 35 años

print(ana.nombre)  # Ana
print(ana.edad)    # 28
__init__ y self

__init__ es el constructor — se ejecuta automáticamente cuando creás un objeto. self es la referencia al objeto actual y debe ser el primer parámetro de todos los métodos de la clase.

Probá el Ejemplo 1: Clase básica

Atributos y métodos

Atributos de instancia vs atributos de clase

class Contador:
    total = 0  # atributo de clase — compartido entre todas las instancias

    def __init__(self, nombre):
        self.nombre  = nombre  # atributo de instancia — único por objeto
        Contador.total += 1

    def info(self):
        print(f"{self.nombre} — total creados: {Contador.total}")

c1 = Contador("A")
c2 = Contador("B")
c3 = Contador("C")

c1.info()  # A — total creados: 3
c2.info()  # B — total creados: 3

Métodos especiales (dunder methods)

class Producto:
    def __init__(self, nombre, precio):
        self.nombre = nombre
        self.precio = precio

    def __str__(self):
        """Representación legible — usada por print()"""
        return f"{self.nombre} (${self.precio:.2f})"

    def __repr__(self):
        """Representación técnica — usada en la consola"""
        return f"Producto('{self.nombre}', {self.precio})"

p = Producto("Laptop", 1200)
print(p)       # Laptop ($1200.00)  ← usa __str__
print(repr(p)) # Producto('Laptop', 1200) ← usa __repr__
Probá el Ejemplo 2: Atributos y métodos

Herencia

Una clase puede heredar atributos y métodos de otra. La clase que hereda se llama subclase; la que se hereda, superclase.

class Animal:
    def __init__(self, nombre, especie):
        self.nombre  = nombre
        self.especie = especie

    def describir(self):
        print(f"{self.nombre} es un {self.especie}")

    def hablar(self):
        print("...")

class Perro(Animal):     # Perro hereda de Animal
    def __init__(self, nombre):
        super().__init__(nombre, "perro")  # llama al __init__ del padre
        self.trucos = []

    def hablar(self):    # sobreescribe el método del padre
        print("¡Guau!")

    def aprender(self, truco):
        self.trucos.append(truco)
        print(f"{self.nombre} aprendió: {truco}")

class Gato(Animal):
    def __init__(self, nombre):
        super().__init__(nombre, "gato")

    def hablar(self):
        print("¡Miau!")

rex  = Perro("Rex")
misu = Gato("Misu")

rex.describir()          # Rex es un perro  ← método heredado
rex.hablar()             # ¡Guau!           ← método sobreescrito
rex.aprender("sentarse") # Rex aprendió: sentarse
misu.hablar()            # ¡Miau!
super()

super() llama al método de la clase padre. Es muy útil en __init__ para inicializar los atributos heredados sin repetir código.

Probá el Ejemplo 3: Herencia

Encapsulamiento

El encapsulamiento oculta los detalles internos de un objeto. En Python se usa por convención: atributos con _ son "privados" por acuerdo, con __ tienen name mangling real.

class CuentaBancaria:
    def __init__(self, titular, saldo_inicial=0):
        self.titular = titular
        self._saldo  = saldo_inicial  # "privado" por convención

    def depositar(self, monto):
        if monto <= 0:
            raise ValueError("El monto debe ser positivo")
        self._saldo += monto
        print(f"Depósito: +${monto} — Saldo: ${self._saldo}")

    def retirar(self, monto):
        if monto > self._saldo:
            raise ValueError("Saldo insuficiente")
        self._saldo -= monto
        print(f"Retiro: -${monto} — Saldo: ${self._saldo}")

    @property
    def saldo(self):
        """Getter: permite leer _saldo como si fuera un atributo normal."""
        return self._saldo

cuenta = CuentaBancaria("Ana", 1000)
cuenta.depositar(500)
cuenta.retirar(200)
print(f"Saldo final: ${cuenta.saldo}")
@property

El decorador @property permite acceder a un método como si fuera un atributo: cuenta.saldo en vez de cuenta.saldo(). Es útil para controlar el acceso a datos internos.

Ejemplo Completo

Sistema de biblioteca con herencia y encapsulamiento:

class Publicacion:
    def __init__(self, titulo, autor, anio):
        self.titulo = titulo
        self.autor  = autor
        self.anio   = anio
        self._prestado = False

    def prestar(self):
        if self._prestado:
            print(f"'{self.titulo}' ya está prestado")
            return False
        self._prestado = True
        print(f"'{self.titulo}' prestado")
        return True

    def devolver(self):
        self._prestado = False
        print(f"'{self.titulo}' devuelto")

    @property
    def disponible(self):
        return not self._prestado

    def __str__(self):
        estado = "disponible" if self.disponible else "prestado"
        return f"{self.titulo} — {self.autor} ({self.anio}) [{estado}]"

class Libro(Publicacion):
    def __init__(self, titulo, autor, anio, paginas):
        super().__init__(titulo, autor, anio)
        self.paginas = paginas

    def __str__(self):
        return f"[Libro] {super().__str__()} — {self.paginas} págs"

class Revista(Publicacion):
    def __init__(self, titulo, autor, anio, numero):
        super().__init__(titulo, autor, anio)
        self.numero = numero

    def __str__(self):
        return f"[Revista] {super().__str__()} — N°{self.numero}"

# Uso
biblioteca = [
    Libro("Python Crash Course", "Eric Matthes", 2019, 544),
    Libro("Clean Code", "Robert Martin", 2008, 431),
    Revista("Nature", "Varios", 2024, 612),
]

print("=== Catálogo ===")
for pub in biblioteca:
    print(f"  {pub}")

print("\n=== Operaciones ===")
biblioteca[0].prestar()
biblioteca[0].prestar()  # ya prestado
biblioteca[0].devolver()

print("\n=== Disponibles ===")
for pub in biblioteca:
    if pub.disponible:
        print(f"  ✅ {pub.titulo}")
Probá el Ejemplo 4: Sistema de biblioteca

Buenas Prácticas

  • Nombres en PascalCase — las clases van con mayúscula inicial: CuentaBancaria
  • Una responsabilidad por clase — igual que con las funciones, cada clase debe hacer una cosa bien
  • __str__ siempre — definí la representación legible para facilitar el debugging
  • Preferí composición sobre herencia — no heredes solo para reutilizar código; usá composición (una clase que tiene instancias de otra)
  • No abuses de __ — el guión bajo simple _attr es suficiente en la mayoría de los casos
  • Docstrings en clases y métodos — documenten el propósito y los parámetros