Clases — datos y comportamiento juntos
`class`, `__init__` y `self`, métodos, una forma imprimible con `__repr__`, igualdad con `__eq__`, nivel de clase contra nivel de instancia, y cuándo no se quiere una clase.
¿Cómo quieres aprender?
El problema que vamos a resolver
Hasta ahora hemos guardado los varios datos sobre una cosa en un diccionario.
order = {"name": "pen", "price": 15.0, "quantity": 3}
print(order["nmae"])KeyError: 'nmae'El error se atrapó, que ya es algo — pero solo cuando esa línea se ejecutó. Escondido dentro de una condición, podría aparecer por primera vez un mes después.
Y el problema no es solo la ortografía. Cualquier clave puede ponerse en un diccionario, así que nada en ningún sitio dice qué debe contener una línea de pedido. La función que la totaliza está en otra parte, y nadie comprueba que el diccionario tenga todos los campos antes de llamarla. Los datos están en un sitio, lo que se puede hacer con ellos en otro, y cuál pertenece a cuál existe solo en tu cabeza.
class OrderLine:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def total(self):
return round(self.price * self.quantity, 2)
line = OrderLine("pen", 15.0, 3)
print(line.name)
print(line.total())pen
45.0Una clase es un molde: dice qué contiene una cosa de este tipo y qué se puede hacer con ella.
Al terminar este capítulo podrás
- Escribir una
class, y decir qué hacen__init__yself - Escribir métodos, y decir en qué se diferencian de las funciones ordinarias
- Dar a un objeto una forma imprimible con
__repr__ - Decir qué separa un atributo de clase de un atributo de instancia — y evitar una trampa conocida
- Hacer comparables dos objetos con
__eq__ - Decir cuándo una clase no se quiere
Lo que necesitas primero: JSON y CSV.
Qué es self en realidad
self no es una palabra mágica — es el primer parámetro, y lo que cae en él es el objeto sobre el que se llamó al método.
Escribir line.total() significa "ejecuta el método total de OrderLine, y dale line como self". Por eso self.price dentro da el precio de esa línea en particular.
Olvida self y el mensaje resulta confuso al principio:
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())TypeError: OrderLine.shout() takes 0 positional arguments but 1 was given"1 was given" — pero los paréntesis estaban vacíos. Lo que se dio fue line mismo, en silencio, y shout no dejó sitio para recibirlo.
__init__ también es un método ordinario, con un nombre especial: Python lo llama justo después de crear el objeto. Dentro, self.name = name establece los atributos.
Un error ortográfico da un error más claro
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(line.nmae)AttributeError: 'OrderLine' object has no attribute 'nmae'Para ser honesto: una clase no atrapa este error temprano. Un AttributeError, igual que un KeyError, aparece solo cuando la línea se ejecuta. Escondido dentro de una condición, este también podría aparecer por primera vez un mes después.
Entonces, ¿dónde está la ganancia? En tres sitios.
- El error dice más. El mensaje nombra la clase —
'OrderLine' object— así que ves en qué tipo de cosa se buscó el nombre equivocado. - Lo que debe estar ahí está escrito en un sitio. Leer
__init__muestra qué atributos tiene unOrderLine. Un diccionario no tiene tal lista. - Tu editor puede ayudar. Un editor lee la clase y sugiere nombres de atributos cuando escribes
line., y muchos editores subrayan un nombre desconocido — antes de que el código se ejecute. Con claves de diccionario un editor normalmente no puede hacer esto.
Recuerda también el límite: un error ortográfico no se atrapa al escribir. Igual que order["nmae"] = 5 crea una clave nueva en silencio, line.nmae = 5 crea un atributo nuevo en silencio.
Y lo que hay dentro se puede mirar:
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(type(line))
print(isinstance(line, OrderLine))
print(line.__dict__)<class '__main__.OrderLine'>
True
{'name': 'pen'}__dict__ muestra que realmente hay un diccionario dentro del objeto. La clase no quitó el diccionario — le dio un nombre y una forma.
__repr__ — qué se ve al imprimir
Sin __repr__:
class OrderLine:
def __init__(self, name):
self.name = name
print(OrderLine("pen"))<__main__.OrderLine object at 0x79b769f03830>El número al final es una dirección de memoria, y difiere en cada ejecución — verás un número distinto en tu pantalla. Al depurar no sirve de nada.
class OrderLine:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"OrderLine(name={self.name!r}, price={self.price!r})"
line = OrderLine("pen", 15.0)
print(line)
print([line, line])OrderLine(name='pen', price=15.0)
[OrderLine(name='pen', price=15.0), OrderLine(name='pen', price=15.0)]La segunda línea es el beneficio real: también funciona dentro de una lista. Cuando Python imprime una lista llama al __repr__ de cada elemento, no a su __str__. Por eso, de los dos, __repr__ es el que hay que escribir primero.
La convención es devolver una cadena que parezca el código que construyó el objeto. Usar !r pone comillas alrededor de las cadenas de dentro — por la misma razón que en el capítulo 24.
Igualdad — el comportamiento por defecto no es lo que quieres
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)False
TrueLos dos objetos guardan lo mismo, y aun así == da False. Porque la igualdad por defecto es identidad — == se comporta igual que is, preguntando "¿es este exactamente el mismo objeto?".
Cambiarlo significa decir qué es ser igual:
class OrderLine:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a is b)True
FalseAhora == dice "parecido" y is dice "el mismo" — la distinción del capítulo 21, esta vez en un tipo propio.
Una precaución: escribir __eq__ deja el objeto sin hash por defecto, así que no puede ir en un conjunto ni ser clave de diccionario. El dataclass del próximo capítulo hace las dos cosas en una línea.
Nivel de clase y nivel de instancia
class Basket:
TAX_RATE = 0.15
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.items is b.items)['pen', 'bag']
['pen', 'bag']
TrueDos cestos separados, pero solo una lista.
¿Lo reconoces? Esta es la trampa del valor por defecto mutable del capítulo 21, en un sitio nuevo. El items = [] justo debajo de la línea class se ejecuta una vez, cuando se crea la clase — no una vez por objeto. Así que la lista pertenece a la clase, y todas las instancias la comparten.
La solución es del mismo tipo: lo que pertenezca a cada objeto, constrúyelo dentro de __init__.
class Basket:
TAX_RATE = 0.15
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.TAX_RATE, b.TAX_RATE)['pen']
['bag']
0.15 0.15TAX_RATE se quedó a nivel de clase, y con razón — es lo mismo para todos los cestos, y no pretendemos cambiarlo. Pero ten en cuenta que Python no te impide cambiarlo: Basket.TAX_RATE = 0.2 no lanza error. El nombre en mayúsculas es solo una convención que dice al lector "trata esto como una constante, no lo cambies". La regla en una línea: constantes a nivel de clase; todo lo que cambia en __init__.
Cuándo no escribir una clase
Una vez que has aprendido clases, es tentador hacer todo una clase. Unas cuantas señales de que la clase no hace falta:
La clase solo tiene __init__ y un método. Eso es una función, partida en dos sin razón.
Ningún método, solo datos. El dataclass del próximo capítulo está hecho exactamente para esto, en muchas menos líneas.
El objeto nunca cambia y guarda una sola cosa. Una tupla, o un valor simple, basta.
Una clase es la herramienta adecuada donde los datos y el comportamiento de verdad pertenecen juntos: algunos datos que cambian, y unas cuantas operaciones que siempre se ejecutan sobre esos datos.
Un ejemplo completo
"""An order built from two small classes."""
TAX_RATE = 0.15
class OrderLine:
"""One item on an order: what it is, what it costs, how many."""
def __init__(self, name, price, quantity):
if quantity < 1:
raise ValueError(f"quantity must be at least 1: {quantity}")
self.name = name
self.price = price
self.quantity = quantity
def total(self):
"""Price for this line, tax included."""
return round(self.price * self.quantity * (1 + TAX_RATE), 2)
def __repr__(self):
return f"OrderLine({self.name!r}, {self.price!r}, {self.quantity!r})"
class Order:
"""A named collection of lines, and the questions you ask about it."""
def __init__(self, customer):
self.customer = customer
self.lines = []
def add(self, name, price, quantity):
self.lines.append(OrderLine(name, price, quantity))
return self
def total(self):
return round(sum(line.total() for line in self.lines), 2)
def largest(self):
"""The most expensive line, or None for an empty order."""
if not self.lines:
return None
return max(self.lines, key=lambda line: line.total())
def __repr__(self):
return f"Order({self.customer!r}, {len(self.lines)} lines)"
def main():
order = Order("rafi")
order.add("pen", 15.0, 3)
order.add("bag", 850.0, 1)
order.add("ink", 120.0, 2)
for line in order.lines:
print(f"{line.name:<6} {line.total():>9.2f}")
print(f"{'total':<6} {order.total():>9.2f}")
print()
print(order)
print("largest:", order.largest())
try:
order.add("clip", 5.0, 0)
except ValueError as err:
print("rejected:", err)
if __name__ == "__main__":
main()pen 51.75
bag 977.50
ink 276.00
total 1305.25
Order('rafi', 3 lines)
largest: OrderLine('bag', 850.0, 1)
rejected: quantity must be at least 1: 0Cuatro cosas que merece la pena ver.
__init__ valida. Con quantity < 1 el objeto nunca se crea. Esta es la cosa más útil que ofrece una clase: tener un OrderLine significa que su cantidad es al menos uno — en ningún sitio donde se use hace falta comprobarlo otra vez. Un diccionario no puede dar esa garantía.
Order no sabe cómo se calcula el precio de una línea. Llama a line.total() y ya está. Cómo se aplica el impuesto es asunto de OrderLine, y cambiarlo cambia un solo sitio.
Las dos clases tienen __repr__, y la salida de largest() muestra por qué importa. Sin él, esa última línea habría sido una dirección de memoria, que no te dice nada.
add se devuelve a sí mismo (return self). Eso hace posible order.add(...).add(...). No se usa aquí, a propósito — tener la opción y estirar cada línea con ella son cosas distintas.
Cuando falla
TypeError: ... takes 0 positional arguments but 1 was given Al método le falta self. Añade self como primer parámetro.
AttributeError: 'X' object has no attribute 'y' Un error ortográfico, o self.y nunca se estableció en __init__. print(obj.__dict__) muestra qué hay realmente.
TypeError: X() takes no arguments No hay __init__, o el nombre está mal escrito (__init__, no __int__, dos guiones bajos a cada lado).
Todos los objetos comparten una lista La lista está a nivel de clase. Muévela dentro de __init__.
Dos objetos guardan lo mismo y == da False La igualdad por defecto es identidad. Escribe __eq__ — o usa el dataclass del próximo capítulo.
Después de escribir __eq__ no entra en un conjunto Escribir __eq__ quita el hash por defecto. dataclass(frozen=True) da las dos.
Al imprimir sale <__main__.X object at 0x...> No hay __repr__.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
shout() se llama sin argumentos. ¿Qué pasa?
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())- AUn `TypeError` — toma uno, y se dieron cero
- BUn `TypeError` — toma cero, y se dio uno
- CUn `AttributeError`
- DSe imprime `hello`
Dos cestos separados, un elemento cada uno. ¿Qué se imprime?
class Basket:
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)- A['pen', 'bag'] []
- B['pen'] ['pen', 'bag']
- C['pen'] ['bag']
- D['pen', 'bag'] ['pen', 'bag']
Los dos objetos guardan el mismo nombre. ¿Qué se imprime?
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)- AFalse True
- BUn `TypeError`
- CFalse False
- DTrue True
Para responder necesitas una cuenta
Inicia sesión para comprobar tus respuestas
Las preguntas están arriba, y resolverlas mentalmente es lo que de verdad importa. Inicia sesión para ver las respuestas, las explicaciones y las pistas de tres niveles.
Tu turno
Escribe dos clases.
Book — construida con title, author y pages. Lanza un ValueError cuando pages es menor que uno. Dale un __repr__, y un __eq__ que diga que dos libros son iguales cuando el título y el autor coinciden.
Shelf — recoge libros, y ofrece add(book), total_pages(), longest(), y by_author(name) devolviendo una lista.
Luego cinco experimentos:
- Mueve
books = []deShelffuera de__init__y ponlo bajo la líneaclass. Haz dos estanterías separadas, añade un libro a una, luego imprime la otra. ¿Qué ves? - Borra el
__repr__e imprime una lista de libros. ¿Cuánto más difícil es depurar? - Borra
__eq__y compara dos libros con el mismo título y autor. - Quita
selfde un método y llámalo. ¿Cuántos argumentos dice el mensaje que se dieron? - Crea un
Booky escribebook.pagse = 100(mal escrito a propósito). ¿Hay error? Luegoprint(book.__dict__)— ¿qué ves?
Esta última importa porque muestra el límite de una clase. Un nombre equivocado lanza error al leer, no al escribir — asignar un atributo nuevo siempre está permitido. El próximo capítulo muestra que las anotaciones de tipo y un comprobador de tipos pueden cerrar esa brecha también.
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Inicia sesión para hacer el quiz