Saltar al contenido
🐍 Curso de Python
/

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.

⏱ 13 min de lectura

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:

python
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)
Salida en pantalla
Creando la cuenta de Lucía Ferrer
Lucía Ferrer
1500.0

Fí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:

python
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))
Salida en pantalla
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:

python
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)
Salida en pantalla
Lucía Ferrer Ruiz: 1750.00 €
Zaragoza centro

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

python
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)
Salida en pantalla
Banco del Ebro
Banco del Ebro
Banco del Ebro
2

Hay 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.

python
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)
Salida en pantalla
Caja del Norte
Banco del Ebro
Banco del Ebro

La 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 instanciaAtributo de clase
Dónde se defineDentro de __init__, con self.En el cuerpo de la clase
A quién perteneceA cada objeto por separadoA la clase, compartido
Cuándo usarloDatos que cambian de un objeto a otroConstantes y contadores comunes
Ejemploself.saldobanco, 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.

python
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())
Salida en pantalla
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:

python
extranjera = CuentaBancaria(titular="Paul Meier", saldo=900.0, divisa="CHF")

Los errores característicos#