Hacer que tus objetos se comporten como tipos incorporados
Imprime un objeto simple y Python muestra algo como <__main__.Position object at 0x7f3a>, y Position("A", 1) == Position("A", 1) es False porque por defecto dos objetos son iguales solo si son exactamente el mismo objeto. Los métodos especiales (llamados dunder, por los guiones bajos dobles) permiten que tu clase se integre en la sintaxis de Python: __repr__ controla cómo se muestra un objeto, __eq__ controla ==, __add__ controla +, __lt__ controla <, etc. Nunca los llamas directamente; Python los llama cuando usas repr(x), a == b, a + b o sorted(items).
class Plain:
def __init__(self, cents):
self.cents = cents
a, b = Plain(500), Plain(500)
print(a == b) # False: different objects
print(a.cents == b.cents) # True
print("object at 0x" in str(a)) # True: the default display is unhelpful__repr__: mostrar el objeto con claridad
__repr__ devuelve una cadena que describe el objeto para desarrolladores. Por convención se parece al código que lo construiría: Money(1999, 'USD'). Las listas, el depurador y los mensajes de error la usan, lo que hace los errores mucho más legibles. print(x) y str(x) también usan __repr__ salvo que definas además __str__ para un texto más bonito de cara al usuario. Debe devolver la cadena, no imprimirla, y {self.currency!r} en una f-string da la forma entrecomillada de un atributo de texto.
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __repr__(self):
return f"Money({self.cents}, {self.currency!r})"
price = Money(1999)
print(price) # Money(1999, 'USD')
print([Money(5), Money(7, "EUR")]) # [Money(5, 'USD'), Money(7, 'EUR')]__eq__ y __hash__: ¿qué significa igual?
__eq__(self, other) decide self == other. Compara los campos que definen la identidad y, cuando other no es del tipo esperado, devuelve el valor especial NotImplemented (no False): Python prueba entonces el otro lado y por fin responde False él mismo, así que Money(1) == 5 es seguro. Definir __eq__ desactiva el hash: los objetos ya no pueden ser miembros de un conjunto ni claves de diccionario a menos que definas también __hash__ de forma coherente, por ejemplo hash((self.cents, self.currency)).
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented # let Python decide
return self.cents == other.cents and self.currency == other.currency
def __hash__(self):
return hash((self.cents, self.currency))
print(Money(500) == Money(500)) # True
print(Money(500) == Money(500, "EUR")) # False
print(Money(500) == 500) # False, no error
print(len({Money(1), Money(1)})) # 1: usable in sets nowOperadores y orden
__add__(self, other) da + a tu clase. Devuelve un objeto nuevo en lugar de cambiar self, y rechaza lo absurdo: sumar dólares y euros debe lanzar ValueError, sumar un número suelto debe devolver NotImplemented. __lt__(self, other) da <, y con eso basta para que sorted(), min() y max() ordenen tus objetos, porque solo necesitan preguntar «¿a es menor que b?». Añade también __le__, __gt__, __ge__, o deja que functools.total_ordering los derive.
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __repr__(self):
return f"Money({self.cents}, {self.currency!r})"
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if other.currency != self.currency:
raise ValueError("cannot add different currencies")
return Money(self.cents + other.cents, self.currency)
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.cents < other.cents
total = Money(150) + Money(275)
print(total) # Money(425, 'USD')
print(sorted([Money(30), Money(10), Money(20)])) # [Money(10, 'USD'), Money(20, 'USD'), Money(30, 'USD')]
print(sum([Money(1), Money(2)], Money(0))) # Money(3, 'USD')__repr__ debe mostrar todo lo que mira __eq__. No des a + efectos secundarios sorprendentes: debe devolver un valor nuevo y dejar intactos ambos operandos, como los números. Las pruebas del lab comprueban estos puntos: originales sin cambios, monedas distintas rechazadas y tipos sin relación que dan NotImplemented.__repr__devuelve una cadena (comoMoney(1999, 'USD')); no imprime.__eq__compara campos y devuelveNotImplementedpara tipos ajenos; añade__hash__para conservar el hash.__add__devuelve un objeto nuevo;__lt__basta parasorted,minymax.
A continuación: dataclasses, que escriben __init__, __repr__ y __eq__ por ti.
Lab: qué se califica
- Mostrar un objeto Money con claridad con
__repr__25 pts - Decir cuándo dos importes son iguales con
__eq__25 pts - Sumar dos importes con
__add__25 pts - Hacer ordenables los importes con
__lt__25 pts