Ir al contenido principal

Clases y objetos: conceptos básicos

Introducción

La Programación Orientada a Objetos (POO) es un paradigma fundamental en la programación moderna que permite estructurar el código de manera más organizada y reutilizable. Python, siendo un lenguaje versátil, incorpora este paradigma ofreciendo una sintaxis clara y potente para definir clases y crear objetos. Esta forma de programar nos permite modelar conceptos del mundo real como entidades digitales con propiedades (atributos) y comportamientos (métodos).

En este artículo, exploraremos los fundamentos de las clases y objetos en Python. Aprenderás cómo representar entidades, definir sus características y comportamientos, y crear instancias que puedas utilizar en tus programas. Estos conceptos son esenciales para desarrollar aplicaciones más complejas y mantenibles, especialmente cuando trabajas con sistemas que modelan el mundo real.

¿Qué son las clases y los objetos?

Una clase es una plantilla o un plano que define la estructura y el comportamiento que tendrán los objetos creados a partir de ella. Un objeto (también llamado instancia) es una entidad concreta creada a partir de dicha clase.

Para entenderlo mejor, podemos usar una analogía:

  • Una clase es como el plano para construir un edificio.
  • Un objeto es un edificio real construido siguiendo ese plano.

Definición de una clase

En Python, definimos una clase usando la palabra clave class. Veamos un ejemplo sencillo:

class Persona:
    """
    Esta clase representa a una persona con nombre y edad.
    """
    pass  # Por ahora, nuestra clase está vacía

En este ejemplo:

  • class es la palabra clave para definir una clase
  • Persona es el nombre de la clase (por convención, se usa CamelCase para los nombres de clases)
  • El bloque de texto entre triple comilla es un docstring que describe la clase
  • pass es una instrucción que no hace nada, la usamos como marcador temporal

Creación de objetos (instanciación)

Para crear un objeto a partir de una clase, llamamos a la clase como si fuera una función:

# Creamos dos personas
persona1 = Persona()
persona2 = Persona()

# Ahora tenemos dos objetos distintos de la clase Persona
print(persona1)
print(persona2)

Esto mostrará algo como:

<__main__.Persona object at 0x7f8b2d7e3c10>
<__main__.Persona object at 0x7f8b2d7e3c50>

Observa cómo cada objeto tiene una dirección de memoria diferente, lo que indica que son instancias independientes aunque creadas a partir de la misma clase.

Atributos de instancia

Los atributos son variables asociadas a un objeto. Para definir atributos de instancia, usualmente utilizamos un método especial llamado __init__ (el constructor):

class Persona:
    """
    Esta clase representa a una persona con nombre y edad.
    """
    
    def __init__(self, nombre, edad):
        """
        Inicializa una nueva persona.
        
        Args:
            nombre (str): El nombre de la persona
            edad (int): La edad de la persona en años
        """
        self.nombre = nombre
        self.edad = edad

Observaciones importantes:

  • __init__ es un método especial que se llama automáticamente cuando se crea un objeto
  • self es una referencia al objeto que se está creando (similar a this en otros lenguajes)
  • self.nombre y self.edad son atributos de instancia

Ahora podemos crear personas con nombre y edad:

# Creamos dos personas con diferentes atributos
ana = Persona("Ana García", 28)
luis = Persona("Luis Martínez", 35)

# Accedemos a los atributos de cada objeto
print(f"{ana.nombre} tiene {ana.edad} años")
print(f"{luis.nombre} tiene {luis.edad} años")

El resultado será:

Ana García tiene 28 años
Luis Martínez tiene 35 años

Métodos de instancia

Los métodos son funciones definidas dentro de una clase que describen los comportamientos que pueden tener los objetos:

class Persona:
    def __init__(self, nombre, edad):
        self.nombre = nombre
        self.edad = edad
    
    def saludar(self):
        """
        Método que hace que la persona se presente.
        """
        return f"Hola, me llamo {self.nombre} y tengo {self.edad} años."
    
    def cumplir_anos(self):
        """
        Incrementa la edad de la persona en un año.
        """
        self.edad += 1
        return f"¡Feliz cumpleaños! Ahora tengo {self.edad} años."

Características importantes de los métodos:

  • Todos los métodos de instancia reciben self como primer parámetro
  • Pueden acceder y modificar los atributos del objeto usando self
  • Pueden retornar valores, igual que las funciones normales

Veamos cómo usar estos métodos:

# Creamos una persona
maria = Persona("María López", 30)

# Llamamos a los métodos del objeto
print(maria.saludar())
print(maria.cumplir_anos())
print(maria.saludar())  # Notarás que la edad ha cambiado

Esto mostrará:

Hola, me llamo María López y tengo 30 años.
¡Feliz cumpleaños! Ahora tengo 31 años.
Hola, me llamo María López y tengo 31 años.

Atributos y métodos de clase

Además de los atributos y métodos de instancia, Python permite definir atributos y métodos a nivel de clase:

class Persona:
    # Atributo de clase
    especie = "Homo sapiens"
    
    def __init__(self, nombre, edad):
        # Atributos de instancia
        self.nombre = nombre
        self.edad = edad
    
    def saludar(self):
        # Método de instancia
        return f"Hola, me llamo {self.nombre}"
    
    @classmethod
    def crear_sin_edad(cls, nombre):
        # Método de clase
        return cls(nombre, 0)
    
    @staticmethod
    def es_adulto(edad):
        # Método estático
        return edad >= 18

Veamos las diferencias:

  • Atributos de clase: Compartidos por todas las instancias (como especie)
  • Métodos de clase: Usan el decorador @classmethod y reciben la clase (cls) como primer parámetro
  • Métodos estáticos: Usan el decorador @staticmethod y no reciben automáticamente ni self ni cls

Ejemplo de uso:

# Acceso a atributo de clase
print(Persona.especie)  # "Homo sapiens"

# Uso de un método de clase
bebe = Persona.crear_sin_edad("Bebé Rodríguez")
print(f"{bebe.nombre} tiene {bebe.edad} años")

# Uso de un método estático
print(f"¿Es adulto alguien de 16 años? {Persona.es_adulto(16)}")
print(f"¿Es adulto alguien de 21 años? {Persona.es_adulto(21)}")

Resultado:

Homo sapiens
Bebé Rodríguez tiene 0 años
¿Es adulto alguien de 16 años? False
¿Es adulto alguien de 21 años? True

Interacción entre objetos

Una de las ventajas de la programación orientada a objetos es la capacidad de modelar interacciones complejas entre diferentes entidades:

class Persona:
    def __init__(self, nombre, edad):
        self.nombre = nombre
        self.edad = edad
    
    def saludar(self, otra_persona=None):
        if otra_persona:
            return f"Hola {otra_persona.nombre}, me llamo {self.nombre}."
        else:
            return f"Hola, me llamo {self.nombre}."

# Creamos dos personas
juan = Persona("Juan Pérez", 25)
elena = Persona("Elena Gómez", 28)

# Juan saluda a Elena
print(juan.saludar(elena))

Resultado:

Hola Elena Gómez, me llamo Juan Pérez.

Ejemplo práctico: Una clase para representar cuentas bancarias

Veamos un ejemplo más completo para consolidar los conceptos aprendidos:

class CuentaBancaria:
    """
    Clase que representa una cuenta bancaria simple.
    """
    
    # Atributo de clase
    banco = "Banco Python"
    
    def __init__(self, titular, saldo_inicial=0):
        """
        Inicializa una nueva cuenta bancaria.
        
        Args:
            titular (str): Nombre del titular de la cuenta
            saldo_inicial (float, optional): Saldo inicial de la cuenta. Por defecto 0.
        """
        self.titular = titular
        self.__saldo = saldo_inicial  # Atributo "privado" (encapsulamiento)
        self.movimientos = []
        
        # Registramos el depósito inicial si es mayor que cero
        if saldo_inicial > 0:
            self.movimientos.append(f"Depósito inicial: +{saldo_inicial}€")
    
    def depositar(self, cantidad):
        """
        Deposita una cantidad en la cuenta.
        
        Args:
            cantidad (float): Cantidad a depositar (debe ser positiva)
            
        Returns:
            float: El nuevo saldo
        """
        if cantidad <= 0:
            print("Error: La cantidad debe ser positiva")
            return self.__saldo
        
        self.__saldo += cantidad
        self.movimientos.append(f"Depósito: +{cantidad}€")
        return self.__saldo
    
    def retirar(self, cantidad):
        """
        Retira una cantidad de la cuenta si hay saldo suficiente.
        
        Args:
            cantidad (float): Cantidad a retirar (debe ser positiva)
            
        Returns:
            float: El nuevo saldo o None si la operación falla
        """
        if cantidad <= 0:
            print("Error: La cantidad debe ser positiva")
            return self.__saldo
        
        if cantidad > self.__saldo:
            print("Error: Saldo insuficiente")
            return self.__saldo
        
        self.__saldo -= cantidad
        self.movimientos.append(f"Retiro: -{cantidad}€")
        return self.__saldo
    
    def consultar_saldo(self):
        """
        Devuelve el saldo actual de la cuenta.
        
        Returns:
            float: El saldo actual
        """
        return self.__saldo
    
    def ver_movimientos(self):
        """
        Muestra el historial de movimientos de la cuenta.
        """
        print(f"Movimientos de la cuenta de {self.titular}:")
        for movimiento in self.movimientos:
            print(f"- {movimiento}")
        print(f"Saldo actual: {self.__saldo}€")

# Uso de la clase
cuenta_ana = CuentaBancaria("Ana López", 1000)
cuenta_ana.depositar(500)
cuenta_ana.retirar(200)
cuenta_ana.ver_movimientos()
print(f"Saldo final: {cuenta_ana.consultar_saldo()}€")

Este ejemplo muestra:

  • Diseño de una clase con atributos y métodos coherentes
  • Encapsulamiento con atributos "privados" (usando doble guion bajo)
  • Validación de operaciones
  • Mantenimiento de un historial de acciones
  • Interfaz clara para interactuar con el objeto

Resumen

Las clases y objetos son fundamentales en la programación orientada a objetos y Python ofrece una sintaxis elegante para trabajar con ellos. En este artículo, hemos aprendido a definir clases, crear objetos, añadir atributos y métodos, y hacer que diferentes objetos interactúen entre sí.

Estos conceptos básicos son la puerta de entrada al mundo de la programación orientada a objetos, que continuaremos explorando en los siguientes artículos con temas como la herencia, el polimorfismo y la encapsulación avanzada. Con lo aprendido hasta ahora, ya puedes comenzar a estructurar tus programas Python de manera más organizada y reutilizable, modelando problemas del mundo real con clases y objetos.