Clases e instancias: qué ocurre al hacer `MiClase()`

Cuando escribes Perro("Rex"), Python no hace una sola cosa —hace cuatro o cinco, en un orden muy preciso. Entender ese orden cambia la forma en que lees y depuras código orientado a objetos, porque ya no es magia: es protocolo.

Una clase en Python no es solo una plantilla sintáctica. Es un objeto real, de tipo type, que vive en el namespace donde fue definido. Cuando el intérprete procesa class Perro:, crea ese objeto-clase y lo asigna al nombre Perro en el scope actual, igual que x = 42 crea un entero y lo asigna. Que las clases sean objetos importa: puedes pasarlas como argumentos, guardarlas en listas, inspeccionarlas en tiempo de ejecución.

Cuando luego escribes Perro("Rex"), estás llamando ese objeto-clase. type define __call__ en las clases, así que la llamada desencadena dos pasos distintos:

  1. Perro.__new__(Perro) — reserva memoria y construye una instancia vacía del tipo Perro. Es un método de clase heredado de object. El resultado es el objeto recién nacido.
  2. instancia.__init__("Rex") — recibe esa instancia (como self) y la inicializa: asigna atributos, valida argumentos, conecta recursos. No crea el objeto; lo configura.

Si __new__ devuelve una instancia de Perro, Python invoca __init__ automáticamente. Si devuelve otra cosa (patrón útil para singletons o inmutables), __init__ no se ejecuta. Para el 99 % del código cotidiano solo tocas __init__, pero saber que __new__ existe explica comportamientos que de otro modo parecen contradictorios.

El tercer punto clave: las instancias no almacenan métodos. Cada instancia tiene un __dict__ con sus atributos propios (nombre, edad, lo que tú definas en __init__). Los métodos viven en el __dict__ de la clase. Cuando haces rex.ladrar(), Python primero busca ladrar en rex.__dict__ —no está—, luego sube a Perro.__dict__ —lo encuentra— y lo envuelve en un bound method que inyecta rex como self. Ese mecanismo de búsqueda se llama descriptor protocol y ocurre en cada acceso a atributo.

class Perro:
    especie = "Canis lupus familiaris"  # atributo de clase, en Perro.__dict__

    def __init__(self, nombre: str, edad: int) -> None:
        # self ya existe; aquí solo lo poblamos
        self.nombre = nombre  # va a rex.__dict__, no a Perro.__dict__
        self.edad = edad

    def presentarse(self) -> str:
        return f"Soy {self.nombre}, tengo {self.edad} años."

    def cumpleanos(self) -> None:
        self.edad += 1  # modifica el __dict__ de esta instancia, no el de la clase


rex = Perro("Rex", 3)
luna = Perro("Luna", 5)

print(rex.presentarse())        # Soy Rex, tengo 3 años.
print(luna.presentarse())       # Soy Luna, tengo 5 años.

# El atributo de clase es compartido; el de instancia es propio
print(rex.especie)              # Canis lupus familiaris
print(Perro.especie)            # mismo objeto, mismo valor

# Verificamos qué hay realmente en cada namespace
print(rex.__dict__)             # {'nombre': 'Rex', 'edad': 3}
print(Perro.__dict__.keys())    # ..., 'especie', '__init__', 'presentarse', 'cumpleanos', ...

# Los métodos no están en la instancia
print("presentarse" in rex.__dict__)    # False
print("presentarse" in Perro.__dict__)  # True

rex.cumpleanos()
print(rex.edad)   # 4
print(luna.edad)  # 5 — luna no se tocó

Desglose del código

especie está definido en el cuerpo de la clase, fuera de cualquier método, así que aterriza en Perro.__dict__. Todas las instancias lo comparten y lo leen por la misma búsqueda dinámica que los métodos. Eso tiene una consecuencia importante: si haces rex.especie = "otro", creas una entrada nueva en rex.__dict__ que sombrea al atributo de clase sin modificarlo —luna.especie sigue siendo el original.

En __init__, self ya es el objeto creado por __new__. Las asignaciones self.nombre = nombre escriben directamente en rex.__dict__. Por eso rex y luna tienen valores independientes para nombre y edad aunque comparten el mismo __init__.

cumpleanos modifica self.edad, que está en el __dict__ de la instancia que llama al método. El rex.edad de luna no existe en su __dict__; Python lo encontraría en la clase si estuviera ahí, pero como es un atributo de instancia, vive solo donde fue creado.

Las dos últimas comprobaciones con in y __dict__ son la forma directa de verificar todo esto en una sesión interactiva. Cuando algo se comporta raro con atributos, vars(objeto) (sinónimo de objeto.__dict__) es tu primera herramienta de diagnóstico.

Errores que debes conocer

Error: usar un objeto mutable como valor por defecto de un atributo de clase en lugar de inicializarlo en __init__, lo que hace que todas las instancias compartan la misma lista u objeto.

# ❌ Wrong
class Carrito:
    items = []  # está en Carrito.__dict__, compartido por todas las instancias

    def agregar(self, item):
        self.items.append(item)  # modifica la lista de la clase, no de la instancia

c1 = Carrito()
c2 = Carrito()
c1.agregar("manzana")
print(c2.items)  # ['manzana'] — sorpresa

# ✅ Right
class Carrito:
    def __init__(self):
        self.items = []  # nueva lista en cada instancia, en su propio __dict__

    def agregar(self, item):
        self.items.append(item)

c1 = Carrito()
c2 = Carrito()
c1.agregar("manzana")
print(c2.items)  # []

Mover la inicialización a __init__ garantiza que cada llamada a Carrito() crea una lista nueva en el __dict__ de esa instancia, sin ninguna conexión con las demás.

Error: olvidar self como primer parámetro en un método, lo que hace que Python pase la instancia como primer argumento posicional y falle con un TypeError confuso.

# ❌ Wrong
class Contador:
    def __init__(self):
        self.valor = 0

    def incrementar():  # falta self
        self.valor += 1

c = Contador()
c.incrementar()  # TypeError: incrementar() takes 0 positional arguments but 1 was given

# ✅ Right
class Contador:
    def __init__(self):
        self.valor = 0

    def incrementar(self):  # Python inyecta la instancia aquí automáticamente
        self.valor += 1

El error dice “1 argumento” porque Python intenta pasar c como self —ese es el mecanismo del bound method en acción, y el mensaje tiene exactamente sentido una vez que sabes que los métodos viven en la clase, no en la instancia.

92

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio