31. Funcionalidades avanzadas de clases
En el Capítulo 30, aprendimos a crear clases básicas con atributos y métodos de instancia. Ahora exploraremos funcionalidades de clases más sofisticadas que te dan un control detallado sobre cómo se comportan tus objetos. Estas funcionalidades te permiten crear clases que se sienten como tipos integrados de Python, con una sintaxis natural para operaciones como suma, comparación e indexación.
31.1) Variables de clase vs variables de instancia
Cuando creamos atributos en una clase, tenemos dos lugares fundamentalmente diferentes para almacenarlos: en la propia clase o en instancias individuales. Comprender esta distinción es crucial para escribir código orientado a objetos correcto.
31.1.1) Comprender las variables de instancia
Las variables de instancia (instance variables) son atributos que pertenecen a un objeto específico. Cada instancia tiene su propia copia separada de estas variables. Hemos estado usando variables de instancia a lo largo del Capítulo 30: son los atributos que creamos en __init__ usando self:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Variable de instancia
self.balance = balance # Variable de instancia
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.balance) # Output: 1000
print(account2.balance) # Output: 500Cada instancia de BankAccount tiene su propio owner y balance. Cambiar account1.balance no afecta a account2.balance: son completamente independientes.
31.1.2) Comprender las variables de clase
Las variables de clase (class variables) son atributos que pertenecen a la propia clase, no a ninguna instancia en particular. Todas las instancias comparten la misma variable de clase. Definimos variables de clase directamente en el cuerpo de la clase, fuera de cualquier método:
class BankAccount:
interest_rate = 0.02 # Variable de clase: compartida por todas las instancias
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def apply_interest(self):
self.balance += self.balance * BankAccount.interest_rate
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.interest_rate) # Output: 0.02
print(account2.interest_rate) # Output: 0.02
print(BankAccount.interest_rate) # Output: 0.02Fíjate en que podemos acceder a interest_rate a través de instancias (account1.interest_rate) o a través de la propia clase (BankAccount.interest_rate). Ambos se refieren a la misma variable.
Esto es lo que hace que las variables de clase sean potentes: cuando cambiamos la variable de clase, todas las instancias ven el cambio:
class BankAccount:
interest_rate = 0.02
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.interest_rate) # Output: 0.02
print(account2.interest_rate) # Output: 0.02
# Cambiar la variable de clase
BankAccount.interest_rate = 0.03
print(account1.interest_rate) # Output: 0.03
print(account2.interest_rate) # Output: 0.03Ambas instancias ven inmediatamente la nueva tasa de interés porque todas están mirando la misma variable de clase.
31.1.3) La trampa del sombreado: cuando las variables de instancia ocultan variables de clase
Aquí hay un comportamiento sutil pero importante: si asignas a un atributo a través de una instancia, Python crea una variable de instancia que sombrea (oculta) la variable de clase:
class BankAccount:
interest_rate = 0.02
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
# Crear una variable de instancia que sombrea la variable de clase
account1.interest_rate = 0.05
print(account1.interest_rate) # Output: 0.05 (instance variable)
print(account2.interest_rate) # Output: 0.02 (class variable)
print(BankAccount.interest_rate) # Output: 0.02 (class variable)Ahora account1 tiene su propia variable de instancia interest_rate que oculta la variable de clase. La variable de clase sigue existiendo, pero account1.interest_rate se refiere a la variable de instancia en su lugar. Normalmente esto no es lo que quieres: si necesitas cambiar una variable de clase, cámbiala a través del nombre de la clase, no a través de una instancia.
31.1.4) Usos prácticos de las variables de clase
Las variables de clase son útiles para datos que deben compartirse entre todas las instancias:
class Student:
school_name = "Python High School" # Igual para todos los estudiantes
total_students = 0 # Registrar cuántos estudiantes existen
def __init__(self, name, grade):
self.name = name
self.grade = grade
Student.total_students += 1 # Incrementar al crear un estudiante
def __str__(self):
return f"{self.name} (Grade {self.grade}) at {Student.school_name}"
student1 = Student("Alice", 10)
student2 = Student("Bob", 11)
student3 = Student("Carol", 10)
print(student1) # Output: Alice (Grade 10) at Python High School
print(f"Total students: {Student.total_students}") # Output: Total students: 3Fíjate en cómo usamos Student.total_students (no self.total_students) en __init__ para dejar claro que estamos modificando la variable de clase, no creando una variable de instancia.
31.2) Gestionar atributos con @property
A veces quieres controlar lo que ocurre cuando alguien accede o modifica un atributo. Por ejemplo, quizá quieras validar que un valor sea positivo, o calcular un valor al vuelo en lugar de almacenarlo. El decorador @property de Python te permite escribir métodos que se ven como un acceso simple a un atributo.
31.2.1) El problema: el acceso directo a atributos no puede validar
Cuando se accede a los atributos directamente, no puedes validar ni transformar los valores:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
temp = Temperature(25)
print(temp.celsius) # Output: 25
# Nada nos impide establecer temperaturas físicamente imposibles
temp.celsius = -500 # ¡Por debajo del cero absoluto (-273.15°C)!
print(temp.celsius) # Output: -500
# O valores absurdamente altos
temp.celsius = 1000000
print(temp.celsius) # Output: 1000000Sin validación, podemos establecer datos inválidos por accidente, lo que provoca errores más adelante en el programa. Podríamos usar métodos como get_celsius() y set_celsius(), pero eso no es idiomático en Python. Los desarrolladores de Python esperan acceder a los atributos directamente, no mediante métodos getter/setter como en Java o C++.
31.2.2) Usar @property para atributos calculados
El decorador @property convierte un método en un "getter" al que se accede como a un atributo:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
"""Convert celsius to fahrenheit on-the-fly"""
return self.celsius * 9/5 + 32
temp = Temperature(25)
print(temp.celsius) # Output: 25
print(temp.fahrenheit) # Output: 77.0 (computed, not stored)Fíjate en que llamamos a temp.fahrenheit sin paréntesis: parece un acceso a un atributo, pero en realidad está llamando al método. El valor de fahrenheit se calcula cada vez que accedes a él, así que siempre está sincronizado con celsius:
temp = Temperature(0)
print(temp.fahrenheit) # Output: 32.0
temp.celsius = 100
print(temp.fahrenheit) # Output: 212.0 (automatically updated)31.2.3) Añadir un setter con @property_name.setter
Para permitir establecer una property, añadimos un método setter usando el decorador @property_name.setter:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Convert fahrenheit to celsius when setting"""
self.celsius = (value - 32) * 5/9
temp = Temperature(0)
print(temp.celsius) # Output: 0
print(temp.fahrenheit) # Output: 32.0
# Establecer la temperatura usando fahrenheit
temp.fahrenheit = 212
print(temp.celsius) # Output: 100.0
print(temp.fahrenheit) # Output: 212.0El método setter recibe el nuevo valor y puede validarlo o transformarlo antes de almacenarlo.
31.2.4) Usar propiedades para validación
Las propiedades son excelentes para imponer restricciones:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # El guion bajo sugiere "uso interno"
@property
def balance(self):
"""Obtener el saldo actual"""
return self._balance
@balance.setter
def balance(self, value):
"""Establecer el saldo, pero solo si no es negativo"""
if value < 0:
raise ValueError("Balance cannot be negative")
self._balance = value
account = BankAccount("Alice", 1000)
print(account.balance) # Output: 1000
account.balance = 1500 # Funciona bien
print(account.balance) # Output: 1500
# Esto lanza un error
account.balance = -100
# Output: ValueError: Balance cannot be negativeFíjate en la convención de nombres: almacenamos el valor real en _balance (con un guion bajo inicial) y lo exponemos mediante la property balance. El guion bajo es una convención de Python que sugiere "este es un detalle de implementación interno", aunque el atributo sigue siendo técnicamente accesible. Este patrón nos permite controlar el acceso a través de la property mientras mantenemos separado el almacenamiento real.
31.2.5) Propiedades de solo lectura
Si defines una property sin un setter, se vuelve de solo lectura:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
"""Propiedad calculada de solo lectura"""
return self.width * self.height
rect = Rectangle(5, 3)
print(rect.area) # Output: 15
rect.width = 10
print(rect.area) # Output: 30 (automatically updated)
# Intentar establecer area lanza un error
rect.area = 50
# Output: AttributeError: property 'area' of 'Rectangle' object has no setterEsto es útil para valores derivados que deben calcularse, no almacenarse.
31.3) Métodos de clase con @classmethod
A veces necesitas métodos que trabajen con la propia clase en lugar de con instancias. Los métodos de clase (class methods) reciben la clase como su primer argumento (por convención llamado cls) en lugar de una instancia (self).
31.3.1) Definir métodos de clase
Creamos métodos de clase usando el decorador @classmethod:
class Student:
school_name = "Python High School"
def __init__(self, name, grade):
self.name = name
self.grade = grade
@classmethod
def get_school_name(cls):
"""Método de clase: recibe la clase, no una instancia"""
return cls.school_name
# Llamar en la propia clase
print(Student.get_school_name()) # Output: Python High School
# También se puede llamar en una instancia (pero cls sigue siendo la clase)
student = Student("Alice", 10)
print(student.get_school_name()) # Output: Python High SchoolEl parámetro cls recibe automáticamente la clase, igual que self recibe automáticamente la instancia en los métodos normales.
31.3.2) Constructores alternativos con métodos de clase
Uno de los usos más comunes de los métodos de clase es crear constructores alternativos: diferentes formas de crear instancias:
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
"""Crear un Date a partir de una cadena como '2024-12-27'"""
year, month, day = date_string.split('-')
return cls(int(year), int(month), int(day))
@classmethod
def today(cls):
"""Crear un Date para hoy (ejemplo simplificado)"""
# En código real, usarías el módulo datetime
return cls(2024, 12, 27)
def __str__(self):
return f"{self.year}-{self.month:02d}-{self.day:02d}"
# Constructor normal
date1 = Date(2024, 12, 27)
print(date1) # Output: 2024-12-27
# Constructor alternativo desde cadena
date2 = Date.from_string("2024-12-27")
print(date2) # Output: 2024-12-27
# Constructor alternativo para hoy
date3 = Date.today()
print(date3) # Output: 2024-12-27Fíjate en que from_string y today devuelven cls(...): esto crea una nueva instancia de la clase. Usar cls en lugar de codificar Date hace que el código funcione correctamente con subclases (aprenderemos sobre herencia en el Capítulo 32).
31.3.3) Métodos de clase para patrones de fábrica
Los métodos de clase son útiles para crear instancias con diferentes configuraciones:
class DatabaseConnection:
def __init__(self, host, port, database, username):
self.host = host
self.port = port
self.database = database
self.username = username
@classmethod
def for_development(cls):
"""Crear una conexión configurada para desarrollo"""
return cls("localhost", 5432, "dev_db", "dev_user")
@classmethod
def for_production(cls):
"""Crear una conexión configurada para producción"""
return cls("prod.example.com", 5432, "prod_db", "prod_user")
def __str__(self):
return f"Connection to {self.database} at {self.host}:{self.port}"
# Fácil crear conexiones preconfiguradas
dev_conn = DatabaseConnection.for_development()
prod_conn = DatabaseConnection.for_production()
print(dev_conn) # Output: Connection to dev_db at localhost:5432
print(prod_conn) # Output: Connection to prod_db at prod.example.com:543231.3.4) Métodos de clase para contar instancias
Los métodos de clase pueden trabajar con variables de clase para rastrear información sobre todas las instancias:
class Product:
total_products = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.total_products += 1
@classmethod
def get_total_products(cls):
"""Devolver el número total de productos creados"""
return cls.total_products
@classmethod
def reset_count(cls):
"""Restablecer el contador de productos"""
cls.total_products = 0
product1 = Product("Laptop", 999)
product2 = Product("Mouse", 25)
product3 = Product("Keyboard", 75)
print(Product.get_total_products()) # Output: 3
Product.reset_count()
print(Product.get_total_products()) # Output: 031.4) Métodos estáticos con @staticmethod
Los métodos estáticos (static methods) son métodos que no reciben la instancia (self) ni la clase (cls) como su primer argumento. Son simplemente funciones normales que están definidas dentro de una clase porque están lógicamente relacionadas con esa clase.
31.4.1) Definir métodos estáticos
Creamos métodos estáticos usando el decorador @staticmethod:
class MathUtils:
@staticmethod
def is_even(number):
"""Check if a number is even"""
return number % 2 == 0
@staticmethod
def is_prime(number):
"""Check if a number is prime (simplified)"""
if number < 2:
return False
for i in range(2, int(number ** 0.5) + 1):
if number % i == 0:
return False
return True
# Llamar métodos estáticos en la clase
print(MathUtils.is_even(4)) # Output: True
print(MathUtils.is_even(7)) # Output: False
print(MathUtils.is_prime(17)) # Output: True
print(MathUtils.is_prime(18)) # Output: False
# También se pueden llamar en una instancia (pero es la misma función)
utils = MathUtils()
print(utils.is_even(10)) # Output: TrueLos métodos estáticos no necesitan acceso a datos de instancia o de clase: son funciones utilitarias autocontenidas.
31.4.2) Cuándo usar métodos estáticos vs métodos de clase vs métodos de instancia
Aquí tienes cómo elegir:
class Temperature:
# Variable de clase
absolute_zero_celsius = -273.15
def __init__(self, celsius):
self.celsius = celsius
# Método de instancia: necesita acceso a datos de instancia (self)
def to_fahrenheit(self):
return self.celsius * 9/5 + 32
# Método de clase: necesita acceso a datos de clase (cls)
@classmethod
def get_absolute_zero(cls):
return cls.absolute_zero_celsius
# Método estático: no necesita datos de instancia ni de clase
@staticmethod
def celsius_to_kelvin(celsius):
return celsius + 273.15
@staticmethod
def fahrenheit_to_celsius(fahrenheit):
return (fahrenheit - 32) * 5/9
temp = Temperature(25)
# Método de instancia: usa datos de instancia
print(temp.to_fahrenheit()) # Output: 77.0
# Método de clase: usa datos de clase
print(Temperature.get_absolute_zero()) # Output: -273.15
# Métodos estáticos: solo funciones utilitarias
print(Temperature.celsius_to_kelvin(25)) # Output: 298.15
print(Temperature.fahrenheit_to_celsius(77)) # Output: 25.0Pautas:
- Usa métodos de instancia (instance methods) cuando necesites acceso a atributos de instancia (
self) - Usa métodos de clase (class methods) cuando necesites acceso a atributos de clase o quieras constructores alternativos (
cls) - Usa métodos estáticos (static methods) cuando no necesites acceso a datos de instancia o de clase, pero la función esté lógicamente relacionada con la clase
Nota: Los métodos estáticos podrían ser funciones independientes, pero ponerlos en la clase agrupa la funcionalidad relacionada y evita ensuciar el espacio de nombres global.
| Tipo de método | Primer parámetro | Úsalo cuando |
|---|---|---|
| Método de instancia | self | Necesitas acceso a datos de instancia |
| Método de clase | cls | Necesitas acceso a datos de clase o constructores alternativos |
| Método estático | (ninguno) | Función utilitaria relacionada con la clase |
31.4.3) Ejemplo práctico: utilidades de validación
Los métodos estáticos son geniales para validación y funciones utilitarias:
class User:
def __init__(self, username, password):
if not User.is_valid_username(username):
raise ValueError("Invalid username")
if not User.is_valid_password(password):
raise ValueError("Invalid password")
self.username = username
self._password = password
@staticmethod
def is_valid_username(username):
"""Check if username meets requirements"""
return len(username) >= 3 and username.isalnum()
@staticmethod
def is_valid_password(password):
"""Check if password meets security requirements"""
return len(password) >= 8 and any(c.isdigit() for c in password)
# Estos métodos de validación pueden usarse de forma independiente
print(User.is_valid_username("alice123")) # Output: True
print(User.is_valid_username("ab")) # Output: False
print(User.is_valid_password("pass1234")) # Output: True
# Y pueden usarse en cualquier método de la clase
try:
user = User("ab", "short")
except ValueError as e:
print(f"Error: {e}") # Output: Error: Invalid username31.5) Comprender los métodos especiales (magic methods)
Los métodos especiales (special methods) (también llamados magic methods o métodos dunder porque tienen doble guion bajo) te permiten personalizar cómo se comportan tus objetos con las operaciones integradas de Python. Ya hemos usado __init__, __str__ y __repr__ en el Capítulo 30. Ahora exploraremos muchos más.
31.5.1) Qué hacen los métodos especiales
Los métodos especiales son llamados automáticamente por Python cuando usas cierta sintaxis o funciones integradas:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point({self.x}, {self.y})"
point = Point(3, 4)
# Cuando llamas a print(), Python llama a __str__()
print(point) # Output: Point(3, 4)
# Esto es equivalente a: print(point.__str__())Los métodos especiales te permiten hacer que tus clases se comporten como tipos integrados. Por ejemplo, puedes hacer que tus objetos:
- Soporten operaciones aritméticas (
+,-,*,/) - Sean comparables (
<,>,==) - Funcionen con
len(),ine indexación - Actúen como contenedores o secuencias
31.5.2) Categorías comunes de métodos especiales
Estas son las principales categorías de métodos especiales:
Representación de cadena (cómo se muestran los objetos):
__str__()- paraprint()ystr()__repr__()- para el REPL yrepr()
Comparación (comparar objetos):
__eq__()- para==__ne__()- para!=__lt__()- para<__le__()- para<=__gt__()- para>__ge__()- para>=
Aritmética (operaciones matemáticas):
__add__()- para+__sub__()- para-__mul__()- para*__truediv__()- para/
Contenedor/Secuencia (comportamiento tipo colección):
__len__()- paralen()__contains__()- parain__getitem__()- para indexaciónobj[key]__setitem__()- para asignaciónobj[key] = value
Exploraremos estos en detalle en las siguientes secciones.
31.6) Ejemplo 1: Interfaz de colección (len, contains)
Vamos a crear una clase que gestione una colección de elementos y hacer que funcione con la función integrada len() y el operador in de Python.
31.6.1) Implementar len para len()
El método especial __len__() se llama cuando usas len() en tu objeto:
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def __len__(self):
"""Devolver el número de elementos en el carrito"""
return len(self.items)
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
cart.add_item("Orange")
# len() llama a __len__()
print(len(cart)) # Output: 3Sin __len__(), llamar a len(cart) lanzaría un TypeError. Al implementarlo, nuestro ShoppingCart funciona igual que las colecciones integradas.
31.6.2) Implementar contains para el operador in
El método especial __contains__() se llama cuando usas el operador in:
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
def __contains__(self, item):
"""Comprobar si un elemento está en el carrito"""
return item in self.items
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
# El operador in llama a __contains__()
print("Apple" in cart) # Output: True
print("Orange" in cart) # Output: FalseAhora nuestro carrito soporta la sintaxis natural de Python para comprobación de pertenencia.
31.6.3) Construir una clase de colección más completa
Vamos a crear una clase de colección más realista que haga seguimiento de las notas de estudiantes:
class GradeBook:
def __init__(self):
self.grades = {} # student_name: list of grades
def add_grade(self, student, grade):
"""Añadir una nota para un estudiante"""
if student not in self.grades:
self.grades[student] = []
self.grades[student].append(grade)
def __len__(self):
"""Devolver el número de estudiantes"""
return len(self.grades)
def __contains__(self, student):
"""Comprobar si un estudiante tiene alguna nota"""
return student in self.grades
def get_average(self, student):
"""Obtener la nota media de un estudiante"""
if student not in self:
return None
grades = self.grades[student]
return sum(grades) / len(grades)
def __str__(self):
return f"GradeBook with {len(self)} students"
gradebook = GradeBook()
gradebook.add_grade("Alice", 85)
gradebook.add_grade("Alice", 90)
gradebook.add_grade("Bob", 78)
gradebook.add_grade("Bob", 82)
gradebook.add_grade("Bob", 88)
print(gradebook) # Output: GradeBook with 2 students
print(len(gradebook)) # Output: 2
print("Alice" in gradebook) # Output: True
print("Carol" in gradebook) # Output: False
print(f"Alice's average: {gradebook.get_average('Alice')}") # Output: Alice's average: 87.5
print(f"Bob's average: {gradebook.get_average('Bob')}") # Output: Bob's average: 82.66666666666667Fíjate en que get_average() usa if student not in self: esto llama a nuestro método __contains__(), haciendo que el código se lea de forma natural.
31.7) Ejemplo 2: Sobrecarga de operadores (add, eq, lt)
La sobrecarga de operadores (operator overloading) significa definir qué hacen operadores como +, == y < para tus clases personalizadas. Esto hace que tus objetos funcionen de forma natural con la sintaxis de Python.
31.7.1) Implementar add para la suma
El método especial __add__() se llama cuando usas el operador +:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Sumar dos vectores"""
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
# El operador + llama a __add__()
v3 = v1 + v2
print(v3) # Output: Vector(4, 6)Cuando Python ve v1 + v2, llama a v1.__add__(v2). El método __add__() del operando izquierdo recibe el operando derecho como argumento.
31.7.2) Implementar eq para igualdad
El método especial __eq__() se llama cuando usas el operador ==:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
"""Comprobar si dos vectores son iguales"""
return self.x == other.x and self.y == other.y
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(1, 2)
v3 = Vector(3, 4)
# El operador == llama a __eq__()
print(v1 == v2) # Output: True
print(v1 == v3) # Output: FalseSin __eq__(), Python compara identidad de objetos (si son el mismo objeto en memoria), no sus valores. Con __eq__(), definimos qué significa igualdad para nuestra clase.
31.7.3) Implementar operadores de comparación
Vamos a implementar operadores de comparación para una clase Money:
class Money:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
"""Comprobar si los importes son iguales"""
return self.amount == other.amount
def __lt__(self, other):
"""Comprobar si este importe es menor que el otro"""
return self.amount < other.amount
def __le__(self, other):
"""Comprobar si este importe es menor o igual que el otro"""
return self.amount <= other.amount
def __gt__(self, other):
"""Comprobar si este importe es mayor que el otro"""
return self.amount > other.amount
def __ge__(self, other):
"""Comprobar si este importe es mayor o igual que el otro"""
return self.amount >= other.amount
def __str__(self):
return f"${self.amount:.2f}"
price1 = Money(10.50)
price2 = Money(15.75)
price3 = Money(10.50)
print(price1 == price3) # Output: True
print(price1 < price2) # Output: True
print(price1 <= price3) # Output: True
print(price2 > price1) # Output: True
print(price2 >= price1) # Output: True31.7.4) Manejar incompatibilidades de tipo en operadores
Al implementar operadores, deberías manejar casos donde el otro operando no es del tipo esperado:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Sumar dos vectores o sumar un escalar a ambos componentes"""
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
elif isinstance(other, (int, float)):
return Vector(self.x + other, self.y + other)
else:
return NotImplemented # Permitir que Python pruebe other.__radd__(self)
def __eq__(self, other):
if not isinstance(other, Vector):
return False
return self.x == other.x and self.y == other.y
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Output: Vector(4, 6) (vector addition)
print(v1 + 5) # Output: Vector(6, 7) (scalar addition)
print(v1 == v2) # Output: False
print(v1 == "not a vector") # Output: False (no error)Devolver NotImplemented (una constante especial integrada) le dice a Python que intente la operación reflejada en el otro operando. Esto es importante para que los operadores funcionen correctamente con distintos tipos.
31.8) Ejemplo 3: Acceso a secuencias (getitem, setitem)
Los métodos especiales __getitem__() y __setitem__() te permiten usar sintaxis de indexación (obj[key]) con tus clases personalizadas. Esto hace que tus objetos se comporten como listas, diccionarios u otras secuencias.
31.8.1) Implementar getitem para indexación
El método __getitem__() se llama cuando usas corchetes para acceder a un elemento:
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
"""Obtener una canción por índice"""
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
# La indexación llama a __getitem__()
print(playlist[0]) # Output: Song A
print(playlist[1]) # Output: Song B
print(playlist[-1]) # Output: Song C (negative indexing works!)Como delegamos en self.songs[index], todas las funcionalidades de indexación de listas funcionan automáticamente: índices positivos, índices negativos e incluso lanzar IndexError para índices inválidos.
31.8.2) Soportar slicing con getitem
El mismo método __getitem__() también maneja el slicing:
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
"""Obtener una canción por índice o slice"""
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
playlist.add_song("Song D")
# El slicing también llama a __getitem__()
print(playlist[1:3]) # Output: ['Song B', 'Song C']
print(playlist[:2]) # Output: ['Song A', 'Song B']
print(playlist[::2]) # Output: ['Song A', 'Song C']Cuando usas slicing, Python pasa un objeto slice a __getitem__(). Al delegar en self.songs[index], soportamos automáticamente toda la sintaxis de slices.
31.8.3) Implementar setitem para asignación
El método __setitem__() se llama cuando asignas a un índice:
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
return self.songs[index]
def __setitem__(self, index, value):
"""Reemplazar una canción en un índice específico"""
self.songs[index] = value
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
print(playlist[1]) # Output: Song B
# La asignación llama a __setitem__()
playlist[1] = "New Song B"
print(playlist[1]) # Output: New Song B31.8.4) Hacer que los objetos sean iterables con getitem
Un efecto secundario interesante: si implementas __getitem__() con índices enteros empezando en 0, tu objeto automáticamente se vuelve iterable:
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
# ¡Los bucles for funcionan automáticamente!
for song in playlist:
print(song)
# Output:
# Song A
# Song B
# Song CPython intenta iterar llamando a __getitem__(0), luego __getitem__(1), y así sucesivamente hasta que obtiene un IndexError. Este es un protocolo de iteración más antiguo: aprenderemos sobre el protocolo moderno de iteradores en el Capítulo 35.
31.8.5) Acceso tipo diccionario con claves de cadena
__getitem__() y __setitem__() funcionan con cualquier tipo de clave, no solo enteros:
class ScoreBoard:
def __init__(self):
self.scores = {}
def __getitem__(self, player_name):
"""Obtener la puntuación de un jugador"""
return self.scores.get(player_name, 0)
def __setitem__(self, player_name, score):
"""Establecer la puntuación de un jugador"""
self.scores[player_name] = score
def __contains__(self, player_name):
return player_name in self.scores
def __len__(self):
return len(self.scores)
scoreboard = ScoreBoard()
# Establecer puntuaciones usando claves de cadena
scoreboard["Alice"] = 100
scoreboard["Bob"] = 85
# Actualizar una puntuación
scoreboard["Alice"] = 120
# Obtener puntuaciones
print(scoreboard["Alice"]) # Output: 120
print(scoreboard["Bob"]) # Output: 85
print(scoreboard["Carol"]) # Output: 0
print("Alice" in scoreboard) # Output: True
print(len(scoreboard)) # Output: 2Este capítulo te ha mostrado cómo crear clases sofisticadas que se integran perfectamente con la sintaxis de Python. Al implementar variables de clase, properties, métodos de clase, métodos estáticos y métodos especiales, puedes hacer que tus clases personalizadas se comporten como tipos integrados. En el Capítulo 32, exploraremos la herencia y el polimorfismo, que te permiten construir jerarquías de clases relacionadas que comparten y amplían comportamiento.