POO: Métodos especiales en Python

Publicado por Andrea Navarro el

En este artículo veremos qué son los métodos especiales en Python utilizados en la programación orientada a objetos. Explicaremos los tipos existentes, sus usos y la forma en que estos son invocados.

Métodos especiales en Python

Los métodos especiales en Python, o dunder methods son funciones definidas dentro de una clase que permite modificar o personalizar el comportamiento básico de los objetos. Pueden ser identificadas porque su nombre comienza y finaliza con dos guiones bajos _.

A diferencia de otros métodos los métodos especiales, no se llaman directamente, sino que son invocados de manera automática al utilizar ciertas sintaxis u operaciones.

La función principal de los métodos especiales es permitir integrar los objetos con el resto del lenguaje, dándoles capacidades como ser inicializados por parámetros, tener representaciones legibles o técnicas, compararse con otros objetos o convertirse en tipos básicos.

Métodos de creación e inicialización de objetos: __init__, __new__ y __del__

Posiblemente el método especial más usado es __init__(). Este se ejecuta cuando se crea una instancia de una clase. Este método actúa como inicializador (aunque no como constructor) y es utilizado para establecer el estado inicial del objeto, permitiendo cargar los valores de los atributos a través de parámetros. Durante este proceso es posible realizar validaciones sobre los datos y crear las estructuras internas necesarias para el funcionamiento del objeto.

class ClaseEjemplo:
    def __init__(self, parametro1, parametro2):
        self.parametro1 = parametro1
        self.parametro2 = parametro2

objeto = ClaseEjemplo("Valor1", "Valor2")

El verdadero constructor en Python es el método __new__(). Este es el que reserva la memoria para un nuevo objeto y devuelve la instancia una vez que ha sido creada. Su ejecución se realiza al instanciar un objeto antes de la ejecución del __init__() en el caso que exista.

Aunque en la mayoría de los casos no es necesaria su utilización, se usa cuando se trabaja con tipos de datos inmutables, o con patrones de diseños como Singleton.

class EjemploClase:
    def __new__(cls, *args, **kwargs):
        print("Creando instancia...")
        instancia = super().__new__(cls)
        return instancia

    def __init__(self, valor):
        print("Inicializando instancia...")
        self.valor = valor

objeto = EjemploClase(10)

De manera similar, existe el método especial llamado __del__(). Este se ejecuta antes de que el objeto sea destruido por el recolector de basura de Python, esto es, cuando ya no existen referencias al objeto, o cuando finaliza el programa.

No puede garantizarse el momento en el que se ejecutará el método o si se ejecutará por lo que no se recomienda su uso para tareas críticas. Su uso debe limitarse a liberar recursos que no son manejados por contextos, o para escribir mensajes de depuración.

class EjemploClase:
    def __init__(self):
        self.recurso = True

    def __del__(self):
        if self.recurso:
            print("Liberando recurso...")

Métodos de representación y conversión de objetos: __str__, __repr__ y __call__

Existen funciones especiales utilizadas para generar una representación legible de un objeto aunque las dos tienen usos diferentes.

La función __str__() tiene como objetivo principal generar una representación legible y estética para el usuario final, utilizada para mostrar información en interfaces o reportes. No necesita incluir todos los atributos y es invocada cuando se utilizan las funciones print o str pasando por parámetro el objeto.

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

    def __str__(self):
        return f"{self.nombre} {self.apellido}:{self.edad} años"

p = Persona("María", "Gonzales", 30)
print(p)  
objeto = str(p)
print(objeto)
María Gonzales:30 años
María Gonzales:30 años

El otro método es asociado es __repr__(), esta orientado a desarrolladores, y la representación tiene que ser precisa, técnica y completa, ya que es usada para hacer debugging.

Idealmente debería poder usarse su salida para recrear el objeto completo utilizando la función eval. Para invocarlo se utiliza la función repr directamente, o cuando se imprime un objeto y no está definida la función __str__().

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

    def __repr__(self):
        return f"Persona('{self.nombre}','{self.apellido}', {self.edad})"

    def __str__(self):
        return f"{self.nombre} {self.apellido}:{self.edad} años"

p = Persona("María", "Gonzales", 30)
print(p)
print(repr(p))
p2 = eval(repr(p)) # Recreación del objeto a partir de salida de __repr__
María Gonzales:30 años
Persona('María','Gonzales', 30)

Existen también métodos utilizados para especificar como convertir objetos en diferentes tipos de datos como __int__(), __float__(), __bool__(), __bytes__() y __format__().

class Temperatura:
    def __init__(self, celsius):
        self.celsius = celsius

    def __float__(self):
        return float(self.celsius)
        
t = Temperatura(36)
print(float(t))

Finalmente __call__() es utilizado para que la instancia de una clase se comporte como una función y pueda ser llamada directamente. Es especialmente útil para objetos que son contadores o validadores.

class ValidadorLongitud:
    def __init__(self, minimo, maximo):
        self.minimo = minimo
        self.maximo = maximo

    def __call__(self, texto):
        return self.minimo <= len(texto) <= self.maximo
        
validador = ValidadorLongitud(3, 10)
print(validador("Hola"))   # Muestra "True"
print(validador("Ho"))      # Muestra "False"

En este ejemplo, la clase que valida la longitud de una cadena almacena los valores mínimos y máximos como atributos. Al crear un objeto y llamarlo como si fuera una función con un valor como parámetro se invoca el método __call__() retornando True o False dependiendo de si el valor se encuentra entre los límites.

Métodos de comparación y operadores

Esta serie de funciones especiales se utilizan para permitir la comparación entre objetos definiendo sus comparaciones internas. Si están definidos, es posible utilizar los comparadores tradicionales entre objetos.

OperadorFunción especial
==__eq__()
!=__ne__()
<__lt__()
<=__le__()
>__gt__()
>=__ge__()

Internamente, estos métodos pueden comparar uno o más atributos, realizar cálculos, validaciones y conversiones. Su retorno deberá ser True, False o NotImplemented en el caso que esa operación no sea posible.

class Persona:
	def __init__(self, nombre, apellido , edad):
		self.nombre = nombre
		self.apellido = apellido
		self.edad = edad
	def __eq__(self, other):
		return (self.nombre, self.apellido) == (other.nombre, other.apellido)
	def __gt__(self, other):
		return self.edad > other.edad

p = Persona("María", "Gonzales", 30)
p2 = Persona("María", "Gonzales", 33)
print(p==p2)     # Muestra "True"
print(p>p2)       # Muestra "False"

En este ejemplo se definen dos operaciones de comparación: la operación de igualdad dará como resultado True si los dos objetos comparados tienen el mismo valor de nombre y apellido. La operación de mayor que será verdadera teniendo en cuenta solamente el atributo de edad.

Aunque ni self ni other son nombres obligatorios son la convención recomendada al referirse al mismo objeto y al objeto comparado respectivamente tanto en los métodos especiales de comparación como en los que se verán a continuación.

Junto con el comparador de igualdad es posible utilizar el método especial __hash__() , este será invocado al ejecutar la función hash sobre un objeto y definirá cómo calcular el hash de un objeto.


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

    def __eq__(self, other):
        return isinstance(other, Persona) and self.dni == other.dni

    def __hash__(self):
        return hash(self.dni)

p1 = Persona(534544, "Marta", "Gonzales")
p2 = Persona(332343, "Marta", "Gonzales")

print(p1 == p2)                        # Muestra False
print(hash(p1), hash(p2))     # Muestra 534544 332343

Métodos de operadores aritméticos

Similar a los casos anteriores, estas funciones especiales se utilizan para definir el comportamiento de los objetos al utilizar operadores aritméticos. El retorno de estas funciones debe ser un nuevo objeto o un valor coherente para el tipo de operación que se está realizando.

A continuación se listan algunas de estas funciones.

OperadorFunción especial
+__add__(self, other)
-__sub__(self, other)
*__mul__(self, other)
/__truediv__(self, other)
//__floordiv__(self, other)
%__mod__(self, other)
**__pow__(self, other)
@__matmul__(self, other)
+=__iadd__(self, other)
-=__isub__(self, other)
*=__imul__(self, other)
/=__itruediv__(self, other)
//=__ifloordiv__(self, other)

En el siguiente ejemplo se muestra la utilización de la función especial de suma para sumar el stock total de un producto sumando los objetos correspondientes a diferentes operaciones. La función da como resultado la suma de los valores correspondientes a el atributo unidades.

class Operacion:
    def __init__(self, tipo, unidades):
        self.tipo = tipo
        self.unidades = unidades

    def __add__(self, other):        
        stock = self.unidades + other.unidades        
        return stock
        
o1 = Operacion("venta", -10)
o2 = Operacion("Compra", 150)
stock = o1 + o2
print(stock)          
140

También sería posible definir la función para que retorne un objeto que sume las unidades y asignarle un valor al atributo operador.

class Operacion:
    def __init__(self, tipo, unidades):
        self.tipo = tipo
        self.unidades = unidades
        
    def __str__(self):
       return f"{self.tipo}:{self.unidades}"

    def __add__(self, other):        
        stock = self.unidades + other.unidades        
        return Operacion("total",stock)
        
o1 = Operacion("venta", -10)
o2 = Operacion("Compra", 150)
stock = o1 + o2
print(stock)     
total:140

Métodos para contenedores

Los métodos especiales para contenedores permiten que una clase personalizada se comporte como una colección.

A través de funciones como __len__(), __getitem__(), __setitem__(), __delitem__() y __contains__(), un objeto puede obtener su longitud, acceder o modificar elementos mediante índices o claves, eliminar valores o verificar la pertenencia mediante el operador in.

No solo es posible replicar el comportamiento de estructuras como listas y diccionarios, sino que también es posible agregar validaciones, procesamientos o restricciones.

Ejemplo sugerido:

Función especialUsoInvocación
__len__(self)Devuelve la cantidad de elementos del contenedor.len(obj)
__getitem__(self, key)Permite acceder a un elemento mediante índice o clave.obj[key]
__setitem__(self, key, value)Permite asignar un valor a una posición o clave.obj[key] = valor
__delitem__(self, key)Elimina un elemento por índice o clave.del obj[key]
__contains__(self, item)Evalúa si un ítem pertenece al contenedor.item in obj

En el siguiente caso se utiliza la función __getitem__() para poder acceder al atributo «productos», que es un diccionario, directamente desde el objeto Inventario de forma obj[key].

class Inventario:
    def __init__(self):
        self.productos = {
            "manzanas": 10,
            "bananas": 6,
            "naranjas": 4,
        }

    def __getitem__(self, nombre_producto):        
        return self.productos.get(nombre_producto, 0)



inv = Inventario()
print(inv["manzanas"])
print(inv["bananas"])   
10
6

Métodos de iteración: __iter__ y __next__

Los métodos __iter__() y __next__() se usan en conjunto para poder tratar objetos de clases como si fuesen objetos iterables.

Mientras que __iter__() es encargado de especificar cómo se debe obtener el objeto iterable y devolverlo, __next__ define cómo obtener el siguiente elemento durante la iteración. Cuándo no existan más objetos debe generar la excepción StopIteration.

class Inventario:
    def __init__(self, productos):
        self.productos = productos
        self.indice = 0  

    def __iter__(self):
        self.indice = 0
        return self

    def __next__(self):
        if self.indice >= len(self.productos):
            raise StopIteration 

        producto = self.productos[self.indice]
        self.indice += 1
        return producto
        
productos = [
    {"id": 1, "nombre": "Notebook", "stock": 10},
    {"id": 2, "nombre": "Mouse", "stock": 50},
    {"id": 3, "nombre": "Teclado", "stock": 20},
]

inventario = Inventario(productos)

for item in inventario:
    print(f"{item['nombre']}: {item['stock']} unidades")

Una vez definidos estos dos métodos especiales se puede recorrer el objeto como si fuese usa lista.

Notebook: 10 unidades
Mouse: 50 unidades
Teclado: 20 unidades

Métodos de contexto: __enter__ y __exit__

Los métodos __enter__() y __exit__() permiten que un objeto sea utilizado dentro de un bloque with, y se suele utilizar cuando se trabaja con recursos que deben abrirse, utilizarse y cerrarse o liberarse después de su uso.

El método __enter__() es ejecutado cuando se entra a un bloque with y devuelve el recurso que quiere utilizarse, por otro lado __exit__() se ejecuta cuando se sale del bloque with, independientemente si ha ocurrido o no una excepción. __exit__() es utilizado para limpiar recursos (cerrar archivos y conexiones, etc).

Este método cuenta con los argumentos correspondientes al tipo de excepción, el valor de la misma, y la traza, permitiendo modificar el comportamiento dependiendo de las excepciones que han sido generadas. Si devuelve True se suprime la excepción que se ha generado, y si devuelve False o no retorna ningún valor, la excepción se propagará.

class ConexionDB:
    def __init__(self):
        self.conectado = False

    def __enter__(self):
        print("Abriendo conexión...")
        self.conectado = True
        return self  # lo que se recibe dentro del 'as'

    def ejecutar(self, consulta):
        if not self.conectado:
            raise RuntimeError("No hay conexión activa.")
        print(f"Ejecutando consulta: {consulta}")

    def __exit__(self, tipo_excepcion, valor_excepcion, traza):
        print("Cerrando conexión...")
        self.conectado = False

        if tipo_excepcion:
            print(f"Ocurrió una excepción: {valor_excepcion}")
            return False  # Propaga la excepción

        return True

try:
    with ConexionDB() as db:
		
        db.ejecutar("Consulta A")
        db.ejecutar("Consulta B")
except Exception as e:
    print("Error manejado externamente:", e)

En este ejemplo el objeto encargado de la conexión a una base de datos ejecuta la abertura de la conexión al utilizar with con la clase y cierra la conexión al salir del bloque.

Abriendo conexión...
Ejecutando consulta: Consulta A
Ejecutando consulta: Consulta B
Cerrando conexión...

En este artículo hemos vistos los métodos especiales que pueden crearse en las clases de Python para facilitar su interacción con otras partes del lenguajes. Aunque pueden hacer nuestro código más legible es importante utilizarlos solo cuando tenga sentido semántico, e implementarlos de manera clara y legible.

Espero que les sirva! Cualquier duda me comentan!


¿Querés aprender más? 📚

👉 Visitá nuestros cursos!
💬 Y si tenés dudas, o querés dejarnos tus comentarios sumate a la Comunidad JuncoTIC en Telegram!
¡Te esperamos!


Andrea Navarro

- Ingeniera en Informática - Docente universitaria - Investigadora