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.