Módulo 1 · Lección 53 de 59
Atributos, __init__ y self
Cómo se inicializan los objetos, qué es self de verdad y en qué se diferencian los atributos de instancia de los de clase.
En la lección anterior se creaban vehículos vacíos y se les iban colgando atributos desde fuera, uno a uno. Eso funciona, pero es frágil: nada obliga a poner todos los datos, nadie sabe cuáles hacen falta y cualquier despiste deja un objeto a medio hacer que fallará más adelante, lejos de donde estaba el error.
__init__ es el sitio donde se escribe, de una vez y para siempre, qué datos necesita un objeto para existir.
El método __init__#
Python llama a __init__ automáticamente justo después de fabricar el objeto. No lo llamas tú nunca:
class CuentaBancaria:
def __init__(self, titular, saldo):
print(f"Creando la cuenta de {titular}")
self.titular = titular
self.saldo = saldo
cuenta = CuentaBancaria("Lucía Ferrer", 1500.0)
print(cuenta.titular)
print(cuenta.saldo)Creando la cuenta de Lucía Ferrer
Lucía Ferrer
1500.0Fíjate en que nunca aparece cuenta.__init__(...) por ningún lado. Al escribir CuentaBancaria("Lucía Ferrer", 1500.0), Python crea un objeto vacío y le pasa esos dos argumentos a __init__ para que lo rellene.
A este método se le llama constructor porque es el que deja el objeto construido y listo para usar. Estrictamente hablando, en Python quien crea el objeto es otro método interno y __init__ solo lo inicializa, pero todo el mundo dice "constructor" y se entiende perfectamente.
Qué es self, sin misterio#
self es el propio objeto. Nada más.
Cuando escribes cuenta.resumen(), Python traduce esa llamada a CuentaBancaria.resumen(cuenta): coge el objeto que hay a la izquierda del punto y lo pasa como primer argumento. Esas dos formas son literalmente equivalentes:
class CuentaBancaria:
def __init__(self, titular, saldo):
self.titular = titular
self.saldo = saldo
def resumen(self):
return f"{self.titular}: {self.saldo:.2f} €"
cuenta = CuentaBancaria("Lucía Ferrer", 1500.0)
print(cuenta.resumen())
print(CuentaBancaria.resumen(cuenta))Lucía Ferrer: 1500.00 €
Lucía Ferrer: 1500.00 €La segunda línea no se escribe nunca en código real, pero explica de golpe dos cosas que suelen dar problemas:
- Por qué al llamar a un método no escribes el primer argumento: ya lo pone Python con el objeto que hay delante del punto.
- Por qué al definirlo sí tienes que escribirlo: la función necesita un nombre para referirse al objeto sobre el que está trabajando.
Dentro de __init__, la línea self.titular = titular significa "guarda en el atributo titular de este objeto el valor que ha llegado por el parámetro titular". Son dos cosas distintas con el mismo nombre: a la izquierda, un atributo permanente; a la derecha, un parámetro que desaparece al terminar el método.
Atributos de instancia#
Los que se crean con self.algo = ... son atributos de instancia: cada objeto tiene los suyos y son independientes.
Se leen y se modifican desde fuera con el punto, como ya sabes:
cuenta = CuentaBancaria("Lucía Ferrer", 1500.0)
cuenta.saldo += 250 # modificar
cuenta.titular = "Lucía Ferrer Ruiz"
cuenta.oficina = "Zaragoza centro" # crear uno nuevo sobre la marcha
print(cuenta.resumen())
print(cuenta.oficina)Lucía Ferrer Ruiz: 1750.00 €
Zaragoza centroQue se puedan crear atributos nuevos desde fuera, como ese oficina, es una libertad que Python te da y que conviene no usar: si un objeto puede tener oficina, dilo en __init__. Lo contrario lleva a objetos que a veces tienen el atributo y a veces no.
Atributos de clase#
Un atributo escrito directamente en el cuerpo de la clase, fuera de cualquier método, pertenece a la clase y lo comparten todas las instancias:
class CuentaBancaria:
banco = "Banco del Ebro"
comision_mensual = 1.50
cuentas_abiertas = 0
def __init__(self, titular, saldo):
self.titular = titular
self.saldo = saldo
CuentaBancaria.cuentas_abiertas += 1
primera = CuentaBancaria("Lucía Ferrer", 1500.0)
segunda = CuentaBancaria("Mateo Arias", 320.0)
print(primera.banco)
print(segunda.banco)
print(CuentaBancaria.banco)
print(CuentaBancaria.cuentas_abiertas)Banco del Ebro
Banco del Ebro
Banco del Ebro
2Hay un solo banco en memoria, y las dos cuentas lo ven. El contador cuentas_abiertas aprovecha justamente eso: como es único, sirve para llevar la cuenta de cuántos objetos se han creado.
La diferencia, demostrada#
Aquí es donde se ve la distinción con claridad. Al asignar sobre la instancia, Python no modifica el atributo de clase: le crea a esa instancia uno propio que lo tapa.
class CuentaBancaria:
banco = "Banco del Ebro"
def __init__(self, titular):
self.titular = titular
primera = CuentaBancaria("Lucía Ferrer")
segunda = CuentaBancaria("Mateo Arias")
primera.banco = "Caja del Norte"
print(primera.banco)
print(segunda.banco)
print(CuentaBancaria.banco)Caja del Norte
Banco del Ebro
Banco del EbroLa regla que sigue Python al leer primera.banco es: primero busca en los atributos de la instancia; si no lo encuentra, sube a la clase. Al escribir, en cambio, siempre escribe en la instancia. Si de verdad quieres cambiarlo para todas, tienes que decirlo: CuentaBancaria.banco = "Caja del Norte".
| Atributo de instancia | Atributo de clase | |
|---|---|---|
| Dónde se define | Dentro de __init__, con self. | En el cuerpo de la clase |
| A quién pertenece | A cada objeto por separado | A la clase, compartido |
| Cuándo usarlo | Datos que cambian de un objeto a otro | Constantes y contadores comunes |
| Ejemplo | self.saldo | banco, comision_mensual |
Valores por defecto en __init__#
__init__ es una función normal, así que admite todo lo que sabes de parámetros: valores por defecto, argumentos con nombre y el resto.
class CuentaBancaria:
def __init__(self, titular, saldo=0.0, divisa="EUR"):
self.titular = titular
self.saldo = saldo
self.divisa = divisa
self.movimientos = []
def resumen(self):
return f"{self.titular}: {self.saldo:.2f} {self.divisa} ({len(self.movimientos)} movimientos)"
nueva = CuentaBancaria("Iván Soto")
veterana = CuentaBancaria("Nuria Gil", 2400.0)
extranjera = CuentaBancaria("Paul Meier", 900.0, "CHF")
print(nueva.resumen())
print(veterana.resumen())
print(extranjera.resumen())Iván Soto: 0.00 EUR (0 movimientos)
Nuria Gil: 2400.00 EUR (0 movimientos)
Paul Meier: 900.00 CHF (0 movimientos)Dos detalles de este ejemplo:
- Los parámetros con valor por defecto van después de los obligatorios, igual que en cualquier función.
self.movimientos = []no es un parámetro. Es un atributo que siempre empieza vacío y que el objeto irá llenando.__init__no está obligado a limitarse a copiar argumentos: puede inicializar lo que haga falta.
Con tres parámetros el orden todavía se recuerda, pero en cuanto crezcan conviene llamar con nombre para que se lea solo:
extranjera = CuentaBancaria(titular="Paul Meier", saldo=900.0, divisa="CHF")