Ir al contenido principal

Métodos especiales: __str__, __len__, etc.

Introducción

En la programación orientada a objetos de Python, los métodos especiales (también conocidos como "métodos mágicos" o "dunder methods" por el doble guion bajo que los rodea) permiten a nuestras clases interactuar con las operaciones y funcionalidades incorporadas del lenguaje. Estos métodos son los responsables de que podamos usar operadores como +, -, funciones integradas como len() o str(), e incluso sintaxis como [] para acceder a elementos, todo ello con nuestros propios objetos. En este artículo, exploraremos los métodos especiales más comunes y cómo pueden hacer que nuestras clases se comporten de manera más natural e integrada con el resto del código Python.

Funcionamiento de los métodos especiales

Los métodos especiales son reconocidos por Python porque siguen un patrón de nomenclatura específico: comienzan y terminan con doble guion bajo (__nombre__). Cuando usamos ciertas operaciones o funciones con nuestros objetos, Python busca e invoca automáticamente estos métodos especiales si están definidos en la clase.

El método __str__

El método __str__ determina la representación en cadena de texto de un objeto cuando se usa la función str() o cuando se imprime el objeto con print(). Proporciona una versión "amigable para el usuario" del objeto.

class Persona:
    def __init__(self, nombre, edad):
        self.nombre = nombre
        self.edad = edad
    
    def __str__(self):
        return f"Persona: {self.nombre}, {self.edad} años"

# Creamos una instancia
juan = Persona("Juan", 30)

# Al imprimir el objeto, se invoca automáticamente __str__
print(juan)  # Muestra: Persona: Juan, 30 años

# Lo mismo ocurre al convertir el objeto a cadena
texto = str(juan)
print(texto)  # Muestra: Persona: Juan, 30 años

El método __repr__

Similar a __str__, el método __repr__ proporciona una representación en texto del objeto, pero está pensado para ser más técnica y precisa, idealmente mostrando cómo recrear el objeto.

class Persona:
    def __init__(self, nombre, edad):
        self.nombre = nombre
        self.edad = edad
    
    def __str__(self):
        return f"Persona: {self.nombre}, {self.edad} años"
    
    def __repr__(self):
        return f"Persona(nombre='{self.nombre}', edad={self.edad})"

juan = Persona("Juan", 30)

# __str__ se usa en print
print(juan)  # Persona: Juan, 30 años

# __repr__ se usa cuando mostramos el objeto directamente en el intérprete
# o cuando usamos la función repr()
print(repr(juan))  # Persona(nombre='Juan', edad=30)

En el intérprete interactivo, cuando evaluamos una expresión que devuelve un objeto, se llama a __repr__ para mostrar el resultado.

El método __len__

El método __len__ permite que nuestros objetos sean compatibles con la función len(), que normalmente se usa para obtener la longitud o tamaño de una colección.

class Equipo:
    def __init__(self):
        self.miembros = []
    
    def agregar_miembro(self, miembro):
        self.miembros.append(miembro)
    
    def __len__(self):
        return len(self.miembros)

# Creamos un equipo y añadimos miembros
equipo_futbol = Equipo()
equipo_futbol.agregar_miembro("Carlos")
equipo_futbol.agregar_miembro("Ana")
equipo_futbol.agregar_miembro("Pedro")

# Ahora podemos usar la función len() con nuestro objeto
print(len(equipo_futbol))  # Muestra: 3

Métodos para operadores aritméticos

Podemos definir cómo se comportan los operadores aritméticos cuando se aplican a nuestros objetos.

El método __add__ (operador +)

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __add__(self, otro):
        # Permite sumar dos vectores usando el operador +
        return Vector(self.x + otro.x, self.y + otro.y)

# Creamos dos vectores
v1 = Vector(3, 4)
v2 = Vector(2, 1)

# Al usar el operador +, Python invoca el método __add__
v3 = v1 + v2

print(v3)  # Muestra: Vector(5, 5)

El método __sub__ (operador -)

Similar a __add__, pero para la resta:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __add__(self, otro):
        return Vector(self.x + otro.x, self.y + otro.y)
    
    def __sub__(self, otro):
        # Permite restar dos vectores usando el operador -
        return Vector(self.x - otro.x, self.y - otro.y)

v1 = Vector(5, 8)
v2 = Vector(2, 3)

v_resta = v1 - v2
print(v_resta)  # Muestra: Vector(3, 5)

Métodos para comparaciones

También podemos definir cómo se comparan nuestros objetos mediante métodos especiales.

El método __eq__ (operador ==)

class Libro:
    def __init__(self, titulo, autor, isbn):
        self.titulo = titulo
        self.autor = autor
        self.isbn = isbn
    
    def __eq__(self, otro):
        # Dos libros son iguales si tienen el mismo ISBN
        return self.isbn == otro.isbn

# Creamos dos libros con mismo ISBN pero diferentes títulos
libro1 = Libro("Don Quijote v1", "Miguel de Cervantes", "9788420412146")
libro2 = Libro("Don Quijote v2", "Miguel de Cervantes", "9788420412146")

# Al usar ==, Python invoca el método __eq__
print(libro1 == libro2)  # Muestra: True, porque tienen el mismo ISBN

Otros métodos de comparación incluyen:

  • __lt__: Para el operador menor que (<)
  • __gt__: Para el operador mayor que (>)
  • __le__: Para el operador menor o igual que (<=)
  • __ge__: Para el operador mayor o igual que (>=)
  • __ne__: Para el operador distinto (!=)

Métodos para el acceso a elementos

Los siguientes métodos permiten que nuestros objetos se comporten como colecciones accesibles mediante índices o claves.

El método __getitem__ (operador [])

class Inventario:
    def __init__(self):
        self.items = {}
    
    def agregar_item(self, codigo, nombre):
        self.items[codigo] = nombre
    
    def __getitem__(self, codigo):
        # Permite acceder a items con la sintaxis inventario[codigo]
        return self.items[codigo]

# Creamos un inventario y añadimos items
inventario = Inventario()
inventario.agregar_item("A001", "Teclado")
inventario.agregar_item("A002", "Monitor")

# Podemos acceder a los items usando la sintaxis de corchetes
print(inventario["A001"])  # Muestra: Teclado

El método __setitem__ (asignación con [])

class Inventario:
    def __init__(self):
        self.items = {}
    
    def __getitem__(self, codigo):
        return self.items[codigo]
    
    def __setitem__(self, codigo, nombre):
        # Permite asignar valores con la sintaxis inventario[codigo] = nombre
        self.items[codigo] = nombre

# Creamos un inventario
inventario = Inventario()

# Podemos añadir y modificar items usando la sintaxis de corchetes
inventario["A001"] = "Teclado"
inventario["A002"] = "Monitor"

print(inventario["A001"])  # Muestra: Teclado

Otros métodos especiales comunes

El método __contains__ (operador in)

class Biblioteca:
    def __init__(self):
        self.libros = []
    
    def agregar_libro(self, libro):
        self.libros.append(libro)
    
    def __contains__(self, titulo):
        # Permite usar la sintaxis "titulo in biblioteca"
        return titulo in [libro.titulo for libro in self.libros]

# Creamos una biblioteca y añadimos libros
biblioteca = Biblioteca()
biblioteca.agregar_libro(Libro("El Quijote", "Cervantes", "123"))
biblioteca.agregar_libro(Libro("Cien años de soledad", "García Márquez", "456"))

# Podemos usar el operador "in" para verificar si un título está en la biblioteca
print("El Quijote" in biblioteca)  # Muestra: True
print("Hamlet" in biblioteca)      # Muestra: False

El método __call__ (hacer al objeto "invocable")

class Calculadora:
    def __call__(self, a, b, operacion="+"):
        # Permite usar la instancia como una función
        if operacion == "+":
            return a + b
        elif operacion == "-":
            return a - b
        elif operacion == "*":
            return a * b
        elif operacion == "/":
            return a / b
        else:
            raise ValueError("Operación no soportada")

# Creamos una calculadora
calc = Calculadora()

# Podemos "invocar" la instancia como si fuera una función
print(calc(5, 3))          # Muestra: 8 (suma por defecto)
print(calc(5, 3, "+"))     # Muestra: 8
print(calc(5, 3, "-"))     # Muestra: 2
print(calc(5, 3, "*"))     # Muestra: 15

Ejemplo práctico: Una clase Poligono completa

Veamos un ejemplo más completo que utiliza varios métodos especiales en una clase:

class Poligono:
    def __init__(self, *puntos):
        # Puntos es una secuencia de tuplas (x, y)
        self.puntos = list(puntos)
    
    def __str__(self):
        return f"Polígono con {len(self.puntos)} vértices"
    
    def __repr__(self):
        puntos_str = ", ".join([f"({x}, {y})" for x, y in self.puntos])
        return f"Poligono({puntos_str})"
    
    def __len__(self):
        # Número de vértices
        return len(self.puntos)
    
    def __getitem__(self, indice):
        # Acceso a los puntos por índice
        return self.puntos[indice]
    
    def __setitem__(self, indice, punto):
        # Modificar un punto por índice
        self.puntos[indice] = punto
    
    def __contains__(self, punto):
        # Verificar si un punto está en el polígono
        return punto in self.puntos
    
    def __add__(self, otro):
        # Unir dos polígonos combinando sus puntos
        return Poligono(*(self.puntos + otro.puntos))
    
    def __eq__(self, otro):
        # Dos polígonos son iguales si tienen los mismos puntos
        return set(self.puntos) == set(otro.puntos)

# Usamos nuestra clase Poligono
triangulo = Poligono((0, 0), (1, 0), (0, 1))
cuadrado = Poligono((0, 0), (1, 0), (1, 1), (0, 1))

print(triangulo)                  # Polígono con 3 vértices
print(len(triangulo))             # 3
print(triangulo[1])               # (1, 0)
triangulo[1] = (2, 0)
print((2, 0) in triangulo)        # True
print(triangulo + cuadrado)       # Polígono con 7 vértices

Resumen

Los métodos especiales (mágicos) son una de las características más potentes y elegantes de Python, ya que permiten que nuestros objetos personalizados se integren a la perfección con la sintaxis y las operaciones nativas del lenguaje. Al implementar estos métodos en nuestras clases, podemos lograr que se comporten de manera intuitiva y consistente con el resto del ecosistema Python. Ya sea para hacer que nuestros objetos sean imprimibles con str(), medibles con len(), comparables con == o accesibles con [], los métodos especiales nos brindan control total sobre cómo interactúan nuestros objetos con el código Python. En los próximos temas, exploraremos más aspectos avanzados de la programación orientada a objetos y otras características del lenguaje que complementan lo aprendido hasta ahora.