Dos cosas sobre __init__ y self sorprenden a quien viene de otro lenguaje, y las dos se entienden mejor cuando ves lo que Python está haciendo en realidad.
self es un parámetro, no una palabra reservada
Cuando llamas d.speak(), Python llama Dog.speak(d). La instancia se pasa como primer argumento, y self es solo el nombre que le damos.
# self is a parameter name, not a keyword
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says woof"
d = Dog('Rex')
print(d.speak())
print(Dog.speak(d)) # the same call, written out
Imprime:
Rex says woof
Rex says woof
Las dos líneas son la misma llamada. d.speak() es la forma corta.
Puedes comprobar que self es solo una convención:
# proof that self is only a convention
class Cat:
def __init__(whatever, name):
whatever.name = name
def speak(this):
return f"{this.name} says meow"
print(Cat('Mia').speak())
Imprime:
Mia says meow
Funciona. Nunca lo escribas así: todo el que lee Python espera self, y romper esa convención cuesta más de lo que podría ganar. Pero saber que es un parámetro explica por qué tienes que escribirlo en la firma de cada método, algo que de otro modo parece ruido.
__init__ no crea el objeto
El nombre sugiere un constructor. No lo es. Para cuando __init__ corre, el objeto ya existe: __init__ solo lo llena.
# __init__ does not create the object, it fills one in
class Tracked:
def __new__(cls, *args):
print(' __new__ ran, object does not exist yet')
return super().__new__(cls)
def __init__(self, value):
print(' __init__ ran, self already exists:', type(self).__name__)
self.value = value
t = Tracked(5)
print('value =', t.value)
Imprime:
__new__ ran, object does not exist yet
__init__ ran, self already exists: Tracked
value = 5
__new__ es el constructor de verdad. Casi nunca vas a escribir uno: importa para los tipos inmutables y las metaclases, y en ningún otro lado. Pero explica el nombre: __init__ inicializa, no crea.
Los atributos de clase son compartidos
Un atributo asignado en el cuerpo de la clase pertenece a la clase. Existe uno solo, sin importar cuántas instancias crees.
# instance attributes vs class attributes
class Counter:
total = 0 # shared by every instance
def __init__(self):
self.count = 0 # one per instance
def tick(self):
self.count += 1
Counter.total += 1
a, b = Counter(), Counter()
a.tick(); a.tick(); b.tick()
print('a.count =', a.count, '| b.count =', b.count, '| Counter.total =', Counter.total)
Imprime:
a.count = 2 | b.count = 1 | Counter.total = 3
count es por instancia. total es un único número para toda la clase.
La parte que hace tropezar a la gente
Leer un atributo de clase a través de una instancia funciona. Asignar a través de una instancia no actualiza la clase: crea un atributo de instancia nuevo que lo tapa.
# assigning through the instance shadows the class attribute
a.total = 99
print('a.total =', a.total, '| b.total =', b.total, '| Counter.total =', Counter.total)
print('a has its own:', 'total' in a.__dict__, '| b does not:', 'total' in b.__dict__)
Imprime:
a.total = 99 | b.total = 3 | Counter.total = 3
a has its own: True | b does not: False
Ahora a tiene su propio total. b y la clase siguen compartiendo el original. Por eso Counter.total += 1 dentro de tick está escrito contra la clase y no como self.total += 1: esta última leería el valor de la clase, le sumaría uno, y guardaría el resultado en la instancia sin decir nada.
La misma regla explica por qué un atributo de clase mutable es una trampa. Una lista en el cuerpo de la clase la comparten todas las instancias, y self.items.append(x) muta la lista compartida en lugar de taparla. Asigna los atributos mutables en __init__.
Qué recordar
-
d.speak()esDog.speak(d).selfes el primer parámetro, nombrado por convención. -
__init__inicializa un objeto que ya existe.__new__lo crea, y rara vez vas a escribir uno. -
Un atributo de clase es compartido. Leerlo a través de una instancia lo encuentra; asignarlo a través de una instancia lo tapa.
-
Los atributos de clase mutables son compartidos y mutables: ponlos en
__init__.