Métodos de instancia
En resumen: Un método de instancia es una función de la clase cuyo primer parámetro es self y que trabaja con los datos de ese objeto, y __str__ dice cómo se imprime el objeto.
Problema
Cada uno retira a su manera
El banco tiene una clase Account con un titular y un saldo. En el programa se retira dinero en dos lugares. El cajero comprueba que haya dinero suficiente. El pago con tarjeta lo escribió otra persona, y no comprueba nada:
account = Account("María", 100)
# El cajero comprueba que haya dinero suficiente.if account.balance >= 70: account.balance -= 70
# El pago con tarjeta lo escribió otra persona, y no comprueba nada.account.balance -= 50
print(account.balance)-20La cuenta está por debajo de cero. La regla «no puedes retirar más de lo que tienes» es importante, pero se escribe (o se olvida) por separado en cada lugar que retira.
La idea
Los métodos viven con los datos
La lógica del retiro es parte de la cuenta, así que ponla en la clase Account. Una función escrita en la clase es un método del objeto:
class Account: def __init__(self, owner, balance): self.owner = owner self.balance = balance
def deposit(self, amount): self.balance += amount
def withdraw(self, amount): if amount <= self.balance: self.balance -= amount else: print(f"No hay dinero suficiente: {self.owner}")
account = Account("María", 100)account.withdraw(70)account.withdraw(50)account.deposit(20)print(account.balance)No hay dinero suficiente: María50- El método es un
defcon sangría dentro de la clase. Su primer parámetro esself, como en__init__. - Lo llamas sobre un objeto con un punto:
account.withdraw(70), igual que a los campos. Python poneaccountenselfpor sí solo, así que solo pasas el importe. - Dentro,
self.balancees el saldo de esta cuenta, el objeto que está antes del punto. Elself.es obligatorio: sin él,balanceno es el campo. - La comprobación se escribe una vez. Todo el que retira a través del método pasa por ella.
maria = Account("María", 100)peter = Account("Pablo", 40)maria.withdraw(70)peter.withdraw(70)print(maria.balance, peter.balance)Paso a paso, en palabras
- Dos cuentas, dos objetos. Cada uno tiene su propio saldo.
- maria.withdraw(70): self recibe una flecha al objeto que está antes del punto.
- En el método, self.balance es el saldo de María: 100 - 70 = 30.
- El mismo método, otro objeto: ahora self apunta a Pablo.
- 70 es más que 40: el método imprime un rechazo, y el saldo de Pablo no cambia.
- El código del método existe una sola vez. Los campos son del objeto sobre el que lo llamas.
El pago con tarjeta ya no puede «olvidar» la comprobación si retira con withdraw. Pero todavía puede escribir directamente en el campo. Cómo impedirlo lo verás en la lección «Encapsulamiento».
La idea
Método o función
Las funciones del módulo «Funciones» viven fuera de la clase. Lo reciben todo como parámetros y no tienen un objeto propio. Un método trabaja con los campos del objeto sobre el que se llamó:
def apply_discount(price, percent): return price * (100 - percent) / 100
price = apply_discount(10, 10)account.withdraw(70)No puedes llamar a un método de instancia a través del nombre de la clase. No está claro de qué cuenta retirar:
Account.withdraw(70)TypeError: Account.withdraw() missing 1 required positional argument: 'amount'70 fue a self, y no quedó nada para amount. En realidad, account.withdraw(70) es la forma corta de Account.withdraw(account, 70): el objeto que está antes del punto se convierte en el primer argumento. También hay métodos de la propia clase, sin objeto. Los verás en la lección «Lo que comparten todos los objetos: atributos de clase».
Pruébalo
¿De quién es la cuenta?
¿Qué imprimirá el programa?
maria = Account("María", 100)peter = Account("Pablo", 40)maria.deposit(30)peter.withdraw(10)print(maria.balance, peter.balance)Cada objeto tiene su propio saldo. maria.deposit(30) cambia solo el saldo de María, y peter.withdraw(10) cambia solo el de Pablo. Pablo tiene 40, así que retirar 10 sale bien.
Pruébalo
Encuentra el error
account.withdraw(30) detiene el programa con un error. ¿Qué línea tiene el error?Elige la línea con el error.
Sin self., el nombre balance es una variable normal, y el método no tiene ninguna variable así:
NameError: name 'balance' is not definedEl campo es self.balance. La línea siguiente lo escribe bien, así que el error está solo en la comprobación.
La idea
Un método también puede responder
Un método de instancia puede devolver un resultado, igual que las funciones del módulo «Funciones». Un producto conoce su precio y su cantidad, así que puede decirte lo que cuesta:
class Product: # los campos y el constructor, como antes
def total(self): return self.price * self.quantity
cart = [ Product("Pan", 1.20, 2), Product("Leche", 2.10, 1), Product("Café", 7.50, 1),]amount = 0for p in cart: amount += p.total()print(f"Total: {amount} €")Total: 12.0 €Compáralo con la primera lección del módulo: allí la suma se calculaba en el código de debajo de la clase a partir de los campos. Ahora ese código solo le pregunta a cada producto. Si mañana se añade un descuento por cantidad, cambias solo total().
La idea
¿Cómo se ve el objeto como texto?
Si imprimes un producto directamente, obtienes algo inútil:
coffee = Product("Café", 7.50, 1)print(coffee)<__main__.Product object at 0x7f3a2c1b2d10>Python imprime el nombre de la clase y la dirección del objeto en la memoria, que cambia en cada ejecución. No te dice nada del producto.
La clase puede decir cómo debe mostrarse. El método se llama __str__, y devuelve un texto:
class Product: # los campos y el constructor, como antes
def total(self): return self.price * self.quantity
def __str__(self): return f"{self.name} x{self.quantity}: {self.total()} €"
coffee = Product("Café", 7.50, 1)bread = Product("Pan", 1.20, 2)print(coffee)print(f"En el carrito: {bread}")Café x1: 7.5 €En el carrito: Pan x2: 2.4 €No lo llamas tú. print(coffee) y el f-string f"En el carrito: {bread}" lo llaman por ti cuando el objeto tiene que convertirse en texto. Los dos guiones bajos delante y detrás significan justo eso: el método es para Python, y Python lo llama por sí solo en el momento adecuado, como __init__ al crear el objeto.
Pero con + solo puedes unir texto con texto. "En el carrito: " + bread se detiene con
TypeError: can only concatenate str (not "Product") to strUsa un f-string o "En el carrito: " + str(bread): str() también llama a __str__.
Más precisamente, para curiosos
Además de __str__ existe __repr__: el texto para el programador. Una lista lo usa cuando imprimes la lista, así que sin __repr__, print(cart) sigue dando <__main__.Product object at …>. Por convención, __repr__ se parece al código que crea el objeto:
class Product: # los campos y el constructor, como antes
def __repr__(self): return f'Product("{self.name}", {self.price}, {self.quantity})'
cart = [Product("Café", 7.50, 1), Product("Pan", 1.20, 2)]print(cart)[Product("Café", 7.5, 1), Product("Pan", 1.2, 2)]Pruébalo
Un libro en una frase
¿Qué imprimirá el programa?
class Book: def __init__(self, title, author): self.title = title self.author = author
def __str__(self): return f"{self.title} de {self.author}"
book = Book("Rayuela", "Julio Cortázar")print(f"Leyendo: {book}")f"Leyendo: {book}" tiene que convertir book en texto. Así que se llama al __str__ del libro, y devuelve el título y el autor.Ejercicio
Objetos que hacen algo
Resuelve las tareas en tu editor. Toma las clases de las lecciones anteriores y añádeles métodos.
Person: un métodointroduce()que imprime el nombre y la edad.SimpleCar: un métodoaccelerate()que aumenta la velocidad en 10. Ahora el código de debajo de la clase no toca la velocidad directamente.DanceMove: un métodoperform()que «ejecuta» el paso.Book: un métodoread()que imprime «Leyendo [título] de [autor]».
Crea un objeto de cada clase y llama a sus métodos:
Hola, soy María y tengo 34 añosVelocidad tras acelerar: 10Ejecuto moonwalk durante 5 segundosLeyendo Cien años de soledad de Gabriel García MárquezEjercicio
Un coche con combustible
Una clase Car con una marca, una velocidad, combustible (en litros) y un kilometraje. El constructor recibe la marca y el combustible. La velocidad y el kilometraje empiezan en 0.
accelerate()aumenta la velocidad en 10.refuel(liters)añade combustible, pero en el depósito caben como mucho 50 litros.drive(distance)gasta 6 litros cada 100 km y suma la distancia al kilometraje. Si no hay combustible suficiente, imprime un mensaje y el coche no se mueve.__str__()devuelve la marca, el kilometraje y el combustible (el combustible con una cifra después del punto:{self.fuel:.1f}).
car = Car("Toyota", 20)car.accelerate()car.accelerate()print(f"Velocidad: {car.speed}")car.drive(100)print(car)car.drive(300)car.refuel(40)print(car)car.drive(300)print(car)Velocidad: 20Toyota: 100 km, 14.0 lNo hay combustible suficiente para 300 kmToyota: 100 km, 50.0 lToyota: 400 km, 32.0 ldistance * 6 / 100. En Python, / siempre da un número decimal, así que para 30 km obtienes 1,8 litros.Más tareas
- Descuento. Añade a
Productun métodoapply_discount(percent)que baja el precio del producto. Compáralo con la función del módulo «Funciones» que devolvía un precio nuevo. - Una cuenta con historial. Añade a
Accountun contador de operaciones: cada depósito y cada retiro que sale bien lo aumenta en 1. Un método__str__()devuelve el titular, el saldo y el número de operaciones.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Funciones · Parámetros y return · Clases y objetos · Constructores y self
- Continúa en: Encapsulamiento · Lo que comparten todos los objetos: atributos de clase · Herencia · Polimorfismo
- Ejercicios sobre el tema: Clases y objetos