Ir al contenido
wannacode

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)
Salida
-20

La 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:

bank.py
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)
Salida
No hay dinero suficiente: María
50
  • El método es un def con sangría dentro de la clase. Su primer parámetro es self, como en __init__.
  • Lo llamas sobre un objeto con un punto: account.withdraw(70), igual que a los campos. Python pone account en self por sí solo, así que solo pasas el importe.
  • Dentro, self.balance es el saldo de esta cuenta, el objeto que está antes del punto. El self. es obligatorio: sin él, balance no 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
  1. Dos cuentas, dos objetos. Cada uno tiene su propio saldo.
  2. maria.withdraw(70): self recibe una flecha al objeto que está antes del punto.
  3. En el método, self.balance es el saldo de María: 100 - 70 = 30.
  4. El mismo método, otro objeto: ahora self apunta a Pablo.
  5. 70 es más que 40: el método imprime un rechazo, y el saldo de Pablo no cambia.
  6. 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)
Error
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)
Respuesta

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.

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 = 0
for p in cart:
amount += p.total()
print(f"Total: {amount} €")
Salida
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)
Salida
<__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}")
Salida
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

Error
TypeError: can only concatenate str (not "Product") to str

Usa 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)
Salida
[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}")
Respuesta

Ejercicio

Objetos que hacen algo

Resuelve las tareas en tu editor. Toma las clases de las lecciones anteriores y añádeles métodos.

  1. Person: un método introduce() que imprime el nombre y la edad.
  2. SimpleCar: un método accelerate() que aumenta la velocidad en 10. Ahora el código de debajo de la clase no toca la velocidad directamente.
  3. DanceMove: un método perform() que «ejecuta» el paso.
  4. Book: un método read() que imprime «Leyendo [título] de [autor]».

Crea un objeto de cada clase y llama a sus métodos:

Salida
Hola, soy María y tengo 34 años
Velocidad tras acelerar: 10
Ejecuto moonwalk durante 5 segundos
Leyendo Cien años de soledad de Gabriel García Márquez

Ejercicio

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}).
El programa
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)
Salida
Velocidad: 20
Toyota: 100 km, 14.0 l
No hay combustible suficiente para 300 km
Toyota: 100 km, 50.0 l
Toyota: 400 km, 32.0 l
Pista: distance * 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 Product un método apply_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 Account un 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

1. ¿Qué hace maria.deposit(30)?
2. ¿Qué puede ver un método que una función fuera de la clase no puede?
3. ¿Por qué Account.withdraw(70) se detiene con un TypeError?
4. ¿Cuándo llama Python a __str__ por sí solo?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·