Python

Python

Tutorial Python: Clases y objetos

Aprende los conceptos clave de clases y objetos en Python para dominar la programación orientada a objetos con ejemplos prácticos y buenas prácticas.

Aprende Python y certifícate

Concepto teórico de clase y objeto

La programación orientada a objetos (POO) representa un paradigma que nos permite modelar el mundo real en nuestro código. En Python, como en otros lenguajes de programación modernos, este enfoque se implementa mediante dos conceptos fundamentales: clases y objetos.

Imagina que quieres construir casas. Antes de construir una casa real, necesitas un plano que defina cómo será esa casa: cuántas habitaciones tendrá, cómo se distribuirán, qué materiales se usarán, etc. En programación, este plano sería una clase. Una vez que tienes el plano, puedes construir múltiples casas basadas en él; estas casas serían los objetos.

Clases: los planos

Una clase es esencialmente una plantilla o un plano que define:

  • Las características (atributos) que tendrán los objetos creados a partir de ella
  • Los comportamientos (métodos) que podrán realizar estos objetos

En Python, definimos una clase usando la palabra clave class:

class Coche:
    # Aquí definiremos los atributos y métodos
    pass

Esta definición crea un "plano" para coches, pero aún no hemos creado ningún coche real. Es como tener el diseño de un coche en papel, pero sin haber fabricado ninguno todavía.

Objetos: las instancias concretas

Un objeto (también llamado instancia) es una realización concreta de una clase. Si la clase es el plano, el objeto es la casa construida siguiendo ese plano. Cada objeto:

  • Tiene su propio conjunto de valores para los atributos definidos en la clase
  • Puede realizar las acciones (métodos) definidas en la clase

Creamos un objeto a partir de una clase llamando a la clase como si fuera una función:

# Creamos dos objetos de tipo Coche
mi_coche = Coche()
coche_de_amigo = Coche()

Ahora tenemos dos objetos diferentes (mi_coche y coche_de_amigo), ambos basados en la misma clase Coche. Son como dos casas construidas siguiendo el mismo plano, pero que pueden tener características diferentes (color, tamaño, etc.).

La relación entre clases y objetos

Para entender mejor la relación entre clases y objetos, consideremos algunos ejemplos cotidianos:

  • Clase: Perro → Objetos: Fido, Rex, Lassie
  • Clase: Smartphone → Objetos: Mi iPhone, Tu Samsung Galaxy
  • Clase: Cuenta Bancaria → Objetos: Mi cuenta corriente, Tu cuenta de ahorros

Cada objeto es una instancia única de su clase, con sus propios valores para los atributos definidos en la clase. Por ejemplo, todos los perros tienen una raza y una edad (atributos definidos en la clase Perro), pero cada perro concreto tendrá valores específicos para esos atributos (Fido puede ser un Labrador de 3 años, mientras que Rex puede ser un Pastor Alemán de 5 años).

Abstracción y encapsulamiento

La POO nos permite implementar dos conceptos importantes:

  • Abstracción: Nos permite centrarnos en lo que hace un objeto, sin preocuparnos por cómo lo hace internamente. Por ejemplo, cuando usamos un smartphone, no necesitamos saber cómo funciona su circuitería interna.

  • Encapsulamiento: Nos permite agrupar datos (atributos) y comportamientos (métodos) relacionados en una sola unidad (la clase), y controlar el acceso a estos componentes.

Ventajas del enfoque orientado a objetos

El uso de clases y objetos en Python ofrece varias ventajas:

  • Organización del código: Agrupar datos y comportamientos relacionados hace que el código sea más organizado y fácil de entender.
  • Reutilización: Una vez definida una clase, podemos crear múltiples objetos basados en ella sin repetir código.
  • Modularidad: Podemos modificar la implementación interna de una clase sin afectar al código que la utiliza, siempre que mantengamos la misma interfaz.
  • Modelado del mundo real: Las clases y objetos nos permiten representar entidades del mundo real de manera intuitiva en nuestro código.

Ejemplo conceptual

Para ilustrar estos conceptos, pensemos en una clase Libro:

class Libro:
    # Aquí definiremos atributos como título, autor, páginas
    # Y métodos como abrir(), leer(), cerrar()
    pass

# Creamos objetos (instancias) de la clase Libro
libro_python = Libro()  # Un libro específico sobre Python
novela_fantasia = Libro()  # Una novela de fantasía específica

Cada libro tendrá sus propios valores para atributos como título y autor, y podrá realizar acciones como ser abierto o leído. La clase Libro define qué características y comportamientos tendrán todos los libros, mientras que cada objeto representa un libro específico con sus propios valores para esas características.

En las siguientes secciones, veremos cómo implementar concretamente estos conceptos en Python, definiendo constructores, atributos y métodos para nuestras clases.

Clase y constructor

Una vez comprendido el concepto teórico de clases y objetos, es momento de aprender cómo implementar clases en Python y cómo crear objetos a partir de ellas. El primer paso para definir una clase funcional es entender el constructor, que es el método especial que se ejecuta automáticamente cuando creamos un nuevo objeto.

En Python, una clase se define utilizando la palabra clave class seguida del nombre de la clase (generalmente en formato PascalCase) y dos puntos. Dentro del bloque de la clase, definiremos sus atributos y métodos:

class Persona:
    # Aquí irá el código de la clase
    pass

El método constructor: __init__

El constructor es un método especial llamado __init__ (con doble guion bajo al inicio y al final) que Python ejecuta automáticamente cada vez que creamos una nueva instancia de la clase. Su principal función es inicializar los atributos del objeto recién creado.

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

Analicemos este constructor:

  • def __init__(self, nombre, edad): - Define el método constructor que recibe tres parámetros:

  • self - Referencia al objeto que se está creando (siempre es el primer parámetro)

  • nombre y edad - Datos que queremos asignar al nuevo objeto

  • self.nombre = nombre - Crea un atributo llamado nombre en el objeto y le asigna el valor del parámetro nombre

  • self.edad = edad - Crea un atributo llamado edad en el objeto y le asigna el valor del parámetro edad

El parámetro self

El parámetro self es una referencia al objeto que se está creando o manipulando. Es obligatorio como primer parámetro en todos los métodos de instancia (incluido el constructor), pero no lo proporcionamos explícitamente al crear el objeto o llamar a sus métodos. Python lo pasa automáticamente.

# Creamos un objeto Persona
ana = Persona("Ana García", 28)

# Python internamente hace algo equivalente a:
# Persona.__init__(ana, "Ana García", 28)

Cuando creamos el objeto ana, Python llama automáticamente al método __init__ pasando el nuevo objeto como self y los argumentos que proporcionamos.

Creación de objetos

Para crear un objeto (instancia) de una clase, simplemente llamamos a la clase como si fuera una función, pasando los argumentos que requiere su constructor:

# Creamos dos objetos Persona
ana = Persona("Ana García", 28)
juan = Persona("Juan López", 35)

# Accedemos a sus atributos
print(ana.nombre)  # Imprime: Ana García
print(juan.edad)   # Imprime: 35

Cada objeto tiene sus propios valores para los atributos definidos en la clase. En este caso, ana y juan son dos objetos independientes, cada uno con sus propios valores de nombre y edad.

Valores predeterminados en el constructor

Podemos definir valores predeterminados para los parámetros del constructor, haciendo que sean opcionales:

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

# Creamos productos con y sin especificar el stock
laptop = Producto("Laptop XPS", 1200)  # stock será 0
teclado = Producto("Teclado mecánico", 80, 15)  # stock será 15

print(laptop.stock)  # Imprime: 0
print(teclado.stock)  # Imprime: 15

Inicialización de atributos con cálculos

El constructor no solo asigna valores directamente, también puede realizar cálculos para inicializar atributos:

class Rectangulo:
    def __init__(self, ancho, alto):
        self.ancho = ancho
        self.alto = alto
        self.area = ancho * alto  # Calculamos y almacenamos el área
        self.perimetro = 2 * (ancho + alto)  # Calculamos y almacenamos el perímetro

# Creamos un rectángulo
rect = Rectangulo(5, 3)
print(rect.area)      # Imprime: 15
print(rect.perimetro) # Imprime: 16

Atributos con validación

El constructor también puede incluir validaciones para asegurar que los objetos se creen con valores válidos:

class Cuenta:
    def __init__(self, titular, saldo_inicial):
        self.titular = titular
        
        # Validamos que el saldo inicial no sea negativo
        if saldo_inicial < 0:
            raise ValueError("El saldo inicial no puede ser negativo")
        
        self.saldo = saldo_inicial

# Esto funcionará
cuenta_ana = Cuenta("Ana García", 1000)

# Esto lanzará un ValueError
try:
    cuenta_problematica = Cuenta("Juan López", -500)
except ValueError as e:
    print(f"Error: {e}")  # Imprime: Error: El saldo inicial no puede ser negativo

Ejemplo práctico: Modelando una biblioteca

Veamos un ejemplo más completo modelando libros en una biblioteca:

class Libro:
    def __init__(self, titulo, autor, paginas, isbn, disponible=True):
        self.titulo = titulo
        self.autor = autor
        self.paginas = paginas
        self.isbn = isbn
        self.disponible = disponible
        self.pagina_actual = 0  # Inicializamos en la página 0 (cerrado)

# Creamos algunos libros
libro1 = Libro("Python Crash Course", "Eric Matthes", 544, "9781593279288")
libro2 = Libro("Clean Code", "Robert C. Martin", 464, "9780132350884", False)

# Verificamos si están disponibles
print(f"{libro1.titulo} está {'disponible' if libro1.disponible else 'prestado'}")
print(f"{libro2.titulo} está {'disponible' if libro2.disponible else 'prestado'}")

Constructores alternativos con métodos de clase

A veces necesitamos diferentes formas de crear objetos. Python no admite sobrecarga de constructores como otros lenguajes, pero podemos usar métodos de clase como constructores alternativos:

class Fecha:
    def __init__(self, dia, mes, año):
        self.dia = dia
        self.mes = mes
        self.año = año
    
    @classmethod
    def desde_texto(cls, texto):
        """Constructor alternativo que crea una Fecha desde un texto con formato DD-MM-AAAA"""
        dia, mes, año = map(int, texto.split('-'))
        return cls(dia, mes, año)
    
    @classmethod
    def hoy(cls):
        """Constructor alternativo que crea una Fecha con la fecha actual"""
        import datetime
        fecha_actual = datetime.date.today()
        return cls(fecha_actual.day, fecha_actual.month, fecha_actual.year)

# Diferentes formas de crear objetos Fecha
fecha1 = Fecha(15, 3, 2023)  # Constructor normal
fecha2 = Fecha.desde_texto("25-12-2023")  # Constructor alternativo
fecha3 = Fecha.hoy()  # Constructor alternativo que usa la fecha actual

print(f"{fecha1.dia}/{fecha1.mes}/{fecha1.año}")  # Imprime: 15/3/2023
print(f"{fecha2.dia}/{fecha2.mes}/{fecha2.año}")  # Imprime: 25/12/2023

En este ejemplo, @classmethod es un decorador que indica que el método recibe la clase (cls) en lugar del objeto (self) como primer parámetro, permitiéndonos crear nuevas instancias de la clase.

Buenas prácticas al definir constructores

  • Mantén los constructores simples: Enfócate en inicializar atributos, evitando operaciones complejas.
  • Usa valores predeterminados para parámetros opcionales.
  • Valida los datos de entrada para evitar estados inválidos.
  • Documenta el constructor explicando qué parámetros espera y qué hace.
  • Considera constructores alternativos para diferentes formas de crear objetos.

El constructor es la puerta de entrada a tus objetos, y definirlo correctamente es fundamental para crear clases robustas y fáciles de usar. En las siguientes secciones, exploraremos con más detalle los atributos y métodos que dan vida a nuestras clases.

Atributos

Los atributos son las características o propiedades que definen a los objetos creados a partir de una clase. Si continuamos con nuestra analogía del plano y la casa, los atributos serían las características específicas de cada casa: su color, número de habitaciones, tamaño, etc. En Python, los atributos son simplemente variables asociadas a un objeto.

Existen diferentes tipos de atributos y formas de trabajar con ellos en Python. Vamos a explorarlos en detalle.

Tipos de atributos

En Python podemos distinguir principalmente dos tipos de atributos:

  • Atributos de instancia: Pertenecen a cada objeto individual (instancia) y pueden tener valores diferentes para cada objeto.
  • Atributos de clase: Pertenecen a la clase en sí y son compartidos por todas las instancias de esa clase.

Atributos de instancia

Los atributos de instancia son los más comunes. Se definen típicamente en el constructor (__init__) y son específicos para cada objeto:

class Estudiante:
    def __init__(self, nombre, edad):
        self.nombre = nombre  # Atributo de instancia
        self.edad = edad      # Atributo de instancia
        self.activo = True    # Atributo de instancia con valor predeterminado

# Creamos dos estudiantes
estudiante1 = Estudiante("María", 20)
estudiante2 = Estudiante("Carlos", 22)

# Cada estudiante tiene sus propios valores para los atributos
print(estudiante1.nombre)  # Imprime: María
print(estudiante2.nombre)  # Imprime: Carlos

Cada objeto Estudiante tiene sus propios valores para nombre, edad y activo. Modificar el atributo de un objeto no afecta a los demás.

Atributos de clase

Los atributos de clase se definen directamente en la clase, fuera de cualquier método, y son compartidos por todas las instancias:

class Estudiante:
    # Atributo de clase
    universidad = "Universidad Autónoma"
    
    def __init__(self, nombre, edad):
        self.nombre = nombre  # Atributo de instancia
        self.edad = edad      # Atributo de instancia

# Creamos dos estudiantes
estudiante1 = Estudiante("María", 20)
estudiante2 = Estudiante("Carlos", 22)

# Ambos comparten el mismo atributo de clase
print(estudiante1.universidad)  # Imprime: Universidad Autónoma
print(estudiante2.universidad)  # Imprime: Universidad Autónoma
print(Estudiante.universidad)   # También podemos acceder desde la clase

# Si modificamos el atributo de clase, afecta a todas las instancias
Estudiante.universidad = "Universidad Complutense"
print(estudiante1.universidad)  # Imprime: Universidad Complutense
print(estudiante2.universidad)  # Imprime: Universidad Complutense

Los atributos de clase son útiles para:

  • Definir constantes o valores predeterminados compartidos por todos los objetos
  • Mantener contadores o estadísticas sobre todas las instancias
  • Almacenar configuración común a todos los objetos

Acceso a atributos

Para acceder a los atributos de un objeto, utilizamos la notación de punto:

class Producto:
    impuesto = 0.21  # Atributo de clase
    
    def __init__(self, nombre, precio):
        self.nombre = nombre
        self.precio = precio

# Creamos un producto
laptop = Producto("Laptop", 1000)

# Accedemos a sus atributos
print(laptop.nombre)    # Atributo de instancia
print(laptop.precio)    # Atributo de instancia
print(laptop.impuesto)  # Atributo de clase (accedido desde la instancia)
print(Producto.impuesto)  # Atributo de clase (accedido desde la clase)

Modificación de atributos

Los atributos pueden modificarse después de crear el objeto:

class Coche:
    def __init__(self, marca, modelo, color):
        self.marca = marca
        self.modelo = modelo
        self.color = color
        self.kilometraje = 0

# Creamos un coche nuevo
mi_coche = Coche("Toyota", "Corolla", "Azul")
print(f"Color inicial: {mi_coche.color}")  # Imprime: Color inicial: Azul
print(f"Kilometraje inicial: {mi_coche.kilometraje}")  # Imprime: Kilometraje inicial: 0

# Modificamos sus atributos
mi_coche.color = "Rojo"  # Pintamos el coche
mi_coche.kilometraje = 1500  # Actualizamos el kilometraje

print(f"Nuevo color: {mi_coche.color}")  # Imprime: Nuevo color: Rojo
print(f"Kilometraje actual: {mi_coche.kilometraje}")  # Imprime: Kilometraje actual: 1500

Atributos dinámicos

Una característica poderosa de Python es que podemos añadir atributos a un objeto después de crearlo:

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

# Creamos una persona
juan = Persona("Juan")

# Añadimos atributos dinámicamente
juan.edad = 30
juan.profesion = "Ingeniero"

print(f"{juan.nombre} tiene {juan.edad} años y es {juan.profesion}")
# Imprime: Juan tiene 30 años y es Ingeniero

Aunque esta flexibilidad es útil, puede llevar a errores si no se maneja con cuidado. Es mejor definir todos los atributos en el constructor para mantener la consistencia.

Atributos privados y convenciones de nomenclatura

Python no tiene un mecanismo estricto de encapsulamiento como otros lenguajes, pero utiliza convenciones de nomenclatura para indicar la visibilidad prevista de los atributos:

  • Atributos públicos: Nombres normales como nombre o edad. Se pueden acceder desde cualquier parte.
  • Atributos "protegidos": Prefijados con un guion bajo como _saldo. Indica que no deberían accederse directamente desde fuera de la clase o sus subclases.
  • Atributos "privados": Prefijados con doble guion bajo como __pin. Python aplica "name mangling" (modificación del nombre) para dificultar el acceso accidental desde fuera.
class CuentaBancaria:
    tasa_interes = 0.03  # Atributo de clase público
    
    def __init__(self, titular, saldo_inicial, pin):
        self.titular = titular        # Atributo público
        self._saldo = saldo_inicial   # Atributo "protegido"
        self.__pin = pin              # Atributo "privado"
    
    def verificar_pin(self, pin_ingresado):
        return self.__pin == pin_ingresado

# Creamos una cuenta
cuenta = CuentaBancaria("Ana López", 1000, "1234")

# Acceso a atributos según su visibilidad
print(cuenta.titular)  # Funciona: atributo público
print(cuenta._saldo)   # Funciona, pero no deberíamos hacerlo por convención
# print(cuenta.__pin)  # Error: no existe tal atributo debido al name mangling

# El atributo privado existe, pero con un nombre modificado
print(cuenta._CuentaBancaria__pin)  # Funciona, pero es una mala práctica

Es importante entender que los guiones bajos son solo convenciones (excepto el doble guion que sí modifica el nombre). Depende de los desarrolladores respetar estas convenciones.

Propiedades: atributos con comportamiento controlado

Las propiedades nos permiten definir métodos que se comportan como atributos, añadiendo lógica al acceso y modificación:

class Temperatura:
    def __init__(self):
        self._celsius = 0
    
    # Definimos la propiedad celsius
    @property
    def celsius(self):
        """Obtiene la temperatura en grados Celsius"""
        return self._celsius
    
    @celsius.setter
    def celsius(self, valor):
        """Establece la temperatura en grados Celsius"""
        if valor < -273.15:
            raise ValueError("La temperatura no puede ser menor que el cero absoluto")
        self._celsius = valor
    
    # Definimos la propiedad fahrenheit
    @property
    def fahrenheit(self):
        """Obtiene la temperatura en grados Fahrenheit"""
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, valor):
        """Establece la temperatura en grados Fahrenheit"""
        self.celsius = (valor - 32) * 5/9

# Creamos un objeto temperatura
temp = Temperatura()

# Usamos las propiedades como si fueran atributos normales
temp.celsius = 25
print(f"{temp.celsius}°C = {temp.fahrenheit}°F")  # Imprime: 25°C = 77.0°F

temp.fahrenheit = 68
print(f"{temp.celsius}°C = {temp.fahrenheit}°F")  # Imprime: 20.0°C = 68.0°F

# La validación funciona
try:
    temp.celsius = -300  # Esto lanzará un error
except ValueError as e:
    print(f"Error: {e}")  # Imprime: Error: La temperatura no puede ser menor que el cero absoluto

Las propiedades son ideales para:

  • Validar datos antes de asignarlos
  • Calcular valores derivados
  • Mantener la consistencia entre atributos relacionados
  • Implementar atributos de solo lectura

Atributos calculados

A veces, algunos atributos pueden derivarse de otros. En lugar de almacenarlos, podemos calcularlos cuando se necesiten:

class Rectangulo:
    def __init__(self, ancho, alto):
        self.ancho = ancho
        self.alto = alto
    
    @property
    def area(self):
        """Área del rectángulo, calculada dinámicamente"""
        return self.ancho * self.alto
    
    @property
    def perimetro(self):
        """Perímetro del rectángulo, calculado dinámicamente"""
        return 2 * (self.ancho + self.alto)

# Creamos un rectángulo
rect = Rectangulo(5, 3)

# Accedemos a los atributos calculados
print(f"Área: {rect.area}")        # Imprime: Área: 15
print(f"Perímetro: {rect.perimetro}")  # Imprime: Perímetro: 16

# Si modificamos el rectángulo, los atributos calculados se actualizan automáticamente
rect.ancho = 7
print(f"Nueva área: {rect.area}")  # Imprime: Nueva área: 21

Atributos especiales

Python define algunos atributos especiales para todas las clases e instancias, que comienzan y terminan con doble guion bajo. Algunos útiles son:

class Ejemplo:
    """Clase de ejemplo para mostrar atributos especiales"""
    def __init__(self, valor):
        self.valor = valor

# Creamos una instancia
obj = Ejemplo(42)

# Atributos especiales
print(obj.__class__)  # Muestra la clase del objeto
print(Ejemplo.__name__)  # Nombre de la clase
print(Ejemplo.__doc__)  # Documentación de la clase
print(obj.__dict__)  # Diccionario que almacena los atributos de instancia

Gestión de atributos con funciones integradas

Python proporciona funciones útiles para trabajar con atributos:

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

p = Persona("Laura", 29)

# Verificar si un objeto tiene un atributo
print(hasattr(p, "nombre"))  # True
print(hasattr(p, "apellido"))  # False

# Obtener el valor de un atributo
print(getattr(p, "nombre"))  # Laura
print(getattr(p, "apellido", "No especificado"))  # No especificado (valor predeterminado)

# Establecer un atributo
setattr(p, "apellido", "García")
print(p.apellido)  # García

# Eliminar un atributo
delattr(p, "apellido")
# print(p.apellido)  # Esto daría error porque ya no existe

Estas funciones son especialmente útiles cuando necesitamos trabajar con atributos de forma dinámica, por ejemplo, cuando el nombre del atributo se determina en tiempo de ejecución.

Buenas prácticas con atributos

  • Inicializa todos los atributos en el constructor para evitar errores.
  • Usa atributos de clase para valores compartidos entre todas las instancias.
  • Respeta las convenciones de nomenclatura para indicar la visibilidad prevista.
  • Utiliza propiedades para validar datos y mantener la consistencia.
  • Documenta el propósito de cada atributo, especialmente si no es obvio.
  • Evita modificar atributos directamente desde fuera de la clase si tienen lógica asociada.

Los atributos son fundamentales en la programación orientada a objetos, ya que definen el estado de nuestros objetos. Combinados con los métodos, que veremos en la siguiente sección, nos permiten crear modelos completos y funcionales de entidades del mundo real.

Métodos

Los métodos son funciones definidas dentro de una clase que describen los comportamientos o acciones que pueden realizar los objetos creados a partir de esa clase. Si los atributos representan lo que un objeto "es" o "tiene", los métodos representan lo que un objeto "hace" o "puede hacer".

Siguiendo nuestra analogía del plano y la casa, si los atributos son las características de la casa (número de habitaciones, color, tamaño), los métodos serían las acciones que podemos realizar con ella (abrir puertas, encender luces, calentar agua).

Definición de métodos

Para definir un método en Python, simplemente creamos una función dentro de la clase. El primer parámetro de un método de instancia siempre es self, que hace referencia al objeto específico que está ejecutando el método:

class Coche:
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.velocidad = 0
        self.encendido = False
    
    # Método para encender el coche
    def encender(self):
        if not self.encendido:
            self.encendido = True
            return f"{self.marca} {self.modelo} encendido"
        return f"{self.marca} {self.modelo} ya estaba encendido"
    
    # Método para apagar el coche
    def apagar(self):
        if self.encendido:
            self.encendido = False
            self.velocidad = 0
            return f"{self.marca} {self.modelo} apagado"
        return f"{self.marca} {self.modelo} ya estaba apagado"

Llamada a métodos

Para llamar a un método, utilizamos la notación de punto después del nombre del objeto:

mi_coche = Coche("Toyota", "Corolla")
print(mi_coche.encender())  # Imprime: Toyota Corolla encendido
print(mi_coche.encender())  # Imprime: Toyota Corolla ya estaba encendido
print(mi_coche.apagar())    # Imprime: Toyota Corolla apagado

Métodos con parámetros

Los métodos pueden recibir parámetros adicionales además de self:

class Coche:
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.velocidad = 0
        self.encendido = False
        self.velocidad_maxima = 200
    
    def encender(self):
        if not self.encendido:
            self.encendido = True
            return f"{self.marca} {self.modelo} encendido"
        return f"{self.marca} {self.modelo} ya estaba encendido"
    
    # Método con parámetro
    def acelerar(self, incremento):
        if not self.encendido:
            return f"No se puede acelerar: {self.marca} {self.modelo} está apagado"
        
        nueva_velocidad = self.velocidad + incremento
        
        if nueva_velocidad > self.velocidad_maxima:
            self.velocidad = self.velocidad_maxima
            return f"Velocidad máxima alcanzada: {self.velocidad} km/h"
        
        self.velocidad = nueva_velocidad
        return f"Velocidad actual: {self.velocidad} km/h"
    
    # Otro método con parámetro
    def frenar(self, decremento):
        if self.velocidad == 0:
            return "El coche ya está detenido"
        
        nueva_velocidad = self.velocidad - decremento
        
        if nueva_velocidad < 0:
            self.velocidad = 0
            return "Coche detenido"
        
        self.velocidad = nueva_velocidad
        return f"Velocidad actual: {self.velocidad} km/h"

Ahora podemos usar estos métodos con parámetros:

mi_coche = Coche("Toyota", "Corolla")
print(mi_coche.encender())     # Toyota Corolla encendido
print(mi_coche.acelerar(50))   # Velocidad actual: 50 km/h
print(mi_coche.acelerar(30))   # Velocidad actual: 80 km/h
print(mi_coche.frenar(20))     # Velocidad actual: 60 km/h
print(mi_coche.frenar(60))     # Coche detenido

Métodos que interactúan con atributos

Los métodos pueden leer y modificar los atributos del objeto, permitiendo mantener la consistencia interna y aplicar reglas de negocio:

class CuentaBancaria:
    def __init__(self, titular, saldo_inicial=0):
        self.titular = titular
        self._saldo = saldo_inicial
    
    def consultar_saldo(self):
        return f"Saldo actual de {self.titular}: ${self._saldo}"
    
    def depositar(self, cantidad):
        if cantidad <= 0:
            return "La cantidad a depositar debe ser positiva"
        
        self._saldo += cantidad
        return f"Depósito de ${cantidad} realizado. Nuevo saldo: ${self._saldo}"
    
    def retirar(self, cantidad):
        if cantidad <= 0:
            return "La cantidad a retirar debe ser positiva"
        
        if cantidad > self._saldo:
            return "Fondos insuficientes"
        
        self._saldo -= cantidad
        return f"Retiro de ${cantidad} realizado. Nuevo saldo: ${self._saldo}"

Ejemplo de uso:

cuenta = CuentaBancaria("Ana López", 1000)
print(cuenta.consultar_saldo())  # Saldo actual de Ana López: $1000
print(cuenta.depositar(500))     # Depósito de $500 realizado. Nuevo saldo: $1500
print(cuenta.retirar(200))       # Retiro de $200 realizado. Nuevo saldo: $1300
print(cuenta.retirar(2000))      # Fondos insuficientes

Métodos que devuelven valores

Los métodos pueden devolver cualquier tipo de dato, incluyendo objetos complejos:

class Calculadora:
    def sumar(self, a, b):
        return a + b
    
    def restar(self, a, b):
        return a - b
    
    def multiplicar(self, a, b):
        return a * b
    
    def dividir(self, a, b):
        if b == 0:
            return "Error: División por cero"
        return a / b
    
    def calcular_estadisticas(self, numeros):
        if not numeros:
            return {
                "suma": 0,
                "promedio": 0,
                "minimo": None,
                "maximo": None
            }
        
        return {
            "suma": sum(numeros),
            "promedio": sum(numeros) / len(numeros),
            "minimo": min(numeros),
            "maximo": max(numeros)
        }

Ejemplo de uso:

calc = Calculadora()
print(calc.sumar(5, 3))        # 8
print(calc.dividir(10, 2))     # 5.0
print(calc.dividir(10, 0))     # Error: División por cero

# Método que devuelve un diccionario
estadisticas = calc.calcular_estadisticas([4, 7, 2, 9, 5])
print(f"Suma: {estadisticas['suma']}")         # Suma: 27
print(f"Promedio: {estadisticas['promedio']}") # Promedio: 5.4
print(f"Mínimo: {estadisticas['minimo']}")     # Mínimo: 2
print(f"Máximo: {estadisticas['maximo']}")     # Máximo: 9

Métodos que llaman a otros métodos

Un método puede llamar a otros métodos del mismo objeto usando self:

class Persona:
    def __init__(self, nombre, apellido, edad):
        self.nombre = nombre
        self.apellido = apellido
        self.edad = edad
    
    def nombre_completo(self):
        return f"{self.nombre} {self.apellido}"
    
    def es_mayor_de_edad(self):
        return self.edad >= 18
    
    def presentarse(self):
        estado = "mayor" if self.es_mayor_de_edad() else "menor"
        return f"Hola, soy {self.nombre_completo()} y soy {estado} de edad."

Ejemplo de uso:

persona = Persona("Juan", "Pérez", 25)
print(persona.nombre_completo())  # Juan Pérez
print(persona.es_mayor_de_edad()) # True
print(persona.presentarse())      # Hola, soy Juan Pérez y soy mayor de edad.

Métodos especiales (dunder methods)

Python define varios métodos especiales (también llamados "dunder methods" por los dobles guiones bajos) que permiten a nuestras clases interactuar con operadores y funciones integradas del lenguaje:

class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # Representación para desarrolladores (detallada)
    def __repr__(self):
        return f"Punto({self.x}, {self.y})"
    
    # Representación para usuarios (amigable)
    def __str__(self):
        return f"({self.x}, {self.y})"
    
    # Soporte para el operador +
    def __add__(self, otro):
        return Punto(self.x + otro.x, self.y + otro.y)
    
    # Soporte para el operador ==
    def __eq__(self, otro):
        if not isinstance(otro, Punto):
            return False
        return self.x == otro.x and self.y == otro.y
    
    # Soporte para len()
    def __len__(self):
        # Distancia Manhattan desde el origen
        return abs(self.x) + abs(self.y)

Ejemplo de uso:

p1 = Punto(3, 4)
p2 = Punto(1, 2)

# Uso de __str__ (implícito)
print(p1)  # (3, 4)

# Uso de __repr__ (explícito)
print(repr(p1))  # Punto(3, 4)

# Uso de __add__
p3 = p1 + p2
print(p3)  # (4, 6)

# Uso de __eq__
print(p1 == p2)  # False
print(p1 == Punto(3, 4))  # True

# Uso de __len__
print(len(p1))  # 7 (3 + 4)

Algunos métodos especiales comunes:

  • __init__: Constructor
  • __str__: Representación de cadena para usuarios
  • __repr__: Representación de cadena para desarrolladores
  • __len__: Soporte para la función len()
  • __add__, __sub__, etc.: Soporte para operadores aritméticos
  • __eq__, __lt__, etc.: Soporte para operadores de comparación
  • __getitem__, __setitem__: Soporte para acceso con corchetes obj[key]

Métodos estáticos y de clase

Además de los métodos de instancia (que reciben self), Python permite definir:

Métodos estáticos

Los métodos estáticos no reciben automáticamente ni la instancia ni la clase. Son funciones normales que están lógicamente agrupadas en la clase:

class MathUtils:
    @staticmethod
    def es_primo(n):
        """Verifica si un número es primo"""
        if n <= 1:
            return False
        if n <= 3:
            return True
        if n % 2 == 0 or n % 3 == 0:
            return False
        i = 5
        while i * i <= n:
            if n % i == 0 or n % (i + 2) == 0:
                return False
            i += 6
        return True
    
    @staticmethod
    def factorial(n):
        """Calcula el factorial de n"""
        if n < 0:
            raise ValueError("El factorial no está definido para números negativos")
        if n == 0 or n == 1:
            return 1
        return n * MathUtils.factorial(n - 1)

Uso de métodos estáticos:

# No necesitamos crear una instancia
print(MathUtils.es_primo(17))    # True
print(MathUtils.es_primo(20))    # False
print(MathUtils.factorial(5))    # 120

Métodos de clase

Los métodos de clase reciben automáticamente la clase como primer parámetro (convencionalmente llamado cls):

class Empleado:
    # Atributo de clase
    num_empleados = 0
    
    def __init__(self, nombre, salario):
        self.nombre = nombre
        self.salario = salario
        Empleado.num_empleados += 1
    
    @classmethod
    def desde_salario_anual(cls, nombre, salario_anual):
        """Constructor alternativo que recibe salario anual en lugar de mensual"""
        salario_mensual = salario_anual / 12
        return cls(nombre, salario_mensual)
    
    @classmethod
    def obtener_num_empleados(cls):
        """Devuelve el número total de empleados creados"""
        return cls.num_empleados

Uso de métodos de clase:

# Creación normal
emp1 = Empleado("Ana", 3000)

# Usando el método de clase como constructor alternativo
emp2 = Empleado.desde_salario_anual("Carlos", 48000)  # Salario mensual: 4000

print(f"Empleados creados: {Empleado.obtener_num_empleados()}")  # Empleados creados: 2

Ejemplo práctico: Biblioteca

Veamos un ejemplo más completo que integra varios conceptos sobre métodos:

class Libro:
    def __init__(self, titulo, autor, paginas):
        self.titulo = titulo
        self.autor = autor
        self.paginas = paginas
        self.pagina_actual = 0
        self.abierto = False
    
    def abrir(self):
        if self.abierto:
            return f"{self.titulo} ya está abierto"
        self.abierto = True
        return f"{self.titulo} ha sido abierto"
    
    def cerrar(self):
        if not self.abierto:
            return f"{self.titulo} ya está cerrado"
        self.abierto = False
        return f"{self.titulo} ha sido cerrado"
    
    def leer(self, num_paginas):
        if not self.abierto:
            return f"No puedes leer: {self.titulo} está cerrado"
        
        if self.pagina_actual >= self.paginas:
            return f"Ya has terminado de leer {self.titulo}"
        
        paginas_restantes = self.paginas - self.pagina_actual
        paginas_a_leer = min(num_paginas, paginas_restantes)
        
        self.pagina_actual += paginas_a_leer
        
        if self.pagina_actual >= self.paginas:
            return f"Has leído {paginas_a_leer} páginas y has terminado {self.titulo}"
        
        return f"Has leído {paginas_a_leer} páginas. Estás en la página {self.pagina_actual} de {self.paginas}"
    
    def reiniciar_lectura(self):
        self.pagina_actual = 0
        return f"Has reiniciado la lectura de {self.titulo}"
    
    def __str__(self):
        estado = "abierto" if self.abierto else "cerrado"
        progreso = f"{self.pagina_actual}/{self.paginas} páginas"
        return f"{self.titulo} por {self.autor} - {progreso} - {estado}"

Ejemplo de uso:

libro = Libro("El Quijote", "Miguel de Cervantes", 863)

print(libro.leer(50))      # No puedes leer: El Quijote está cerrado
print(libro.abrir())       # El Quijote ha sido abierto
print(libro.leer(50))      # Has leído 50 páginas. Estás en la página 50 de 863
print(libro.leer(100))     # Has leído 100 páginas. Estás en la página 150 de 863
print(libro.cerrar())      # El Quijote ha sido cerrado
print(libro.abrir())       # El Quijote ha sido abierto
print(libro.leer(713))     # Has leído 713 páginas y has terminado El Quijote
print(libro.reiniciar_lectura())  # Has reiniciado la lectura de El Quijote
print(libro)               # El Quijote por Miguel de Cervantes - 0/863 páginas - abierto

Buenas prácticas para métodos

  • Nombres descriptivos: Usa verbos que describan claramente la acción que realiza el método.
  • Responsabilidad única: Cada método debe hacer una sola cosa y hacerla bien.
  • Tamaño adecuado: Mantén los métodos relativamente cortos y enfocados.
  • Validación de parámetros: Verifica que los parámetros recibidos sean válidos.
  • Documentación: Añade docstrings que expliquen qué hace el método, qué parámetros recibe y qué devuelve.
  • Consistencia: Mantén un estilo consistente en todos los métodos de la clase.
  • Encapsulamiento: Usa métodos para acceder y modificar atributos cuando sea necesario aplicar lógica adicional.

Los métodos son la parte activa de nuestras clases, definiendo el comportamiento de los objetos. Combinados con los atributos, nos permiten crear modelos completos y funcionales que representan entidades del mundo real en nuestro código.

Aprende Python online

Otras lecciones de Python

Accede a todas las lecciones de Python y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Python

Python

Introducción

Instalación Y Creación De Proyecto

Python

Introducción

Tema 2: Tipos De Datos, Variables Y Operadores

Python

Introducción

Instalación De Python

Python

Introducción

Tipos De Datos

Python

Sintaxis

Variables

Python

Sintaxis

Operadores

Python

Sintaxis

Estructuras De Control

Python

Sintaxis

Funciones

Python

Sintaxis

Estructuras Control Iterativo

Python

Sintaxis

Estructuras Control Condicional

Python

Sintaxis

Testing Con Pytest

Python

Sintaxis

Listas

Python

Estructuras De Datos

Tuplas

Python

Estructuras De Datos

Diccionarios

Python

Estructuras De Datos

Conjuntos

Python

Estructuras De Datos

Comprehensions

Python

Estructuras De Datos

Clases Y Objetos

Python

Programación Orientada A Objetos

Excepciones

Python

Programación Orientada A Objetos

Encapsulación

Python

Programación Orientada A Objetos

Herencia

Python

Programación Orientada A Objetos

Polimorfismo

Python

Programación Orientada A Objetos

Mixins Y Herencia Múltiple

Python

Programación Orientada A Objetos

Métodos Especiales (Dunder Methods)

Python

Programación Orientada A Objetos

Composición De Clases

Python

Programación Orientada A Objetos

Funciones Lambda

Python

Programación Funcional

Aplicación Parcial

Python

Programación Funcional

Entrada Y Salida, Manejo De Archivos

Python

Programación Funcional

Decoradores

Python

Programación Funcional

Generadores

Python

Programación Funcional

Paradigma Funcional

Python

Programación Funcional

Composición De Funciones

Python

Programación Funcional

Funciones Orden Superior Map Y Filter

Python

Programación Funcional

Funciones Auxiliares

Python

Programación Funcional

Reducción Y Acumulación

Python

Programación Funcional

Archivos Comprimidos

Python

Entrada Y Salida Io

Entrada Y Salida Avanzada

Python

Entrada Y Salida Io

Archivos Temporales

Python

Entrada Y Salida Io

Contexto With

Python

Entrada Y Salida Io

Módulo Csv

Python

Biblioteca Estándar

Módulo Json

Python

Biblioteca Estándar

Módulo Datetime

Python

Biblioteca Estándar

Módulo Math

Python

Biblioteca Estándar

Módulo Os

Python

Biblioteca Estándar

Módulo Re

Python

Biblioteca Estándar

Módulo Random

Python

Biblioteca Estándar

Módulo Time

Python

Biblioteca Estándar

Módulo Collections

Python

Biblioteca Estándar

Módulo Sys

Python

Biblioteca Estándar

Módulo Statistics

Python

Biblioteca Estándar

Módulo Pickle

Python

Biblioteca Estándar

Módulo Pathlib

Python

Biblioteca Estándar

Importar Módulos Y Paquetes

Python

Paquetes Y Módulos

Crear Módulos Y Paquetes

Python

Paquetes Y Módulos

Entornos Virtuales (Virtualenv, Venv)

Python

Entorno Y Dependencias

Gestión De Dependencias (Pip, Requirements.txt)

Python

Entorno Y Dependencias

Python-dotenv Y Variables De Entorno

Python

Entorno Y Dependencias

Acceso A Datos Con Mysql, Pymongo Y Pandas

Python

Acceso A Bases De Datos

Acceso A Mongodb Con Pymongo

Python

Acceso A Bases De Datos

Acceso A Mysql Con Mysql Connector

Python

Acceso A Bases De Datos

Novedades Python 3.13

Python

Características Modernas

Operador Walrus

Python

Características Modernas

Pattern Matching

Python

Características Modernas

Instalación Beautiful Soup

Python

Web Scraping

Sintaxis General De Beautiful Soup

Python

Web Scraping

Tipos De Selectores

Python

Web Scraping

Web Scraping De Html

Python

Web Scraping

Web Scraping Para Ciencia De Datos

Python

Web Scraping

Autenticación Y Acceso A Recursos Protegidos

Python

Web Scraping

Combinación De Selenium Con Beautiful Soup

Python

Web Scraping

Accede GRATIS a Python y certifícate

Ejercicios de programación de Python

Evalúa tus conocimientos de esta lección Clases y objetos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Módulo math

Python
Puzzle

Reto herencia

Python
Código

Excepciones

Python
Test

Introducción a Python

Python
Test

Reto variables

Python
Código

Funciones Python

Python
Puzzle

Reto funciones

Python
Código

Módulo datetime

Python
Test

Reto acumulación

Python
Código

Reto estructuras condicionales

Python
Código

Polimorfismo

Python
Test

Módulo os

Python
Test

Reto métodos dunder

Python
Código

Diccionarios

Python
Puzzle

Reto clases y objetos

Python
Código

Reto operadores

Python
Código

Operadores

Python
Test

Estructuras de control

Python
Puzzle

Funciones lambda

Python
Test

Reto diccionarios

Python
Código

Reto función lambda

Python
Código

Encapsulación

Python
Puzzle

Reto coleciones

Python
Proyecto

Reto funciones auxiliares

Python
Código

Crear módulos y paquetes

Python
Puzzle

Módulo datetime

Python
Puzzle

Excepciones

Python
Puzzle

Operadores

Python
Puzzle

Diccionarios

Python
Test

Reto map, filter

Python
Código

Reto tuplas

Python
Código

Proyecto gestor de tareas CRUD

Python
Proyecto

Tuplas

Python
Puzzle

Variables

Python
Puzzle

Tipos de datos

Python
Puzzle

Conjuntos

Python
Test

Reto mixins

Python
Código

Módulo csv

Python
Test

Módulo json

Python
Test

Herencia

Python
Test

Análisis de datos de ventas con Pandas

Python
Proyecto

Reto fechas y tiempo

Python
Proyecto

Reto estructuras de iteración

Python
Código

Funciones

Python
Test

Reto comprehensions

Python
Código

Variables

Python
Test

Reto serialización

Python
Proyecto

Módulo csv

Python
Puzzle

Reto polimorfismo

Python
Código

Polimorfismo

Python
Puzzle

Clases y objetos

Python
Código

Reto encapsulación

Python
Código

Estructuras de control

Python
Test

Importar módulos y paquetes

Python
Test

Módulo math

Python
Test

Funciones lambda

Python
Puzzle

Reto excepciones

Python
Código

Listas

Python
Puzzle

Reto archivos

Python
Proyecto

Encapsulación

Python
Test

Reto conjuntos

Python
Código

Clases y objetos

Python
Test

Instalación de Python y creación de proyecto

Python
Test

Reto listas

Python
Código

Tipos de datos

Python
Test

Crear módulos y paquetes

Python
Test

Tuplas

Python
Test

Herencia

Python
Puzzle

Reto acceso a sistema

Python
Proyecto

Proyecto sintaxis calculadora

Python
Proyecto

Importar módulos y paquetes

Python
Puzzle

Clases y objetos

Python
Puzzle

Módulo os

Python
Puzzle

Listas

Python
Test

Conjuntos

Python
Puzzle

Reto tipos de datos

Python
Código

Reto matemáticas

Python
Proyecto

Módulo json

Python
Puzzle

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el concepto de clases y objetos en programación orientada a objetos.
  • Aprender a definir clases y crear objetos en Python.
  • Entender el uso del método constructor init para inicializar objetos.
  • Diferenciar entre atributos de instancia y de clase, y gestionar su acceso y modificación.
  • Definir y utilizar métodos, incluyendo métodos especiales, estáticos y de clase, para modelar comportamientos de objetos.