POO en Python: clases y objetos explicados paso a paso

D
DanisCh
(Actualizado: ) • 12 min de lectura
POO en Python: clases y objetos explicados paso a paso
Python

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 _nombre indica 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 __nombre activa 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.

Etiquetas: Python POO

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

1 COMENTARIOS
D
Denisse Alvarado
2 months ago
Explicación clara de ejercicios
Escribe tu opinión
Respondiendo a