Saltar al contenido
🐍 Curso de Python
/

Módulo 1 · Lección 57 de 59

Métodos especiales

Los métodos con doble guion bajo que hacen que tus objetos funcionen con print, len, ==, in, sorted y el operador más.

⏱ 14 min de lectura

Tus clases funcionan, pero se notan de segunda categoría al lado de las de Python. Una lista se imprime y se entiende, se le puede pedir len(), se compara con ==, se ordena con sorted() y se pregunta por sus elementos con in. Un objeto tuyo no hace nada de eso.

La diferencia no es que las listas sean mágicas: es que definen unos métodos concretos que Python busca cuando le pides esas operaciones. Se llaman métodos especiales, y tú puedes definirlos igual.

Qué son y cómo funcionan#

Ya conoces uno, __init__. El mecanismo es siempre el mismo: tú los defines y Python los llama por ti cuando aparece la operación correspondiente.

  • Escribes len(pedido) y Python ejecuta pedido.__len__().
  • Escribes pedido_a + pedido_b y Python ejecuta pedido_a.__add__(pedido_b).
  • Escribes print(producto) y Python ejecuta producto.__str__().

No se llaman nunca a mano. Escribir pedido.__len__() funciona, pero es como llamar al timbre metiendo la mano por la ventana.

__str__ y __repr__#

Sin ninguno de los dos, un objeto tuyo se imprime así:

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


teclado = Producto("Teclado mecánico", 79.90)

print(teclado)
print([teclado])
print(f"He comprado un {teclado}")
Salida en pantalla
<__main__.Producto object at 0x100d11fd0>
[<__main__.Producto object at 0x100d11fd0>]
He comprado un <__main__.Producto object at 0x100d11fd0>

Inútil en los tres casos. Con los dos métodos definidos:

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

    def __str__(self):
                return f"{self.nombre} ({self.precio:.2f} €)"

    def __repr__(self):
                return f"Producto({self.nombre!r}, {self.precio!r})"


teclado = Producto("Teclado mecánico", 79.90)
raton = Producto("Ratón inalámbrico", 24.50)

print(teclado)
print(str(teclado))
print(repr(teclado))
print([teclado, raton])
Salida en pantalla
Teclado mecánico (79.90 €)
Teclado mecánico (79.90 €)
Producto('Teclado mecánico', 79.9)
[Producto('Teclado mecánico', 79.9), Producto('Ratón inalámbrico', 24.5)]

La diferencia entre los dos#

Son dos públicos distintos:

__str____repr__
Para quiénPara quien usa el programaPara quien lo programa
Qué buscaQue se lea bienQue sea exacto y sin ambigüedad
Quién lo llamaprint(), str(), las f-stringsrepr(), la consola interactiva, las colecciones
Objetivo idealUna frase legibleUn texto que, copiado y pegado, recree el objeto

Fíjate en la cuarta línea de la salida: al imprimir una lista de productos, Python no usa __str__ de cada uno, sino __repr__. Lo hace a propósito, porque dentro de una colección interesa distinguir los elementos sin ambigüedad, no que quede bonito.

El !r dentro de la f-string de __repr__ aplica repr() al valor, que en una cadena significa ponerla entre comillas. Por eso sale Producto('Teclado mecánico', 79.9) y no Producto(Teclado mecánico, 79.9): el primero se puede copiar y pegar en el código y funciona.

__eq__: que funcione ==#

Por defecto, dos objetos solo son iguales si son el mismo objeto. Dos productos con los mismos datos se consideran distintos:

python
uno = Producto("TEC-001", "Teclado mecánico", 79.90)
otro = Producto("TEC-001", "Teclado mecánico", 79.90)

print(uno == otro)
print(uno == uno)
Salida en pantalla
False
True

Eso casi nunca es lo que quieres. Definiendo __eq__ decides tú qué significa que dos productos sean el mismo, que aquí será tener la misma referencia de catálogo aunque el precio haya cambiado:

python
class Producto:
        def __init__(self, referencia, nombre, precio):
                self.referencia = referencia
        self.nombre = nombre
        self.precio = precio

    def __repr__(self):
                return f"Producto({self.referencia!r}, {self.nombre!r}, {self.precio!r})"

    def __eq__(self, otro):
                if not isinstance(otro, Producto):
                        return NotImplemented
        return self.referencia == otro.referencia


uno = Producto("TEC-001", "Teclado mecánico", 79.90)
otro = Producto("TEC-001", "Teclado mecánico", 74.90)
distinto = Producto("RAT-007", "Ratón inalámbrico", 24.50)

print(uno == otro)
print(uno == distinto)
print(uno != distinto)
print(uno == "TEC-001")
print(uno in [distinto, otro])
Salida en pantalla
True
False
True
False
True

Tres cosas que salen gratis al definir __eq__:

  • != funciona solo, como el contrario de ==. No hay que definir __ne__.
  • El in sobre una lista empieza a funcionar como esperas, porque por dentro compara con ==.
  • Métodos de lista como .index() y .count() también.

El return NotImplemented cuando el otro objeto no es un Producto merece explicación: no es un error ni un False. Es la forma de decirle a Python "yo no sé comparar con esto, prueba al revés por si el otro sabe". Si ninguno sabe, Python decide que no son iguales, que es lo que se ve en uno == "TEC-001".

__lt__: poder ordenar#

sorted(), min() y max() necesitan saber cuándo un objeto va antes que otro. Sin __lt__ (de less than) no pueden:

python
catalogo = [Producto("Teclado", 79.9), Producto("Ratón", 24.5)]
print(sorted(catalogo))
Salida en pantalla
TypeError: '<' not supported between instances of 'Producto' and 'Producto'

Definiendo un único método, __lt__, se arregla todo:

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

    def __repr__(self):
                return f"{self.nombre} ({self.precio:.2f} €)"

    def __lt__(self, otro):
                return self.precio < otro.precio


catalogo = [
    Producto("Teclado mecánico", 79.90),
    Producto("Ratón inalámbrico", 24.50),
    Producto("Monitor 27 pulgadas", 219.00),
]

print(sorted(catalogo))
print(min(catalogo))
print(max(catalogo))
print(catalogo[0] < catalogo[1])
Salida en pantalla
[Ratón inalámbrico (24.50 €), Teclado mecánico (79.90 €), Monitor 27 pulgadas (219.00 €)]
Ratón inalámbrico (24.50 €)
Monitor 27 pulgadas (219.00 €)
False

Con __lt__ basta porque sorted, min y max solo preguntan "¿este va antes que este otro?". Los operadores >, <= y >= no vienen incluidos; si los necesitas, están __gt__, __le__ y __ge__.

__len__, __contains__ y __add__#

Los tres van bien juntos en una clase que contiene cosas. Un pedido, por ejemplo:

python
class Pedido:
        def __init__(self, cliente, productos=None):
                self.cliente = cliente
        self.productos = productos if productos is not None else []

    def __str__(self):
                return f"Pedido de {self.cliente} con {len(self)} artículos"

    def __len__(self):
                return len(self.productos)

    def __contains__(self, producto):
                return producto in self.productos

    def __add__(self, otro):
                if not isinstance(otro, Pedido):
                        return NotImplemented
        return Pedido(self.cliente, self.productos + otro.productos)


teclado = Producto("Teclado mecánico", 79.90)
raton = Producto("Ratón inalámbrico", 24.50)
monitor = Producto("Monitor 27 pulgadas", 219.00)

primero = Pedido("Lucía Ferrer", [teclado, raton])
segundo = Pedido("Lucía Ferrer", [monitor])

print(len(primero))
print(teclado in primero)
print(monitor in primero)

completo = primero + segundo
print(completo)
print(len(completo))

vacio = Pedido("Iván Soto")
if not vacio:
        print("El pedido está vacío")
Salida en pantalla
2
True
False
Pedido de Lucía Ferrer con 3 artículos
3
El pedido está vacío

Tres detalles:

  • Dentro de __str__ se escribe len(self), no len(self.productos). Funciona porque la propia clase ya tiene __len__, y así el día que cambie la forma de contar solo hay un sitio que tocar.
  • __add__ devuelve un pedido nuevo en vez de modificar el de la izquierda. Es lo que hace + con las listas y las cadenas, y romper esa expectativa confunde muchísimo. Para modificar en el sitio existe +=, con su propio método __iadd__.
  • La última comprobación, if not vacio, funciona sin definir nada más: cuando no hay __bool__, Python usa __len__ y considera falso todo objeto de longitud cero. Es la misma regla que hace falsa una lista vacía.

Tabla de referencia#

MétodoLo disparaEjemplo de uso
__init__Crear el objetoProducto("Teclado", 79.9)
__str__print(), str(), f-stringsprint(producto)
__repr__repr(), la consola, las colecciones[producto]
__len__len()len(pedido)
__bool__if, not, bool()if pedido:
__eq__== y !=uno == otro
__lt__<, sorted(), min(), max()sorted(catalogo)
__gt__>uno > otro
__contains__inproducto in pedido
__add__+primero + segundo
__sub__-saldo - gasto
__mul__*producto * 3
__getitem__Los corchetespedido[0]
__iter__for, desempaquetadofor p in pedido:
__call__Llamar al objeto con paréntesisobjeto()

Hay bastantes más, pero con estos se cubre casi todo lo que escribirás.