Python

Python

Tutorial Python: Métodos especiales (dunder methods)

Aprende a implementar métodos especiales (dunder) en Python para personalizar clases con str, repr, operadores aritméticos y contenedores.

Aprende Python y certifícate

str y repr

En Python, los métodos especiales (también conocidos como métodos dunder, por el doble guion bajo o "double underscore") permiten personalizar el comportamiento de nuestras clases para que interactúen de forma natural con las funciones y operadores integrados del lenguaje. Dos de los métodos especiales más fundamentales son __str__ y __repr__, que controlan cómo se representan nuestros objetos como cadenas de texto.

Estos métodos son invocados automáticamente cuando usamos las funciones str() y repr() respectivamente, o cuando Python necesita una representación textual de nuestros objetos.

El método __str__

El método __str__ define la representación de cadena "informal" o "amigable para el usuario" de un objeto. Este método es llamado por la función str() y por la función print(). Su propósito es proporcionar una representación legible para humanos.

class Producto:
    def __init__(self, nombre, precio):
        self.nombre = nombre
        self.precio = precio
        
    # Sin __str__, obtenemos algo como <__main__.Producto object at 0x7f8a8c0b5e80>

Si creamos una instancia de esta clase e intentamos imprimirla:

producto = Producto("Laptop", 1200)
print(producto)  # <__main__.Producto object at 0x7f8a8c0b5e80>

Obtenemos una representación poco útil. Implementemos el método __str__:

class Producto:
    def __init__(self, nombre, precio):
        self.nombre = nombre
        self.precio = precio
        
    def __str__(self):
        return f"{self.nombre} - ${self.precio}"

Ahora, al imprimir el objeto:

producto = Producto("Laptop", 1200)
print(producto)  # Laptop - $1200
str(producto)    # 'Laptop - $1200'

Obtenemos una representación mucho más útil y legible.

El método __repr__

El método __repr__ define la representación de cadena "oficial" o "técnica" de un objeto. Este método es llamado por la función repr() y se utiliza en el intérprete interactivo cuando se evalúa una expresión. Su propósito es proporcionar una representación inequívoca que idealmente permita recrear el objeto.

class Producto:
    def __init__(self, nombre, precio):
        self.nombre = nombre
        self.precio = precio
        
    def __str__(self):
        return f"{self.nombre} - ${self.precio}"
    
    def __repr__(self):
        return f"Producto('{self.nombre}', {self.precio})"

Ahora, al usar repr():

producto = Producto("Laptop", 1200)
repr(producto)  # "Producto('Laptop', 1200)"

La representación repr es especialmente útil para depuración y para entender el estado interno de un objeto. Idealmente, eval(repr(obj)) debería crear un objeto con el mismo estado.

Diferencias clave entre __str__ y __repr__

  1. Propósito:
  • __str__: Legibilidad para usuarios finales
  • __repr__: Información técnica para desarrolladores
  1. Comportamiento por defecto:
  • Si __str__ no está definido, Python usa __repr__
  • Si __repr__ no está definido, se usa la representación predeterminada de la clase
  1. Contextos de uso:
  • __str__ se usa con print() y str()
  • __repr__ se usa en el intérprete interactivo y con repr()

Uso en colecciones

Cuando un objeto forma parte de una colección como una lista o un diccionario, Python utiliza __repr__ para mostrar los elementos:

productos = [
    Producto("Laptop", 1200),
    Producto("Mouse", 25)
]

print(productos)  # [Producto('Laptop', 1200), Producto('Mouse', 25)]

Implementación práctica

Veamos un ejemplo más completo con una clase Punto que representa coordenadas en un plano:

class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Punto en ({self.x}, {self.y})"
    
    def __repr__(self):
        return f"Punto({self.x}, {self.y})"

Ahora podemos ver cómo se comporta en diferentes contextos:

p = Punto(3, 4)

# Uso de str() - representación amigable
print(p)           # Punto en (3, 4)
print(f"{p}")      # Punto en (3, 4)

# Uso de repr() - representación técnica
print(repr(p))     # Punto(3, 4)
print(f"{p!r}")    # Punto(3, 4) - usando el especificador !r en f-strings

Caso práctico: Clase para gestión de fechas

Veamos un ejemplo más elaborado con una clase que gestiona fechas:

class Fecha:
    def __init__(self, dia, mes, año):
        self.dia = dia
        self.mes = mes
        self.año = año
        
        # Validación básica
        if not (1 <= dia <= 31 and 1 <= mes <= 12):
            raise ValueError("Fecha inválida")
    
    def __str__(self):
        # Formato amigable para usuarios
        meses = ["enero", "febrero", "marzo", "abril", "mayo", "junio", 
                 "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]
        return f"{self.dia} de {meses[self.mes-1]} de {self.año}"
    
    def __repr__(self):
        # Formato técnico para desarrolladores
        return f"Fecha({self.dia}, {self.mes}, {self.año})"

Uso de esta clase:

# Creamos una fecha
fecha_nacimiento = Fecha(15, 3, 1990)

# Representación para usuarios
print(fecha_nacimiento)  # 15 de marzo de 1990

# Representación para desarrolladores
print(repr(fecha_nacimiento))  # Fecha(15, 3, 1990)

# En una lista
fechas_importantes = [Fecha(1, 1, 2023), Fecha(25, 12, 2023)]
print(fechas_importantes)  # [Fecha(1, 1, 2023), Fecha(25, 12, 2023)]

Buenas prácticas

  1. Siempre implementa __repr__: Es una buena práctica implementar al menos __repr__ para todas tus clases, ya que proporciona información útil para depuración.

  2. Haz que __repr__ sea inequívoco: Idealmente, __repr__ debería proporcionar suficiente información para recrear el objeto.

  3. Implementa __str__ cuando necesites una representación amigable: Si tus objetos serán mostrados a usuarios finales, implementa __str__ para proporcionar una representación más legible.

  4. Reutiliza código: Si __str__ y __repr__ son similares, puedes hacer que uno llame al otro:

class Temperatura:
    def __init__(self, celsius):
        self.celsius = celsius
    
    def __repr__(self):
        return f"Temperatura({self.celsius})"
    
    # Reutilizamos __repr__ si no necesitamos una representación diferente
    __str__ = __repr__

Los métodos __str__ y __repr__ son fundamentales para crear clases que se integren bien con el resto del ecosistema Python y proporcionen información útil tanto para usuarios como para desarrolladores. Implementarlos correctamente mejora significativamente la usabilidad y depuración de nuestro código.

Operadores aritméticos

Los métodos dunder nos permiten personalizar cómo nuestras clases interactúan con los operadores aritméticos de Python. Esto nos da la capacidad de definir comportamientos personalizados cuando aplicamos operaciones como suma, resta o multiplicación a nuestros objetos.

Cuando utilizamos un operador aritmético entre objetos, Python busca un método dunder específico en la clase. Si este método está implementado, se ejecuta para realizar la operación. De lo contrario, obtendremos un error de tipo.

Métodos para operadores aritméticos básicos

Los principales métodos dunder para operaciones aritméticas son:

  • __add__: Implementa el comportamiento del operador +
  • __sub__: Implementa el comportamiento del operador -
  • __mul__: Implementa el comportamiento del operador *
  • __truediv__: Implementa el comportamiento del operador /

Veamos un ejemplo práctico con una clase Vector2D que representa un vector bidimensional:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def __str__(self):
        return f"({self.x}, {self.y})"
    
    def __add__(self, otro):
        """Suma de vectores: v1 + v2"""
        if isinstance(otro, Vector2D):
            return Vector2D(self.x + otro.x, self.y + otro.y)
        return NotImplemented
    
    def __sub__(self, otro):
        """Resta de vectores: v1 - v2"""
        if isinstance(otro, Vector2D):
            return Vector2D(self.x - otro.x, self.y - otro.y)
        return NotImplemented

Ahora podemos sumar y restar vectores de forma intuitiva:

v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)

v3 = v1 + v2  # Vector2D(4, 6)
print(v3)     # (4, 6)

v4 = v1 - v2  # Vector2D(2, 2)
print(v4)     # (2, 2)

Operaciones con escalares

También podemos implementar operaciones entre nuestros objetos y tipos diferentes, como números:

class Vector2D:
    # ... (código anterior) ...
    
    def __mul__(self, escalar):
        """Multiplicación por escalar: v * n"""
        if isinstance(escalar, (int, float)):
            return Vector2D(self.x * escalar, self.y * escalar)
        return NotImplemented
    
    def __rmul__(self, escalar):
        """Multiplicación por escalar (orden inverso): n * v"""
        # Delegamos a __mul__
        return self.__mul__(escalar)
    
    def __truediv__(self, escalar):
        """División por escalar: v / n"""
        if isinstance(escalar, (int, float)):
            if escalar == 0:
                raise ZeroDivisionError("No se puede dividir por cero")
            return Vector2D(self.x / escalar, self.y / escalar)
        return NotImplemented

Con estas implementaciones, podemos realizar:

v = Vector2D(3, 4)

# Multiplicación por escalar
v_doble = v * 2      # Vector2D(6, 8)
print(v_doble)       # (6, 8)

# Multiplicación en orden inverso
v_triple = 3 * v     # Vector2D(9, 12)
print(v_triple)      # (9, 12)

# División por escalar
v_mitad = v / 2      # Vector2D(1.5, 2.0)
print(v_mitad)       # (1.5, 2.0)

El valor NotImplemented

Observa que devolvemos NotImplemented (no confundir con la excepción NotImplementedError) cuando la operación no tiene sentido para los tipos proporcionados. Esto permite a Python buscar un método alternativo o lanzar un error apropiado.

Por ejemplo, si intentamos sumar un vector y un número:

v = Vector2D(3, 4)
# v + 5  # Esto lanzará TypeError: unsupported operand type(s) for +: 'Vector2D' and 'int'

Operadores aritméticos con asignación

Python también proporciona métodos dunder para operadores de asignación combinada como +=, -=, etc.:

class Vector2D:
    # ... (código anterior) ...
    
    def __iadd__(self, otro):
        """Implementa v1 += v2"""
        if isinstance(otro, Vector2D):
            self.x += otro.x
            self.y += otro.y
            return self
        return NotImplemented
    
    def __isub__(self, otro):
        """Implementa v1 -= v2"""
        if isinstance(otro, Vector2D):
            self.x -= otro.x
            self.y -= otro.y
            return self
        return NotImplemented

La diferencia clave es que estos métodos modifican el objeto existente en lugar de crear uno nuevo:

v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)

# Sin __iadd__, esto crearía un nuevo objeto
# Con __iadd__, modifica v1 directamente
v1 += v2
print(v1)  # (4, 6)

Operadores aritméticos adicionales

Python ofrece métodos dunder para todos los operadores aritméticos:

  • __floordiv__: Implementa la división entera //
  • __mod__: Implementa el módulo %
  • __pow__: Implementa la potencia **
  • __matmul__: Implementa la multiplicación de matrices @ (Python 3.5+)

Veamos un ejemplo con potencia y módulo:

class Complejo:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag
    
    def __str__(self):
        if self.imag >= 0:
            return f"{self.real} + {self.imag}i"
        return f"{self.real} - {abs(self.imag)}i"
    
    def __pow__(self, exponente):
        """Implementa c ** n para números enteros positivos"""
        if not isinstance(exponente, int) or exponente < 0:
            return NotImplemented
        
        if exponente == 0:
            return Complejo(1, 0)
        
        resultado = Complejo(self.real, self.imag)
        for _ in range(exponente - 1):
            # Multiplicación de números complejos
            real = resultado.real * self.real - resultado.imag * self.imag
            imag = resultado.real * self.imag + resultado.imag * self.real
            resultado.real, resultado.imag = real, imag
            
        return resultado
    
    def __abs__(self):
        """Implementa abs(c) - módulo del número complejo"""
        return (self.real ** 2 + self.imag ** 2) ** 0.5

Uso de estos métodos:

c = Complejo(3, 4)
print(c)         # 3 + 4i

# Potencia
c_cuadrado = c ** 2
print(c_cuadrado)  # -7 + 24i

# Módulo (valor absoluto)
modulo = abs(c)
print(modulo)      # 5.0

Operadores aritméticos reflexivos

Para cada operador aritmético, existe una versión "reflexiva" que se invoca cuando el operando izquierdo no sabe cómo manejar la operación. Estos métodos tienen nombres como __radd__, __rsub__, etc.

class Dinero:
    def __init__(self, cantidad):
        self.cantidad = cantidad
    
    def __str__(self):
        return f"${self.cantidad:.2f}"
    
    def __add__(self, otro):
        if isinstance(otro, Dinero):
            return Dinero(self.cantidad + otro.cantidad)
        elif isinstance(otro, (int, float)):
            return Dinero(self.cantidad + otro)
        return NotImplemented
    
    def __radd__(self, otro):
        """Maneja: número + Dinero"""
        # Delegamos a __add__
        return self.__add__(otro)

Esto permite operaciones como:

precio = Dinero(19.99)
propina = 3.00

# Ambas operaciones funcionan gracias a __radd__
total1 = precio + propina  # Dinero(22.99)
total2 = propina + precio  # Dinero(22.99)

print(total1)  # $22.99
print(total2)  # $22.99

Caso práctico: Clase Fracción

Veamos un ejemplo más completo implementando una clase Fraccion con soporte para operaciones aritméticas:

def mcd(a, b):
    """Calcula el máximo común divisor de a y b"""
    while b:
        a, b = b, a % b
    return a

class Fraccion:
    def __init__(self, numerador, denominador=1):
        if denominador == 0:
            raise ZeroDivisionError("El denominador no puede ser cero")
        
        # Simplificamos la fracción
        divisor = mcd(abs(numerador), abs(denominador))
        # Manejamos el signo
        if denominador < 0:
            numerador, denominador = -numerador, -denominador
            
        self.num = numerador // divisor
        self.den = denominador // divisor
    
    def __str__(self):
        if self.den == 1:
            return str(self.num)
        return f"{self.num}/{self.den}"
    
    def __add__(self, otro):
        if isinstance(otro, Fraccion):
            nuevo_num = self.num * otro.den + otro.num * self.den
            nuevo_den = self.den * otro.den
            return Fraccion(nuevo_num, nuevo_den)
        elif isinstance(otro, int):
            return self + Fraccion(otro)
        return NotImplemented
    
    def __sub__(self, otro):
        if isinstance(otro, Fraccion):
            nuevo_num = self.num * otro.den - otro.num * self.den
            nuevo_den = self.den * otro.den
            return Fraccion(nuevo_num, nuevo_den)
        elif isinstance(otro, int):
            return self - Fraccion(otro)
        return NotImplemented
    
    def __mul__(self, otro):
        if isinstance(otro, Fraccion):
            return Fraccion(self.num * otro.num, self.den * otro.den)
        elif isinstance(otro, int):
            return Fraccion(self.num * otro, self.den)
        return NotImplemented
    
    def __truediv__(self, otro):
        if isinstance(otro, Fraccion):
            return Fraccion(self.num * otro.den, self.den * otro.num)
        elif isinstance(otro, int):
            if otro == 0:
                raise ZeroDivisionError("División por cero")
            return Fraccion(self.num, self.den * otro)
        return NotImplemented
    
    # Métodos reflexivos
    __radd__ = __add__
    __rmul__ = __mul__

Ahora podemos realizar operaciones aritméticas con fracciones:

f1 = Fraccion(1, 2)  # 1/2
f2 = Fraccion(2, 3)  # 2/3

print(f1 + f2)       # 7/6
print(f1 - f2)       # -1/6
print(f1 * f2)       # 1/3
print(f1 / f2)       # 3/4

# Operaciones con enteros
print(f1 + 1)        # 3/2
print(2 * f1)        # 1/1 (simplificado a 1)

Consideraciones de diseño

Al implementar operadores aritméticos en tus clases, considera estas buenas prácticas:

  • Consistencia matemática: Asegúrate de que tus operaciones sigan las reglas matemáticas esperadas.
  • Inmutabilidad: Para objetos que representan valores matemáticos, es mejor crear nuevas instancias en lugar de modificar las existentes.
  • Validación de tipos: Verifica que los operandos sean compatibles y devuelve NotImplemented cuando no lo sean.
  • Simplificación: Cuando sea apropiado, simplifica los resultados (como hicimos con las fracciones).
  • Documentación: Documenta claramente el comportamiento esperado de cada operador.

La implementación de métodos dunder para operadores aritméticos hace que nuestras clases se integren de manera natural con el lenguaje Python, permitiendo escribir código más intuitivo y expresivo.

Comparación

Los métodos dunder de comparación permiten definir cómo se comportan nuestros objetos cuando los comparamos con otros usando operadores como ==, <, >, etc. Esto nos da control total sobre la lógica de comparación, permitiendo que nuestras clases personalizadas se comporten de manera intuitiva en expresiones condicionales, ordenamientos y otras operaciones de comparación.

Python utiliza seis métodos dunder principales para implementar las operaciones de comparación:

  • __eq__: Define el comportamiento del operador de igualdad (==)
  • __ne__: Define el comportamiento del operador de desigualdad (!=)
  • __lt__: Define el comportamiento del operador "menor que" (<)
  • __le__: Define el comportamiento del operador "menor o igual que" (<=)
  • __gt__: Define el comportamiento del operador "mayor que" (>)
  • __ge__: Define el comportamiento del operador "mayor o igual que" (>=)

Implementación básica de comparaciones

Veamos un ejemplo con una clase Temperatura que almacena valores en grados Celsius:

class Temperatura:
    def __init__(self, celsius):
        self.celsius = celsius
    
    def __str__(self):
        return f"{self.celsius}°C"
    
    def __eq__(self, otro):
        """Implementa: self == otro"""
        if isinstance(otro, Temperatura):
            return self.celsius == otro.celsius
        elif isinstance(otro, (int, float)):
            return self.celsius == otro
        return NotImplemented
    
    def __lt__(self, otro):
        """Implementa: self < otro"""
        if isinstance(otro, Temperatura):
            return self.celsius < otro.celsius
        elif isinstance(otro, (int, float)):
            return self.celsius < otro
        return NotImplemented

Con esta implementación, podemos comparar objetos Temperatura entre sí o con valores numéricos:

t1 = Temperatura(25)
t2 = Temperatura(30)
t3 = Temperatura(25)

# Comparaciones entre objetos
print(t1 == t3)  # True
print(t1 == t2)  # False
print(t1 < t2)   # True

# Comparaciones con números
print(t1 == 25)  # True
print(t2 > 28)   # True

Optimización con functools.total_ordering

Implementar los seis métodos de comparación puede ser repetitivo. Python proporciona el decorador total_ordering del módulo functools que nos permite definir solo __eq__ y uno de los métodos de ordenación (generalmente __lt__), y automáticamente implementa los demás:

from functools import total_ordering

@total_ordering
class Temperatura:
    def __init__(self, celsius):
        self.celsius = celsius
    
    def __str__(self):
        return f"{self.celsius}°C"
    
    def __eq__(self, otro):
        if isinstance(otro, Temperatura):
            return self.celsius == otro.celsius
        elif isinstance(otro, (int, float)):
            return self.celsius == otro
        return NotImplemented
    
    def __lt__(self, otro):
        if isinstance(otro, Temperatura):
            return self.celsius < otro.celsius
        elif isinstance(otro, (int, float)):
            return self.celsius < otro
        return NotImplemented

Ahora podemos usar todos los operadores de comparación, aunque solo implementamos dos métodos:

t1 = Temperatura(25)
t2 = Temperatura(30)

print(t1 <= t2)  # True
print(t1 >= t2)  # False
print(t1 != t2)  # True

Comparaciones reflexivas

Al igual que con los operadores aritméticos, Python intenta la operación inversa cuando el operando izquierdo devuelve NotImplemented. Sin embargo, para las comparaciones, no hay métodos reflexivos específicos como __req__ o __rlt__. En su lugar, Python invierte la comparación y utiliza el método correspondiente.

Por ejemplo, si a < b devuelve NotImplemented, Python intentará b > a.

Caso práctico: Comparación de versiones de software

Veamos un ejemplo más elaborado con una clase Version que permite comparar versiones de software siguiendo el formato semántico (major.minor.patch):

@total_ordering
class Version:
    def __init__(self, version_str):
        # Dividimos la cadena de versión y convertimos a enteros
        parts = version_str.split('.')
        # Aseguramos que tenemos al menos 3 componentes
        while len(parts) < 3:
            parts.append('0')
            
        self.major = int(parts[0])
        self.minor = int(parts[1])
        self.patch = int(parts[2])
    
    def __str__(self):
        return f"{self.major}.{self.minor}.{self.patch}"
    
    def __repr__(self):
        return f"Version('{self}')"
    
    def __eq__(self, otro):
        if not isinstance(otro, Version):
            return NotImplemented
        
        return (self.major, self.minor, self.patch) == (otro.major, otro.minor, otro.patch)
    
    def __lt__(self, otro):
        if not isinstance(otro, Version):
            return NotImplemented
        
        return (self.major, self.minor, self.patch) < (otro.major, otro.minor, otro.patch)

Esta implementación aprovecha el hecho de que las tuplas en Python se comparan elemento por elemento, lo que es perfecto para versiones semánticas:

v1 = Version("1.2.3")
v2 = Version("1.3.0")
v3 = Version("2.0.0")
v4 = Version("1.2.3")

# Comparaciones básicas
print(v1 == v4)  # True
print(v1 < v2)   # True
print(v2 < v3)   # True

# Ordenamiento de versiones
versiones = [v3, v1, v2, v4]
versiones_ordenadas = sorted(versiones)
print([str(v) for v in versiones_ordenadas])  # ['1.2.3', '1.2.3', '1.3.0', '2.0.0']

Comparaciones personalizadas y contextuales

A veces, queremos comparar objetos de manera diferente según el contexto. Por ejemplo, podríamos querer comparar estudiantes por su calificación o por su nombre:

class Estudiante:
    def __init__(self, nombre, calificacion):
        self.nombre = nombre
        self.calificacion = calificacion
    
    def __str__(self):
        return f"{self.nombre}: {self.calificacion}"
    
    def __eq__(self, otro):
        if not isinstance(otro, Estudiante):
            return NotImplemented
        
        # Dos estudiantes son iguales si tienen el mismo nombre
        return self.nombre == otro.nombre
    
    def __lt__(self, otro):
        if not isinstance(otro, Estudiante):
            return NotImplemented
        
        # Por defecto, comparamos por calificación
        return self.calificacion < otro.calificacion
    
    # Métodos alternativos de comparación
    def comparar_por_nombre(self, otro):
        if not isinstance(otro, Estudiante):
            return NotImplemented
        
        return self.nombre < otro.nombre

Podemos usar estos métodos así:

e1 = Estudiante("Ana", 85)
e2 = Estudiante("Carlos", 78)
e3 = Estudiante("Berta", 92)

# Comparación por defecto (calificación)
print(e1 < e2)  # False (85 no es menor que 78)

# Para ordenar por nombre, usamos una función key
estudiantes = [e1, e2, e3]
por_nombre = sorted(estudiantes, key=lambda e: e.nombre)
print([e.nombre for e in por_nombre])  # ['Ana', 'Berta', 'Carlos']

# Para ordenar por calificación
por_calificacion = sorted(estudiantes, key=lambda e: e.calificacion)
print([e.nombre for e in por_calificacion])  # ['Carlos', 'Ana', 'Berta']

Comparaciones ricas y hash

Si implementamos __eq__, generalmente también deberíamos implementar __hash__ para que nuestros objetos puedan usarse como claves en diccionarios o elementos en conjuntos:

class Coordenada:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __eq__(self, otro):
        if not isinstance(otro, Coordenada):
            return NotImplemented
        
        return self.x == otro.x and self.y == otro.y
    
    def __hash__(self):
        # El hash debe ser consistente con __eq__
        return hash((self.x, self.y))

Con esta implementación, podemos usar nuestros objetos como claves:

c1 = Coordenada(3, 4)
c2 = Coordenada(3, 4)  # Igual a c1
c3 = Coordenada(5, 6)

# Usamos coordenadas como claves en un diccionario
mapa = {
    c1: "Tesoro",
    c3: "Enemigo"
}

# Podemos buscar usando una coordenada equivalente
print(mapa[c2])  # "Tesoro"

Comparaciones con tolerancia

En aplicaciones científicas o de ingeniería, a menudo necesitamos comparar valores numéricos con cierta tolerancia:

class ValorNumerico:
    def __init__(self, valor, tolerancia=1e-9):
        self.valor = valor
        self.tolerancia = tolerancia
    
    def __str__(self):
        return str(self.valor)
    
    def __eq__(self, otro):
        if isinstance(otro, ValorNumerico):
            # Comparación con tolerancia
            return abs(self.valor - otro.valor) <= self.tolerancia
        elif isinstance(otro, (int, float)):
            return abs(self.valor - otro) <= self.tolerancia
        return NotImplemented
    
    def __lt__(self, otro):
        if isinstance(otro, ValorNumerico):
            # Para < necesitamos asegurar que la diferencia es mayor que la tolerancia
            return self.valor < otro.valor and not self.__eq__(otro)
        elif isinstance(otro, (int, float)):
            return self.valor < otro and abs(self.valor - otro) > self.tolerancia
        return NotImplemented

Esto permite comparaciones más robustas para valores de punto flotante:

v1 = ValorNumerico(1.0000001)
v2 = ValorNumerico(1.0000002)

print(v1 == v2)  # True (dentro de la tolerancia)
print(v1 == 1.0)  # True

v3 = ValorNumerico(1.1)
print(v1 == v3)  # False (fuera de la tolerancia)
print(v1 < v3)   # True

Buenas prácticas para implementar comparaciones

  1. Consistencia: Asegúrate de que tus comparaciones sean consistentes entre sí. Si a == b entonces no debería ser cierto que a < b o a > b.

  2. Transitividad: Si a < b y b < c, entonces debería ser cierto que a < c.

  3. Reflexividad: Un objeto siempre debe ser igual a sí mismo (a == a).

  4. Simetría: Si a == b entonces b == a.

  5. Manejo de tipos: Devuelve NotImplemented cuando compares con tipos incompatibles, en lugar de lanzar una excepción.

  6. Documentación: Documenta claramente la lógica de comparación, especialmente si no es obvia.

  7. Eficiencia: Las comparaciones deben ser operaciones rápidas, ya que pueden usarse frecuentemente en ordenamientos y búsquedas.

La implementación adecuada de los métodos de comparación hace que nuestras clases se integren perfectamente con las funciones de ordenamiento de Python como sorted(), min(), max() y con estructuras de datos ordenadas como heapq o bisect.

Contenedores

Los métodos dunder para contenedores permiten que nuestras clases personalizadas se comporten como las estructuras de datos integradas de Python (listas, diccionarios, etc.). Estos métodos nos dan la capacidad de crear objetos que se pueden indexar, iterar, y consultar por su longitud, haciendo que nuestras clases se integren perfectamente con el resto del ecosistema Python.

Cuando implementamos estos métodos, nuestros objetos pueden utilizarse con la sintaxis familiar de Python para acceder a elementos, verificar pertenencia y determinar el tamaño de la colección.

Métodos dunder fundamentales para contenedores

Los métodos más importantes para implementar un contenedor son:

  • __len__: Define el comportamiento de la función len()
  • __getitem__: Define el acceso mediante índice o clave objeto[clave]
  • __setitem__: Define la asignación mediante índice o clave objeto[clave] = valor
  • __delitem__: Define la eliminación mediante índice o clave del objeto[clave]
  • __contains__: Define el comportamiento del operador in
  • __iter__: Define el comportamiento para iteración

Veamos un ejemplo básico con una clase Cartera que almacena acciones:

class Cartera:
    def __init__(self):
        self.acciones = {}  # Diccionario con símbolo -> cantidad
    
    def __len__(self):
        """Devuelve el número de símbolos diferentes en la cartera"""
        return len(self.acciones)
    
    def __getitem__(self, simbolo):
        """Permite acceder a la cantidad de acciones con cartera['AAPL']"""
        return self.acciones.get(simbolo, 0)
    
    def __setitem__(self, simbolo, cantidad):
        """Permite asignar cantidades con cartera['AAPL'] = 10"""
        if cantidad <= 0:
            # Si la cantidad es cero o negativa, eliminamos el símbolo
            if simbolo in self.acciones:
                del self.acciones[simbolo]
        else:
            self.acciones[simbolo] = cantidad
    
    def __delitem__(self, simbolo):
        """Permite eliminar símbolos con del cartera['AAPL']"""
        if simbolo in self.acciones:
            del self.acciones[simbolo]
        else:
            raise KeyError(f"Símbolo no encontrado: {simbolo}")
    
    def __contains__(self, simbolo):
        """Permite verificar si un símbolo existe con 'AAPL' in cartera"""
        return simbolo in self.acciones
    
    def __iter__(self):
        """Permite iterar sobre los símbolos de la cartera"""
        return iter(self.acciones)

Ahora podemos usar esta clase como un contenedor nativo de Python:

# Creamos una cartera y añadimos acciones
cartera = Cartera()
cartera['AAPL'] = 10
cartera['MSFT'] = 15
cartera['GOOG'] = 8

# Verificamos la longitud
print(len(cartera))  # 3

# Accedemos a elementos
print(cartera['AAPL'])  # 10
print(cartera['TSLA'])  # 0 (no existe, pero devuelve 0 en lugar de error)

# Verificamos pertenencia
print('MSFT' in cartera)  # True
print('TSLA' in cartera)  # False

# Iteramos sobre los símbolos
for simbolo in cartera:
    print(f"{simbolo}: {cartera[simbolo]} acciones")
# Salida:
# AAPL: 10 acciones
# MSFT: 15 acciones
# GOOG: 8 acciones

# Eliminamos un símbolo
del cartera['GOOG']
print('GOOG' in cartera)  # False

Implementando un contenedor de secuencia

Si queremos crear un contenedor que se comporte como una lista o tupla, necesitamos implementar __getitem__ para manejar índices enteros y slices:

class HistorialPrecios:
    def __init__(self, simbolo, precios):
        self.simbolo = simbolo
        self.precios = list(precios)  # Hacemos una copia de la lista
    
    def __len__(self):
        """Devuelve el número de precios en el historial"""
        return len(self.precios)
    
    def __getitem__(self, indice):
        """Maneja historial[i] y historial[i:j]"""
        if isinstance(indice, slice):
            # Si es un slice, devolvemos un nuevo HistorialPrecios
            return HistorialPrecios(self.simbolo, self.precios[indice])
        else:
            # Si es un índice, devolvemos el precio directamente
            return self.precios[indice]
    
    def __iter__(self):
        """Permite iterar sobre los precios"""
        return iter(self.precios)
    
    def __contains__(self, precio):
        """Verifica si un precio está en el historial"""
        return precio in self.precios
    
    def __str__(self):
        return f"Historial de {self.simbolo}: {self.precios}"

Ahora podemos usar esta clase como una secuencia:

# Creamos un historial de precios
historial = HistorialPrecios('AAPL', [150.25, 152.75, 148.90, 155.30, 153.80])

# Accedemos por índice
print(historial[0])  # 150.25
print(historial[-1])  # 153.80

# Usamos slices
primera_mitad = historial[:3]
print(primera_mitad)  # Historial de AAPL: [150.25, 152.75, 148.9]

# Verificamos longitud
print(len(historial))  # 5

# Verificamos pertenencia
print(152.75 in historial)  # True
print(160.00 in historial)  # False

# Iteramos sobre los precios
for precio in historial:
    print(f"${precio:.2f}")
# Salida:
# $150.25
# $152.75
# $148.90
# $155.30
# $153.80

Implementando un contenedor mutable

Para crear un contenedor que permita modificar sus elementos, necesitamos implementar __setitem__ y posiblemente __delitem__:

class Matriz2D:
    def __init__(self, filas, columnas, valor_inicial=0):
        self.filas = filas
        self.columnas = columnas
        # Creamos una matriz con el valor inicial
        self.datos = [[valor_inicial for _ in range(columnas)] for _ in range(filas)]
    
    def __getitem__(self, indices):
        """Permite acceder con matriz[i, j]"""
        if isinstance(indices, tuple) and len(indices) == 2:
            fila, columna = indices
            if 0 <= fila < self.filas and 0 <= columna < self.columnas:
                return self.datos[fila][columna]
            else:
                raise IndexError("Índices fuera de rango")
        else:
            raise TypeError("Se requieren dos índices: matriz[fila, columna]")
    
    def __setitem__(self, indices, valor):
        """Permite asignar con matriz[i, j] = valor"""
        if isinstance(indices, tuple) and len(indices) == 2:
            fila, columna = indices
            if 0 <= fila < self.filas and 0 <= columna < self.columnas:
                self.datos[fila][columna] = valor
            else:
                raise IndexError("Índices fuera de rango")
        else:
            raise TypeError("Se requieren dos índices: matriz[fila, columna] = valor")
    
    def __len__(self):
        """Devuelve el número total de elementos en la matriz"""
        return self.filas * self.columnas
    
    def __str__(self):
        """Representación en forma de matriz"""
        return '\n'.join([' '.join([str(self.datos[i][j]) for j in range(self.columnas)]) 
                          for i in range(self.filas)])

Uso de esta matriz:

# Creamos una matriz 3x3
matriz = Matriz2D(3, 3)

# Asignamos valores
matriz[0, 0] = 1
matriz[1, 1] = 5
matriz[2, 2] = 9

# Accedemos a valores
print(matriz[1, 1])  # 5

# Mostramos la matriz completa
print(matriz)
# Salida:
# 1 0 0
# 0 5 0
# 0 0 9

# Verificamos la longitud (número total de elementos)
print(len(matriz))  # 9

Implementando iteradores personalizados

El método __iter__ debe devolver un iterador, que es un objeto con un método __next__ que devuelve el siguiente elemento o lanza StopIteration cuando no hay más elementos:

class RangoInverso:
    """Clase que itera desde n hasta 1"""
    
    def __init__(self, n):
        self.n = n
    
    def __iter__(self):
        """Devuelve un iterador"""
        return RangoInversoIterador(self.n)
    
    def __len__(self):
        return self.n
    
    def __contains__(self, item):
        return isinstance(item, int) and 1 <= item <= self.n

class RangoInversoIterador:
    def __init__(self, n):
        self.actual = n
    
    def __next__(self):
        if self.actual < 1:
            raise StopIteration
        
        valor = self.actual
        self.actual -= 1
        return valor

Uso de este iterador personalizado:

# Creamos un rango inverso de 5 a 1
rango = RangoInverso(5)

# Verificamos longitud y pertenencia
print(len(rango))  # 5
print(3 in rango)  # True
print(6 in rango)  # False

# Iteramos sobre el rango
for num in rango:
    print(num)
# Salida:
# 5
# 4
# 3
# 2
# 1

# También podemos usar list() para convertirlo a lista
print(list(rango))  # [5, 4, 3, 2, 1]

Implementando un contenedor de solo lectura

A veces queremos crear contenedores que no permitan modificaciones:

class RegistroInmutable:
    def __init__(self, **kwargs):
        # Almacenamos los valores en un diccionario interno
        self._datos = kwargs
    
    def __getitem__(self, clave):
        """Permite acceso mediante registro['nombre']"""
        if clave in self._datos:
            return self._datos[clave]
        raise KeyError(f"Clave no encontrada: {clave}")
    
    def __contains__(self, clave):
        """Permite verificar si una clave existe"""
        return clave in self._datos
    
    def __iter__(self):
        """Permite iterar sobre las claves"""
        return iter(self._datos)
    
    def __len__(self):
        """Devuelve el número de campos"""
        return len(self._datos)
    
    def __str__(self):
        campos = [f"{k}={v}" for k, v in self._datos.items()]
        return f"Registro({', '.join(campos)})"
    
    # No implementamos __setitem__ ni __delitem__, por lo que no se pueden modificar

Uso de este registro inmutable:

# Creamos un registro de usuario
usuario = RegistroInmutable(id=1001, nombre="Ana García", email="ana@ejemplo.com")

# Accedemos a los campos
print(usuario['nombre'])  # Ana García

# Verificamos si existen campos
print('email' in usuario)  # True
print('telefono' in usuario)  # False

# Iteramos sobre las claves
for campo in usuario:
    print(f"{campo}: {usuario[campo]}")
# Salida:
# id: 1001
# nombre: Ana García
# email: ana@ejemplo.com

# Intentar modificar generará un error
# usuario['email'] = "nuevo@ejemplo.com"  # AttributeError: 'RegistroInmutable' object has no attribute '__setitem__'

Caso práctico: Implementación de una cola de prioridad

Veamos un ejemplo más completo implementando una cola de prioridad simple:

class ColaPrioridad:
    def __init__(self):
        self._elementos = []
    
    def agregar(self, item, prioridad):
        """Agrega un elemento con su prioridad (menor número = mayor prioridad)"""
        self._elementos.append((prioridad, item))
        # Mantenemos la lista ordenada por prioridad
        self._elementos.sort(key=lambda x: x[0])
    
    def __len__(self):
        """Devuelve el número de elementos en la cola"""
        return len(self._elementos)
    
    def __getitem__(self, indice):
        """Permite acceder a los elementos por índice"""
        if 0 <= indice < len(self._elementos):
            # Devolvemos solo el item, no la prioridad
            return self._elementos[indice][1]
        raise IndexError("Índice fuera de rango")
    
    def __iter__(self):
        """Permite iterar sobre los elementos (sin sus prioridades)"""
        for _, item in self._elementos:
            yield item
    
    def __contains__(self, item):
        """Verifica si un elemento está en la cola"""
        return any(elemento == item for _, elemento in self._elementos)
    
    def extraer(self):
        """Extrae el elemento de mayor prioridad"""
        if not self._elementos:
            raise IndexError("La cola está vacía")
        return self._elementos.pop(0)[1]
    
    def __str__(self):
        if not self._elementos:
            return "Cola vacía"
        
        resultado = ["Cola de prioridad:"]
        for prioridad, item in self._elementos:
            resultado.append(f"  Prioridad {prioridad}: {item}")
        return "\n".join(resultado)

Uso de esta cola de prioridad:

# Creamos una cola de prioridad
cola = ColaPrioridad()

# Agregamos elementos con diferentes prioridades
cola.agregar("Tarea crítica", 1)
cola.agregar("Tarea normal", 3)
cola.agregar("Tarea urgente", 2)
cola.agregar("Tarea de baja prioridad", 5)

# Verificamos la longitud
print(len(cola))  # 4

# Accedemos por índice (ya ordenados por prioridad)
print(cola[0])  # Tarea crítica
print(cola[2])  # Tarea normal

# Verificamos si un elemento está en la cola
print("Tarea urgente" in cola)  # True
print("Tarea cancelada" in cola)  # False

# Iteramos sobre los elementos
print("Elementos en la cola:")
for tarea in cola:
    print(f"- {tarea}")
# Salida:
# Elementos en la cola:
# - Tarea crítica
# - Tarea urgente
# - Tarea normal
# - Tarea de baja prioridad

# Extraemos elementos por orden de prioridad
print(cola.extraer())  # Tarea crítica
print(cola.extraer())  # Tarea urgente

# Verificamos el estado actual
print(cola)
# Salida:
# Cola de prioridad:
#   Prioridad 3: Tarea normal
#   Prioridad 5: Tarea de baja prioridad

Implementando un contenedor con acceso múltiple

Podemos crear contenedores que permitan diferentes formas de acceso a sus elementos:

class Estudiantes:
    def __init__(self):
        self._por_id = {}
        self._por_nombre = {}
    
    def agregar(self, id_estudiante, nombre, calificacion):
        """Agrega un estudiante a la colección"""
        estudiante = {"id": id_estudiante, "nombre": nombre, "calificacion": calificacion}
        
        # Almacenamos referencias al mismo diccionario
        self._por_id[id_estudiante] = estudiante
        self._por_nombre[nombre] = estudiante
    
    def __getitem__(self, clave):
        """Permite acceder por ID (int) o por nombre (str)"""
        if isinstance(clave, int):
            # Acceso por ID
            if clave in self._por_id:
                return self._por_id[clave]
            raise KeyError(f"ID no encontrado: {clave}")
        
        elif isinstance(clave, str):
            # Acceso por nombre
            if clave in self._por_nombre:
                return self._por_nombre[clave]
            raise KeyError(f"Nombre no encontrado: {clave}")
        
        else:
            raise TypeError("La clave debe ser un ID (int) o un nombre (str)")
    
    def __contains__(self, clave):
        """Verifica si existe un estudiante con el ID o nombre dado"""
        if isinstance(clave, int):
            return clave in self._por_id
        elif isinstance(clave, str):
            return clave in self._por_nombre
        return False
    
    def __len__(self):
        """Devuelve el número de estudiantes"""
        return len(self._por_id)
    
    def __iter__(self):
        """Itera sobre los datos de los estudiantes"""
        return iter(self._por_id.values())

Uso de esta clase:

# Creamos una colección de estudiantes
estudiantes = Estudiantes()

# Agregamos algunos estudiantes
estudiantes.agregar(101, "Ana López", 85)
estudiantes.agregar(102, "Carlos Ruiz", 92)
estudiantes.agregar(103, "Elena Martín", 78)

# Accedemos por ID
print(estudiantes[101]["calificacion"])  # 85

# Accedemos por nombre
print(estudiantes["Carlos Ruiz"]["id"])  # 102

# Verificamos si existen estudiantes
print(103 in estudiantes)  # True
print("Elena Martín" in estudiantes)  # True
print("Juan Pérez" in estudiantes)  # False

# Iteramos sobre todos los estudiantes
print("Lista de estudiantes:")
for estudiante in estudiantes:
    print(f"ID: {estudiante['id']}, Nombre: {estudiante['nombre']}, Calificación: {estudiante['calificacion']}")

Buenas prácticas para implementar contenedores

  1. Consistencia: Asegúrate de que tus métodos de contenedor se comporten de manera consistente con los contenedores estándar de Python.

  2. Validación: Valida los índices y claves para evitar comportamientos inesperados.

  3. Eficiencia: Implementa los métodos de manera eficiente, especialmente __contains__ y __getitem__ que se usan frecuentemente.

  4. Inmutabilidad: Decide si tu contenedor debe ser mutable o inmutable y sé consistente con esa decisión.

  5. Documentación: Documenta claramente cómo se debe acceder a los elementos y qué excepciones pueden lanzarse.

  6. Iteración: Proporciona una forma eficiente de iterar sobre los elementos con __iter__.

  7. Manejo de errores: Lanza excepciones apropiadas como IndexError, KeyError o TypeError cuando sea necesario.

La implementación adecuada de los métodos dunder para contenedores permite que nuestras clases se integren perfectamente con el resto del ecosistema Python, haciendo que nuestro código sea más intuitivo y fácil de usar.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende Python online

Ejercicios de esta lección Métodos especiales (dunder methods)

Evalúa tus conocimientos de esta lección Métodos especiales (dunder methods) 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

Todas las 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

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la diferencia y uso de los métodos especiales str y repr para representar objetos.
  • Implementar operadores aritméticos personalizados mediante métodos dunder como add, sub, mul, etc.
  • Definir comparaciones personalizadas con métodos como eq, lt, y utilizar functools.total_ordering para simplificar.
  • Crear clases que se comporten como contenedores mediante métodos como len, getitem, setitem, iter, y otros.
  • Aplicar buenas prácticas en la implementación de métodos especiales para mejorar la usabilidad y depuración del código.