Métodos de instancia, classmethod y staticmethod en Python

Cuando defines una función dentro de una clase, Python te da tres sabores distintos según quién necesita participar en la llamada: la instancia, la clase, o nadie de los dos. Esa distinción no es cosmética —determina qué información tiene disponible el método y, por tanto, qué responsabilidades puede asumir.

El caso que ya conoces es el método de instancia: el primer parámetro se llama self por convención y Python lo rellena automáticamente con el objeto concreto desde el que llamas. Si tienes perro.ladrar(), self es ese perro específico, con todos sus atributos. Es el mecanismo que hace que cada instancia tenga su propio estado.

El @classmethod cambia el receptor: en lugar de la instancia, recibe la propia clase como primer argumento, llamado cls por convención. Cuando llamas Perro.crear_cachorro(), cls es Perro. Lo interesante es que también puedes llamarlo desde una instancia —mi_perro.crear_cachorro()— y cls seguirá siendo la clase, no el objeto. El decorador le dice a Python que rewire el enlace. Esto lo hace ideal para constructores alternativos: métodos que crean instancias a partir de formatos diferentes al __init__ principal.

El @staticmethod corta el vínculo por completo: no recibe self ni cls. Es literalmente una función normal que vive dentro del namespace de la clase por razones organizativas —conceptualmente pertenece ahí, pero no necesita tocar ni la instancia ni la clase. Si te preguntas “¿por qué no es simplemente una función suelta?”, la respuesta es legibilidad y cohesión: agrupar utilidades relacionadas junto a la clase que las usa.

La confusión más común es pensar que los tres son intercambiables con distintos nombres. No lo son. Si necesitas acceder a atributos del objeto, es instancia. Si necesitas la clase (para instanciarla, para acceder a atributos de clase), es classmethod. Si no necesitas ninguno de los dos, es staticmethod. Usar el tipo equivocado no siempre explota de inmediato —pero cuando subclasificas, el classmethod instancia la subclase correctamente mientras que una solución hardcoded con el nombre de la clase no lo hace.

from __future__ import annotations
from datetime import date


class Usuario:
    # Atributo de clase — compartido por todas las instancias
    rol_por_defecto = "lector"

    def __init__(self, nombre: str, email: str, rol: str) -> None:
        self.nombre = nombre
        self.email = email
        self.rol = rol

    # ── Método de instancia ─────────────────────────────────────────────
    def presentarse(self) -> str:
        # self da acceso al estado concreto de este usuario
        return f"Hola, soy {self.nombre} ({self.email}), rol: {self.rol}"

    def cambiar_rol(self, nuevo_rol: str) -> None:
        self.rol = nuevo_rol

    # ── classmethod: constructor alternativo ────────────────────────────
    @classmethod
    def desde_dict(cls, datos: dict) -> Usuario:
        # cls es la clase real en el momento de la llamada.
        # Si subclasificas Usuario, cls será la subclase — no Usuario hardcodeado.
        return cls(
            nombre=datos["nombre"],
            email=datos["email"],
            rol=datos.get("rol", cls.rol_por_defecto),  # accede al atributo de clase
        )

    @classmethod
    def invitado(cls) -> Usuario:
        return cls(nombre="Invitado", email="", rol="invitado")

    # ── staticmethod: utilidad relacionada, sin acceso al objeto/clase ──
    @staticmethod
    def es_email_valido(email: str) -> bool:
        # No necesita saber nada de Usuario; sólo valida el formato básico.
        return "@" in email and "." in email.split("@")[-1]

    @staticmethod
    def generar_nombre_usuario(nombre: str, anio_nacimiento: int) -> str:
        return f"{nombre.lower().replace(' ', '_')}_{anio_nacimiento}"


# ── Uso ─────────────────────────────────────────────────────────────────

# Método de instancia: necesita un objeto concreto
u1 = Usuario("Ana García", "ana@example.com", "editor")
print(u1.presentarse())
# Hola, soy Ana García (ana@example.com), rol: editor

# classmethod como constructor alternativo
datos_json = {"nombre": "Luis Pérez", "email": "luis@example.com"}
u2 = Usuario.desde_dict(datos_json)  # rol queda en "lector" por defecto
print(u2.presentarse())

u3 = Usuario.invitado()
print(u3.presentarse())

# staticmethod: puedes llamarlo sin instancia
print(Usuario.es_email_valido("mal-email"))       # False
print(Usuario.es_email_valido("bien@ejemplo.com")) # True
print(Usuario.generar_nombre_usuario("Ana García", 1990))  # ana_garcía_1990

# También funciona desde una instancia (pero no tiene acceso a u1)
print(u1.es_email_valido("test@test.com"))  # True — idéntico a llamarlo en la clase


# ── La ventaja real de classmethod con herencia ──────────────────────────
class Administrador(Usuario):
    rol_por_defecto = "admin"  # sobreescribe el atributo de clase

a = Administrador.desde_dict({"nombre": "Sofía", "email": "sofia@example.com"})
print(type(a).__name__)  # Administrador — no Usuario
print(a.rol)             # admin — leyó rol_por_defecto de Administrador

Qué está pasando en cada parte

presentarse y cambiar_rol son métodos de instancia clásicos: necesitan self porque su trabajo depende del estado de ese usuario concreto. Sin self no podrían distinguir a Ana de Luis.

desde_dict y invitado son @classmethod porque su trabajo es crear un Usuario. Si usaras return Usuario(...) en lugar de return cls(...), el código funcionaría hasta que alguien herede de Usuario. Con cls, la herencia funciona sola: Administrador.desde_dict(datos) devuelve un Administrador y además lee Administrador.rol_por_defecto. Ese detalle —que cls captura la clase real en el momento de la llamada— es el motivo por el que los constructores alternativos casi siempre deben ser classmethod.

es_email_valido y generar_nombre_usuario no necesitan ni la instancia ni la clase. Viven en Usuario porque conceptualmente forman parte del dominio de usuarios, pero su lógica es pura: misma entrada, misma salida, sin efectos secundarios. Un staticmethod aquí comunica esa independencia claramente a quien lea el código.

Errores que debes conocer

Error: Usar un método de instancia como si fuera estático —olvidando que self es obligatorio— y confundirse cuando Python lanza TypeError.

# ❌ Wrong
class Calculadora:
    def sumar(x, y):   # Python espera self como primer parámetro
        return x + y

c = Calculadora()
c.sumar(2, 3)  # TypeError: sumar() takes 2 positional arguments but 3 were given
# ✅ Right
class Calculadora:
    @staticmethod
    def sumar(x, y):   # sin self: le dices a Python que no inyecte nada
        return x + y

c = Calculadora()
c.sumar(2, 3)  # 5

El decorador @staticmethod desactiva la inyección automática de self; sin él, Python siempre pasa el objeto como primer argumento.


Error: Hardcodear el nombre de la clase dentro de un classmethod en lugar de usar cls, rompiendo la herencia.

# ❌ Wrong
class Animal:
    @classmethod
    def crear(cls, nombre):
        return Animal(nombre)   # siempre crea Animal, nunca la subclase

class Perro(Animal):
    pass

p = Perro.crear("Rex")
print(type(p))  # <class 'Animal'> — inesperado
# ✅ Right
class Animal:
    def __init__(self, nombre):
        self.nombre = nombre

    @classmethod
    def crear(cls, nombre):
        return cls(nombre)   # cls es Perro cuando se llama desde Perro

class Perro(Animal):
    pass

p = Perro.crear("Rex")
print(type(p))  # <class 'Perro'>

cls siempre apunta a la clase desde la que se invoca el método, lo que hace que la herencia y los constructores alternativos funcionen de forma polimórfica.

95

Dejar un comentario

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

Scroll al inicio